Skip to main content

Visualize wind data

In this example, we'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 (which we'll provide an example of).
arrow-downGFS wind data

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.
  • MAPBOX_ACCESS_TOKEN - A secret token for your Mapbox account with tilesets:write (for uploading source, creating tileset and publish job) and tilesets:read scope (for getting job status).

We'll reference these values in later steps.

ACCOUNT=your-account-name
ENDPOINT="https://api.mapbox.com/tilesets/v1"
RECIPE_LOCATION="./gfs-wind.json"
SOURCE_NAME=gfs-wind-example
SOURCE_LOCATION="./gfs-wind.grib2"
TILESET_ID=your-account-name.gfs-example
MAPBOX_ACCESS_TOKEN="<Insert your own sk.* token here>"

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 tool for understanding the structure of your data.

Running gdalinfo on this sample source, we 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

Next, we'll 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. In the example GFS data that we're processing we only have a single source file, so we'll make the following POST request once.

curl \
-X POST "${ENDPOINT}/sources/${ACCOUNT}/${SOURCE_NAME}?access_token=${MAPBOX_ACCESS_TOKEN}" \
-F file=@${SOURCE_LOCATION} \
--header "Content-Type: multipart/form-data"

Create a recipe

A recipe is a set of instructions used for processing raster data.

{
"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"
}
}
}
}

Note

Wind animation requires two components which are being selected from the source file using source_rules field. The wind components are expected to be ordered "east-west" first and "north-south" second.

The following recipe feature 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"]]],

We then name the constructed slice of data with:

"name":["to-number",["get","NETCDF_DIM_time"]],

And then organize those distinct slices of data with:

"order":"asc"

Create an empty tileset

Create an empty tileset and provide the recipe to be used for processing.

curl \
-X POST "${ENDPOINT}/${TILESET_ID}?access_token=${MAPBOX_ACCESS_TOKEN}" \
-d @"${RECIPE_LOCATION}" \
--header "Content-Type:application/json"

Note

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 be used for processing the source to this tileset. Now you can start processing your tileset by publishing it.

curl -X POST "${ENDPOINT}/${TILESET_ID}/publish?access_token=${MAPBOX_ACCESS_TOKEN}"

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 we 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>
Was this example helpful?