Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

This notebook demonstrates how to retrieve weather forecasts along a route with the Spire Weather API.

import os
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.environ.get('SPIRE_API_KEY')
BASE_URL = 'https://api.wx.spire.com'
HEADERS = {'spire-api-key': API_KEY}

Define a Route

A route consists of waypoints with latitude, longitude, and expected arrival times.

# Example: Transatlantic shipping route from New York to Southampton
base_time = datetime.utcnow().replace(hour=12, minute=0, second=0, microsecond=0)

route = [
    {
        'name': 'New York',
        'lat': 40.6892,
        'lon': -74.0445,
        'time': base_time.isoformat() + 'Z'
    },
    {
        'name': 'Mid-Atlantic 1',
        'lat': 42.0,
        'lon': -60.0,
        'time': (base_time + timedelta(hours=36)).isoformat() + 'Z'
    },
    {
        'name': 'Mid-Atlantic 2',
        'lat': 45.0,
        'lon': -40.0,
        'time': (base_time + timedelta(hours=72)).isoformat() + 'Z'
    },
    {
        'name': 'Mid-Atlantic 3',
        'lat': 48.0,
        'lon': -20.0,
        'time': (base_time + timedelta(hours=108)).isoformat() + 'Z'
    },
    {
        'name': 'Southampton',
        'lat': 50.9097,
        'lon': -1.4044,
        'time': (base_time + timedelta(hours=144)).isoformat() + 'Z'
    }
]

print("Route waypoints:")
for wp in route:
    print(f"  {wp['name']}: ({wp['lat']:.2f}, {wp['lon']:.2f}) at {wp['time']}")

Request Route Forecast

# Prepare route data for API (without names)
route_data = {
    'route': [
        {'lat': wp['lat'], 'lon': wp['lon'], 'time': wp['time']}
        for wp in route
    ]
}

# Make the POST request
response = requests.post(
    f'{BASE_URL}/forecast/route',
    headers=HEADERS,
    params={'bundles': 'basic'},
    json=route_data
)
response.raise_for_status()
data = response.json()

print(f"Received {len(data['data'])} forecast points")

Convert to DataFrame

def route_to_dataframe(response_data, waypoints):
    """Convert route forecast response to DataFrame with waypoint names."""
    records = []
    
    for i, item in enumerate(response_data['data']):
        # Match with waypoint if possible
        name = waypoints[i]['name'] if i < len(waypoints) else f"Point {i}"
        
        record = {
            'waypoint': name,
            'lat': item['location']['coordinates']['lat'],
            'lon': item['location']['coordinates']['lon'],
            'valid_time': item['times']['valid_time'],
            **item['values']
        }
        records.append(record)
    
    df = pd.DataFrame(records)
    df['valid_time'] = pd.to_datetime(df['valid_time'])
    return df

df = route_to_dataframe(data, route)
print(df.head())

Add Derived Variables

# Temperature conversion
if 'air_temperature' in df.columns:
    df['temp_celsius'] = df['air_temperature'] - 273.15

# Wind speed and direction
if 'northward_wind' in df.columns and 'eastward_wind' in df.columns:
    df['wind_speed'] = np.sqrt(df['northward_wind']**2 + df['eastward_wind']**2)
    df['wind_direction'] = (270 - np.degrees(np.arctan2(df['northward_wind'], 
                                                         df['eastward_wind']))) % 360

print(df[['waypoint', 'valid_time', 'temp_celsius', 'wind_speed']].head())

Plot Route on Map

try:
    import cartopy.crs as ccrs
    import cartopy.feature as cfeature
    
    fig, ax = plt.subplots(
        figsize=(12, 8),
        subplot_kw={'projection': ccrs.PlateCarree()}
    )
    
    ax.add_feature(cfeature.LAND, alpha=0.3)
    ax.add_feature(cfeature.OCEAN, alpha=0.3)
    ax.add_feature(cfeature.COASTLINE)
    ax.add_feature(cfeature.BORDERS, linestyle=':')
    
    # Plot route
    ax.plot(df['lon'], df['lat'], 'b-o', transform=ccrs.PlateCarree(), 
            linewidth=2, markersize=8, label='Route')
    
    # Add labels
    for _, row in df.iterrows():
        ax.annotate(row['waypoint'], 
                    xy=(row['lon'], row['lat']),
                    xytext=(5, 5), textcoords='offset points',
                    fontsize=9, transform=ccrs.PlateCarree())
    
    ax.set_extent([-80, 5, 35, 55])
    ax.gridlines(draw_labels=True)
    ax.set_title('Transatlantic Route')
    
    plt.show()
    
except ImportError:
    print("Cartopy not installed. Install with: conda install -c conda-forge cartopy")
    
    # Simple fallback plot
    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(df['lon'], df['lat'], 'b-o', linewidth=2, markersize=8)
    for _, row in df.iterrows():
        ax.annotate(row['waypoint'], xy=(row['lon'], row['lat']), 
                    xytext=(5, 5), textcoords='offset points')
    ax.set_xlabel('Longitude')
    ax.set_ylabel('Latitude')
    ax.set_title('Route')
    plt.show()

Weather Along Route

fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

# Temperature
axes[0].plot(df['waypoint'], df['temp_celsius'], 'r-o', linewidth=2, markersize=8)
axes[0].set_ylabel('Temperature (°C)')
axes[0].grid(True, alpha=0.3)
axes[0].set_title('Weather Conditions Along Route')

# Wind Speed
axes[1].plot(df['waypoint'], df['wind_speed'], 'g-o', linewidth=2, markersize=8)
axes[1].set_ylabel('Wind Speed (m/s)')
axes[1].grid(True, alpha=0.3)
axes[1].set_xlabel('Waypoint')

plt.tight_layout()
plt.show()

Maritime Route with Waves

If you have maritime bundle access:

try:
    # Request with maritime bundle
    response = requests.post(
        f'{BASE_URL}/forecast/route',
        headers=HEADERS,
        params={'bundles': 'basic,maritime'},
        json=route_data
    )
    response.raise_for_status()
    maritime_data = response.json()
    
    maritime_df = route_to_dataframe(maritime_data, route)
    
    if 'significant_wave_height' in maritime_df.columns:
        print("Maritime data available!")
        print(maritime_df[['waypoint', 'significant_wave_height']].head())
    else:
        print("Maritime variables not in response. Check subscription.")
        
except requests.exceptions.HTTPError as e:
    print(f"Maritime bundle may not be available: {e}")

Route Summary Table

# Create summary table
summary_cols = ['waypoint', 'valid_time', 'temp_celsius', 'wind_speed']
if 'relative_humidity' in df.columns:
    summary_cols.append('relative_humidity')

summary = df[summary_cols].copy()
summary['temp_celsius'] = summary['temp_celsius'].round(1)
summary['wind_speed'] = summary['wind_speed'].round(1)

print("Route Weather Summary")
print("=" * 60)
print(summary.to_string(index=False))

Next Steps