Oceanicsdotio

Tide gauge data

October 14, 2020


Concept

In a perfect world, I would know the tides before going out on the water. But I forget all the time.

I’ve worked with teams that operated entirely around the tides, and others that operated regardless of tides. In the Northeast the tides are a constant feature, while in Louisiana they barely exist.

The historical data are useful for providing context to events. Tide gauges are also more generally water level gauges, which mean that they include storm surge and wind-influenced levels.

Real-time data is an aid to navigation.

Future conditions can help plan operations and assess risk. Let’s no get into sea level rise though.

Source

US water levels are available from NOAA Tides & Currents. Your tax dollars at work.

There are 6 stations in Maine. These generally also have metocean data associated with them, like wind, air pressure, and water temperature.

Lucky for us the Tides & Currents API is documented! Unfortunately, the responses are not really self-describing. “What do the letters in the data columns represent?” asks their own help page. Great. If you feel defensive about your own API design, you might want to reconsider your choices.

Doesn’t seem like you can request with a geographic range.

Looking at the network requests trigger by a map load, you can see that all the stations are retrieved from a metadata API (/mdapi/prod), and then each is populated with individual requests to the core API (/api/prod). FYI, it’s SOAP/XML underneath a JSON friendly wrapper.

The docs for the metadata API indicate that yes indeed you can serach by radius! But, only when requesting a specific station. In other words, you can return nearby stations as part of the request for a named station. Useful for comparisons and topological queries.

So, we absolutely must get all stations, and then filter them in the client. Fine NOAA. They “expand” linked data by default, but let’s not do that. This initial request only takes 55 ms, and can definitely be cached. But it also moves 600KB of data just to get references to 11 links that I need to follow.

The docs describe all of the query parameters for fetching the actual data.

There are many time interval options. Some time based query parameter is required. Use date=latest for a single observation, or range=24 for past day.

You also have to explicit declare a product value or array, datum, time_zone, units. Let’s use Mean Lower Low Water (mllw) cause it’s the default. We’ll go with daylight savings local time, and metric.

They want you to provide information about the accessing application. This is good form, gotta justify providing public data.

The final thing is:

https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=latest&station=8454000&product=water_level&datum=mllw&units=metric&time_zone=lst_ldt&application=oceanics.io&format=json

Mapping the data

Our stations query (as a React hook) is therefore:

useEffect(() => {
    
    if (!map || !animatedIcons) return;
    const id = "tidal-stations";
    const extent = [-71.190, 40.975, -63.598, 46.525];

    map.addImage(id, animatedIcons.waterLevel, { pixelRatio: 4 });

    (async () => {
        const queue = await fetch("https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=waterlevels")
            .then(r => r.json())
            .then(({stations}) => {
                return stations.filter(({lat, lng}) => {
                    return lng >= extent[0] && lng <= extent[2] && lat >= extent[1] && lat <= extent[3];
                }).map(({id})=>{
                    return fetch(`https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=latest&station=${id}&product=water_level&datum=mllw&units=metric&time_zone=lst_ldt&application=oceanics.io&format=json`).then(r => r.json())
                    }
                );
            });
        
        map.addLayer({
            id,
            type: 'symbol',
            source: parseFeatureData({
                features: await Promise.all(queue), 
                standard: "noaa"
            }),
            layout: {
                'icon-image': id
            }
        });     
    })();
}, [map, animatedIcons]);

If you are not familiar with the syntax, here’s what is happening.

We’re waiting for the Mapbox data structure to load, as well as some HTML canvas based icons to create a custom symbol layer on the map.

Station metadata is retrieved then filtered down to only the stations within a bounding box. This could be any spatial query!

The selected stations are queried for the metocean data, and then these are parsed into a GeoJSON FeatureCollection embedded in a GeoJSON layer Mapbox source object.

Custom animated icon

In this case, the icon is just a small blue square; a placeholder for a data-driven tide gauge symbol.

The problem is that you only get one datum per request, like Mean Lowest Low Water (mllw), but you need two to be able to render a progress-bar style of gauge.

So another request the datums product for every location before you can paint the map. Blech.