> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/mapbox-tiling-service/llms.txt)

# Visualize wind data

In this example, you'll process GFS wind data from NOAA using [Raster MTS](https://docs.mapbox.com/mapbox-tiling-service/raster/#what-is-raster-mts). The goal is to create a `raster-array` [source](https://docs.mapbox.com/style-spec/reference/sources/#raster-array) for animating wind data based on speed and direction [using particle animation](https://docs.mapbox.com/style-spec/reference/layers/#raster-particle).

## Prerequisites

To follow along, you'll need:

-   [GFS](https://www.ncei.noaa.gov/products/weather-climate-models/global-forecast) data provided below:

[GFS wind data](https://mts-recipe-example-data.s3.amazonaws.com/gfs-wind.nc)

### 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](https://docs.mapbox.com/help/glossary/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 with `tilesets:write` (for uploading source, creating tileset and publish job) and `tilesets:read` scope (for getting job status).

And when replacing these variables in code snippets or commands, refer to this list:

```js
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`](https://gdal.org/programs/gdalinfo.html) is an excellent resource for understanding the structure of your data.

Running `gdalinfo` on this sample source, you'll see the (truncated) output:

```bash
$ 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**

```shell
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-cli**

```shell
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:

1.  Create a `.json` file named `gfs-wind-recipe.json`.
2.  Paste in the recipe below.
3.  Replace `{ACCOUNT}` and `{SOURCE_NAME}` with your own values.

```json
{
  "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 (warning): Note**
> 
> 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.

```json
"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:

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

Then the recipe organizes those distinct slices of data with:

```json
"order":"asc"
```

## Create an empty tileset

Now you will create an empty tileset and provide the recipe to be used for processing.

**Curl**

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

**tilesets-cli**

```shell
tilesets create $TILESET_ID \
    --recipe $RECIPE_LOCATION \
    --name "tilesets example tileset" 
```

  

> **Note (warning): 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.
> 
> ```json
> {
>   "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**

```shell
curl -X POST "${ENDPOINT}/${TILESET_ID}/publish?access_token=${YOUR_SECRET_MAPBOX_ACCESS_TOKEN}"
```

**tilesets-cli**

```shell
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`.

```bash
$ curl "\${ENDPOINT}/\${TILESET_ID}/jobs/{job_id}?access_token=\${MAPBOX_ACCESS_TOKEN}"
```

## Add tileset to map

Using this tileset as a [`raster-array` source](https://docs.mapbox.com/style-spec/reference/sources/#raster-array) you will add the source to our map as a [`raster-particle` layer](https://docs.mapbox.com/style-spec/reference/layers/#raster-particle). This will render a particle animation on a map based on wind velocity and direction.

```html
<!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.25.0/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v3.25.0/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>
  const map = new mapboxgl.Map({
    // TO MAKE THE MAP APPEAR YOU MUST
    // ADD YOUR ACCESS TOKEN FROM
    // https://account.mapbox.com
    accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
    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](#create-a-recipe) step.