Visualize wind data
In this example, you'll process GFS wind data from NOAA using Raster MTS.
The goal is to create a raster-array source for animating wind data based on speed and direction using particle animation.
Prerequisites
To follow along, you'll need:
- GFS data provided below:
Environment variables
Throughout this example, you'll use the following required environment variables:
ACCOUNT- Set this to your account username.ENDPOINT- The path to the Mapbox Tilesets API.RECIPE_LOCATION- The local file location of your Raster MTS recipe.SOURCE_NAME- The name used to create your tileset source. Once your tileset source has been created, you must reference that returned tileset source URI in your recipe.SOURCE_LOCATION- The local file location of your raster source.TILESET_ID- A unique identifier given to your tileset. A tileset ID always starts with your Mapbox username, followed by the tileset's unique alphanumeric identifier:username.identifier.YOUR_SECRET_MAPBOX_ACCESS_TOKEN- A secret token for your Mapbox account withtilesets:write(for uploading source, creating tileset and publish job) andtilesets:readscope (for getting job status).
And when replacing these variables in code snippets or commands, refer to this list:
ACCOUNT=your-account-name
ENDPOINT="https://api.mapbox.com/tilesets/v1"
RECIPE_LOCATION="path/to/file/gfs-wind-recipe.json"
SOURCE_NAME=gfs-wind-source
SOURCE_LOCATION="/path/to/file/gfs-wind.nc"
TILESET_ID=your-account-name.gfs-wind-tileset
YOUR_SECRET_MAPBOX_ACCESS_TOKEN="<Insert your own sk.* token here>"
(Optional) Designing a Raster MTS recipe
The task of a Raster MTS recipe is to describe how source data is mapped to the MRT-formatted tiles which Raster MTS delivers.
The GDAL tool gdalinfo is an excellent resource for understanding the structure of your data.
Running gdalinfo on this sample source, you'll see the (truncated) output:
$gdalinfo gfs-wind.nc
Driver: netCDF/Network Common Data Format
Files: gfs-wind.nc
Size is 512, 512
Metadata:
NC_GLOBAL#Analysis_or_forecast_generating_process_identifier_defined_by_originating_centre=Analysis from GFS (Global Forecast System)
NC_GLOBAL#Conventions=CF-1.6
NC_GLOBAL#featureType=GRID
NC_GLOBAL#geospatial_lat_max=90
NC_GLOBAL#geospatial_lat_min=-90
NC_GLOBAL#geospatial_lon_max=180
NC_GLOBAL#geospatial_lon_min=-180
NC_GLOBAL#GRIB_table_version=2,1
NC_GLOBAL#History=Translated to CF-1.0 Conventions by Netcdf-Java CDM (CFGridCoverageWriter)
Original Dataset = GFS_Global_0p25deg_20240531_1200.grib2#SRC; Translation Date = 2024-05-31T17:43:29.060Z
NC_GLOBAL#Originating_or_generating_Center=US National Weather Service, National Centres for Environmental Prediction (NCEP)
NC_GLOBAL#Originating_or_generating_Subcenter=0
NC_GLOBAL#Type_of_generating_process=Forecast
Subdatasets:
SUBDATASET_1_NAME=NETCDF:"gfs-wind.nc":u-component_of_wind_height_above_ground
SUBDATASET_1_DESC=[57x1x721x1440] winds (32-bit floating-point)
SUBDATASET_2_NAME=NETCDF:"gfs-wind.nc":v-component_of_wind_height_above_ground
SUBDATASET_2_DESC=[57x1x721x1440] winds (32-bit floating-point)
Corner Coordinates:
Upper Left ( 0.0, 0.0)
Lower Left ( 0.0, 512.0)
Upper Right ( 512.0, 0.0)
Lower Right ( 512.0, 512.0)
Center ( 256.0, 256.0)
Create a tileset source
First you will create a tileset source by uploading our GFS data to MTS. If you're using the API directly, you'll need to make a request for each individual source file that you reference in your recipe. This example is processing only one single source file, so you'll only need to make the following POST request once.
curl \
-X POST "${ENDPOINT}/sources/${ACCOUNT}/${SOURCE_NAME}?access_token=${YOUR_SECRET_MAPBOX_ACCESS_TOKEN}" \
-F file=@${SOURCE_LOCATION} \
--header "Content-Type: multipart/form-data"
tilesets upload-raster-source $ACCOUNT $SOURCE_NAME $SOURCE_LOCATION
Create a recipe
Now that you have a tileset source, you will create a recipe.
A recipe is a set of instructions used for processing raster data. To use your recipe you will need to create a file and add your own information:
- Create a
.jsonfile namedgfs-wind-recipe.json. - Paste in the recipe below.
- Replace
{ACCOUNT}and{SOURCE_NAME}with your own values.
{
"version": 1,
"type": "rasterarray",
"sources": [
{
"uri": "mapbox://tileset-source/{ACCOUNT}/{SOURCE_NAME}"
}
],
"minzoom": 0,
"maxzoom": 3,
"layers": {
// Layer name "10winds" is based on naming conventions from https://cfconventions.org/
// 10winds represents wind vectors at 10 meters above ground.
"10winds": {
"tilesize": 256,
"resampling": "bilinear",
"buffer": 1,
"source_rules": {
"filter":
[
["all",["==",["get","NETCDF_VARNAME"],"u-component_of_wind_height_above_ground"],
["==",["get","NETCDF_DIM_height_above_ground2"],"10"]],
["all",["==",["get","NETCDF_VARNAME"],"v-component_of_wind_height_above_ground"],
["==",["get","NETCDF_DIM_height_above_ground2"],"10"]]
],
"name": ["to-number", ["get", "NETCDF_DIM_time"]],
"sort_key":["to-number",["get","NETCDF_DIM_time"]],
"order": "asc"
}
}
}
}
Wind animation requires two components which are selected from the source file using the source_rules field. The wind components are expected to be ordered "east-west" first and "north-south" second.
The recipe feature from above shows that Raster MTS uses the u-component_of_wind_height_above_ground and v-component_of_wind_height_above_ground bands to form the wind vector, where the NETCDF_DIM_height_above_ground2 dimension is 10.
"filter":
[["all",["==",["get","NETCDF_VARNAME"],"u-component_of_wind_height_above_ground"],
["==",["get","NETCDF_DIM_height_above_ground2"],"10"]],
["all",["==",["get","NETCDF_VARNAME"],"v-component_of_wind_height_above_ground"],
["==",["get","NETCDF_DIM_height_above_ground2"],"10"]]],
The recipe then names the constructed slice of data with:
"name":["to-number",["get","NETCDF_DIM_time"]],
Then the recipe organizes those distinct slices of data with:
"order":"asc"
Create an empty tileset
Now you will create an empty tileset and provide the recipe to be used for processing.
curl \
-X POST "${ENDPOINT}/${TILESET_ID}?access_token=${YOUR_SECRET_MAPBOX_ACCESS_TOKEN}" \
-d @"${RECIPE_LOCATION}" \
--header "Content-Type:application/json"
tilesets create $TILESET_ID \
--recipe $RECIPE_LOCATION \
--name "tilesets example tileset"
If you're using the API directly, you'll need to wrap your recipe in an additional JSON structure to include a recipe and name property.
{
"recipe": {...},
"name": "example_tileset_name"
}
Publish a tileset
At this point, you have uploaded a source, created an empty tileset and provided a recipe to process the source for this tileset. Now you can start processing your tileset by publishing it.
curl -X POST "${ENDPOINT}/${TILESET_ID}/publish?access_token=${YOUR_SECRET_MAPBOX_ACCESS_TOKEN}"
tilesets publish $TILESET_ID
Get your tileset's job information
At this point, Raster MTS will begin to process your job with a job_id. You can retrieve your job's information by calling the API endpoint below with your job_id.
$curl "\${ENDPOINT}/\${TILESET_ID}/jobs/{job_id}?access_token=\${MAPBOX_ACCESS_TOKEN}"
Add tileset to map
Using this tileset as a raster-array source you will add the source to our map as a raster-particle layer. This will render a particle animation on a map based on wind velocity and direction.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Visualize wind data</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script>
// TO MAKE THE MAP APPEAR YOU MUST
// ADD YOUR ACCESS TOKEN FROM
// https://account.mapbox.com
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
maxZoom: 2,
minZoom: 2,
zoom: 2,
center: [-28, 47],
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/dark-v11'
});
map.on('load', () => {
map.addSource('raster-array-source', {
'type': 'raster-array',
// Replace this URL with a 'mapbox://TILESET_ID'
'url': 'mapbox://mapbox.gfs-winds',
'tileSize': 512
});
map.addLayer({
'id': 'wind-layer',
'type': 'raster-particle',
'source': 'raster-array-source',
'source-layer': '10winds',
'paint': {
'raster-particle-speed-factor': 0.4,
'raster-particle-fade-opacity-factor': 0.9,
'raster-particle-reset-rate-factor': 0.4,
'raster-particle-count': 4000,
'raster-particle-max-speed': 40,
'raster-particle-color': [
'interpolate',
['linear'],
['raster-particle-speed'],
1.5,
'rgba(134,163,171,256)',
2.5,
'rgba(126,152,188,256)',
4.12,
'rgba(110,143,208,256)',
4.63,
'rgba(110,143,208,256)',
6.17,
'rgba(15,147,167,256)',
7.72,
'rgba(15,147,167,256)',
9.26,
'rgba(57,163,57,256)',
10.29,
'rgba(57,163,57,256)',
11.83,
'rgba(194,134,62,256)',
13.37,
'rgba(194,134,63,256)',
14.92,
'rgba(200,66,13,256)',
16.46,
'rgba(200,66,13,256)',
18.0,
'rgba(210,0,50,256)',
20.06,
'rgba(215,0,50,256)',
21.6,
'rgba(175,80,136,256)',
23.66,
'rgba(175,80,136,256)',
25.21,
'rgba(117,74,147,256)',
27.78,
'rgba(117,74,147,256)',
29.32,
'rgba(68,105,141,256)',
31.89,
'rgba(68,105,141,256)',
33.44,
'rgba(194,251,119,256)',
42.18,
'rgba(194,251,119,256)',
43.72,
'rgba(241,255,109,256)',
48.87,
'rgba(241,255,109,256)',
50.41,
'rgba(256,256,256,256)',
57.61,
'rgba(256,256,256,256)',
59.16,
'rgba(0,256,256,256)',
68.93,
'rgba(0,256,256,256)',
69.44,
'rgba(256,37,256,256)'
]
}
});
});
</script>
</body>
</html>
Troubleshooting
Here are solutions to some common integration problems:
Empty Tileset
- If you receive an empty tileset error when trying to publish, check that you updated the recipe with your credentials in the Create A Recipe step.