Skip to content

NOTAM Integration - Design Document

Executive Summary

This document describes the design and implementation of automatic NOTAM (Notices to Airmen) integration for the drone operations management system. The feature fetches and filters NOTAMs from the NATS Public Information Bulletin (PIB) XML feed to provide airspace activity awareness during flight planning.

Business Value

  • Enhanced Safety: Pre-flight awareness of airspace restrictions and hazards
  • Regulatory Compliance: Documented NOTAM review for operational records
  • Risk Mitigation: Early identification of conflicts with other airspace users
  • Operational Efficiency: Automatic NOTAM lookup eliminates manual research
  • User Experience: Cached NOTAMs (24 hours) provide fast project page loads

Key Capabilities

  • Automatic Fetching: NOTAMs retrieved during project creation and cached
  • Geographic Filtering: Haversine distance calculation for point-in-circle intersection
  • UAS-Specific Filtering: Code23=WU and "UAS" keyword detection
  • Validity Date Filtering: Only shows NOTAMs active on flight date
  • Intelligent Caching: 24-hour cache with automatic refresh on project view
  • Coordinate Conversion: DDMMSS to WGS84 decimal degrees transformation

Architecture Overview

High-Level System Diagram

┌────────────────────────────────────────────────────────────────────┐
│                         User Journey                                │
├────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  [Step 1: Location] ──> [Step 2: Details + Submit] ──> [NOTAM Fetch]│
│                              │                               │       │
│                              │                               ▼       │
│                              │                    ┌─────────────────┐│
│                              │                    │  NOTAMAPI       ││
│                              │                    │  .fetch_notams()││
│                              │                    └────────┬────────┘│
│                              │                             │         │
│                              │          ┌──────────────────┴─────┐   │
│                              │          │ Download NATS PIB XML  │   │
│                              │          │ (2-5 MB, ~30s timeout) │   │
│                              │          └──────────┬─────────────┘   │
│                              │                     │                 │
│                              │                     ▼                 │
│                              │          ┌─────────────────────────┐  │
│                              │          │ Parse XML (~500 NOTAMs) │  │
│                              │          └──────────┬──────────────┘  │
│                              │                     │                 │
│                              │          ┌──────────▼──────────┐      │
│                              │          │ Three-Stage Filter: │      │
│                              │          │ 1. Geography        │      │
│                              │          │ 2. UAS Relevance    │      │
│                              │          │ 3. Validity Date    │      │
│                              │          └──────────┬──────────┘      │
│                              │                     │                 │
│                              ▼                     ▼                 │
│                       [Step 3: Viability] [Session Storage]         │
│                              │                     │                 │
│                              ▼                     ▼                 │
│                       [Step 4: Toggles]  ┌────────────────┐         │
│                              │            │ Database Save  │         │
│                              │            │ (Project.      │         │
│                              │            │  notam_data)   │         │
│                              │            └────────┬───────┘         │
│                              ▼                     │                 │
│                       [Dashboard] ──────────> [Project View]        │
│                                                    │                 │
│                                                    ▼                 │
│                                             ┌─────────────┐          │
│                                             │Cache Valid? │          │
│                                             └──────┬──────┘          │
│                                                    │                 │
│                                          No ───────┤                 │
│                                                    │                 │
│                                                    ▼                 │
│                                             [Refetch & Update]       │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Component Interaction Flow

┌──────────────────┐
│ routes_new_      │
│ project.py       │
│                  │
│ new_project_     │
│ details()        │
└────────┬─────────┘
         │ (on form submit)
         │
         ▼
┌──────────────────────────────────────────────────────────┐
│ app/utils/notam_api.py                                    │
├──────────────────────────────────────────────────────────┤
│                                                            │
│  NOTAMAPI.fetch_notams(lat, lon, flight_date)            │
│    │                                                       │
│    ├──> 1. _download_and_parse_xml()                     │
│    │        - GET https://pibs.nats.co.uk/.../PIB.xml    │
│    │        - Parse XML with ElementTree                  │
│    │        - Navigate FIR → AD → NotamList → Notam      │
│    │                                                       │
│    ├──> 2. _extract_notams_from_xml(root)                │
│    │        - Loop through all <Notam> elements          │
│    │        - _parse_single_notam() for each             │
│    │        - Extract: NOF, ItemE, coordinates, dates    │
│    │        - _parse_coordinates(DDMMSS → decimal)       │
│    │        - _parse_validity_date(YYMMDDHHmm)           │
│    │                                                       │
│    ├──> 3. _filter_by_geography(notams, lat, lon)        │
│    │        - _haversine_distance() for each NOTAM       │
│    │        - Include if distance <= radius              │
│    │        - Add distance_from_location_nm to dict      │
│    │                                                       │
│    ├──> 4. _filter_by_uas_relevance(notams)              │
│    │        - Include if Code23 == 'WU'                  │
│    │        - OR if 'UAS' in ItemE                       │
│    │                                                       │
│    ├──> 5. _filter_by_validity_date(notams, date)        │
│    │        - Include if start <= flight_date <= end     │
│    │                                                       │
│    └──> 6. Return standardized response                  │
│             {                                              │
│               'success': True,                            │
│               'notam_data': {                             │
│                 'has_notams': bool,                       │
│                 'notams': [...],                          │
│                 'count': int                              │
│               },                                          │
│               'metadata': {                               │
│                 'fetched_at': timestamp,                  │
│                 'cache_valid_until': timestamp,           │
│                 'total_notams_parsed': int,               │
│                 'filtered_count': int                     │
│               }                                           │
│             }                                             │
│                                                            │
└────────────────────────────────────────────────────────────┘
         │
         ▼
┌──────────────────┐
│ Session Storage  │
│ notam_response   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ routes_new_      │
│ project.py       │
│                  │
│ new_project_     │
│ toggles()        │
│                  │
│ → Database Save  │
└──────────────────┘

Filtering Strategy

Why Three-Stage Sequential Filtering?

Problem: NATS PIB XML contains ~500 NOTAMs covering all UK airspace. Most are irrelevant to a specific UAS operation.

Solution: Apply three sequential filters to reduce from 500 → typically 0-10 relevant NOTAMs:

  1. Geographic Filter: Reduce to NOTAMs within their specified radius (~50-100 remain)
  2. UAS Relevance Filter: Reduce to UAS-specific or UAS-mentioned NOTAMs (~10-30 remain)
  3. Validity Date Filter: Reduce to NOTAMs active on flight date (~0-10 remain)

Benefits: - Performance: Early reduction minimise processing for later stages - Relevance: Users only see NOTAMs directly applicable to their operation - Safety: Conservative approach (includes borderline cases)

Filter Stage 1: Geographic Filtering

Algorithm: Haversine formula for great-circle distance

def _filter_by_geography(notams, flight_lat, flight_lon):
    filtered = []
    for notam in notams:
        distance = haversine_distance(
            flight_lat, flight_lon,
            notam['center_lat'], notam['center_lon']
        )
        if distance <= notam['radius']:
            notam['distance_from_location_nm'] = round(distance, 1)
            filtered.append(notam)
    return filtered

Haversine Formula:

a = sin²(Δlat/2) + cos(lat1) × cos(lat2) × sin²(Δlon/2)
c = 2 × atan2(√a, √(1−a))
distance = R × c

Where R = 3440.065 nautical miles (Earth's radius)

Why Haversine? - Accurate for distances <1000 NM (sufficient for UK) - Faster than Vincenty formula (geodesic) - Accounts for Earth's curvature (not flat distance)

Example:

Flight Location: Edinburgh (55.9533°N, 3.1883°W)

NOTAM A: Centre at Glasgow (55.8642°N, 4.2518°W), Radius 50 NM
  → Distance: 35 NM
  → Result: ✓ Included (35 < 50)

NOTAM B: Centre at London (51.5074°N, 0.1278°W), Radius 20 NM
  → Distance: 280 NM
  → Result: ✗ Excluded (280 > 20)

Typical Reduction: 500 → ~50-100 NOTAMs

Filter Stage 2: UAS Relevance Filtering

Criteria (OR logic):

def _filter_by_uas_relevance(notams):
    filtered = []
    for notam in notams:
        code23 = notam.get('code23', '').upper()
        item_e = notam.get('item_e', '').upper()

        if code23 == 'WU' or 'UAS' in item_e:
            filtered.append(notam)
    return filtered

Code23 Values: - WU: UAS-specific activity (most explicit) - WR: Restricted area (may affect UAS) - WD: Danger area (may affect UAS) - WT: Training/military (usually manned aircraft)

Why Include "UAS" Text Match? - Not all UAS-relevant NOTAMs have Code23=WU - Some use generic codes (WR, WD) but mention UAS in description - Conservative approach: better to show than hide

Example NOTAMs:

<!-- Included: Code23=WU -->
<QLine><Code23>WU</Code23></QLine>
<ItemE>UNMANNED ACFT FLYING DISPLAY...</ItemE>

<!-- Included: "UAS" in description -->
<QLine><Code23>WR</Code23></QLine>
<ItemE>RESTRICTED AIRSPACE. UAS OPERATIONS PROHIBITED.</ItemE>

<!-- Excluded: Not UAS-related -->
<QLine><Code23>WT</Code23></QLine>
<ItemE>MILITARY JET TRAINING EXERCISE.</ItemE>

Typical Reduction: 50-100 → ~10-30 NOTAMs

Filter Stage 3: Validity Date Filtering

Algorithm: Date range intersection

def _filter_by_validity_date(notams, flight_date):
    flight_dt = datetime.strptime(flight_date, '%Y-%m-%d')
    filtered = []

    for notam in notams:
        start_dt = parse_iso(notam['start_validity'])
        end_dt = parse_iso(notam['end_validity'])

        if start_dt.date() <= flight_dt.date() <= end_dt.date():
            filtered.append(notam)

    return filtered

Date Format Conversion: - Input: YYMMDDHHmm (e.g., 2512290700 = 29 Dec 2025, 07:00 UTC) - Storage: ISO 8601 (e.g., 2025-12-29T07:00:00+00:00) - Comparison: Date-only (ignore time component for simplicity)

Edge Cases: 1. Missing dates: Excluded (cannot validate) 2. Permanent NOTAMs: Included if start_validity in past 3. Future NOTAMs: Excluded if start_validity > flight_date

Example:

Flight Date: 2026-01-10

NOTAM 1: 2025-12-29 → 2026-01-10  ✓ Included (intersects)
NOTAM 2: 2026-01-11 → 2026-01-15  ✗ Excluded (starts after)
NOTAM 3: 2025-12-01 → 2026-01-09  ✗ Excluded (ends before)
NOTAM 4: 2025-11-01 → 2026-02-28  ✓ Included (spans date)

Typical Reduction: 10-30 → ~0-10 NOTAMs


Coordinate Conversion

DDMMSS to Decimal Degrees

Input Format: DDMMSS[N|S]DDDMMSS[E|W] - DD: Degrees (2 digits for lat, 3 for lon) - MM: Minutes (2 digits) - SS: Seconds (2 digits, optional) - Hemisphere: N/S for latitude, E/W for longitude

Example: 5408N00316W - Latitude: 5408N = 54°08'00"N - Degrees: 54 - Minutes: 08 - Seconds: 00 (implied) - Decimal: 54 + (8/60) + (0/3600) = 54.133333° - Hemisphere: N (positive)

  • Longitude: 00316W = 003°16'00"W
  • Degrees: 003
  • Minutes: 16
  • Seconds: 00 (implied)
  • Decimal: 3 + (16/60) + (0/3600) = 3.266667°
  • Hemisphere: W (negative)

  • Result: (54.133333, -3.266667)

Conversion Algorithm

def _parse_coordinates(coord_str):
    # Split on hemisphere indicators
    if 'N' in coord_str:
        lat_str, lon_part = coord_str.split('N')
        lat_sign = +1
    elif 'S' in coord_str:
        lat_str, lon_part = coord_str.split('S')
        lat_sign = -1
    else:
        return None

    if 'E' in lon_part:
        lon_str = lon_part.replace('E', '')
        lon_sign = +1
    elif 'W' in lon_part:
        lon_str = lon_part.replace('W', '')
        lon_sign = -1
    else:
        return None

    # Parse latitude (DDMMSS or DDMM)
    if len(lat_str) == 6:  # DDMMSS
        lat_deg = int(lat_str[:2])
        lat_min = int(lat_str[2:4])
        lat_sec = int(lat_str[4:6])
    elif len(lat_str) == 4:  # DDMM
        lat_deg = int(lat_str[:2])
        lat_min = int(lat_str[2:4])
        lat_sec = 0
    else:
        return None

    latitude = (lat_deg + lat_min/60.0 + lat_sec/3600.0) * lat_sign

    # Parse longitude (DDDMMSS or DDDMM)
    if len(lon_str) == 7:  # DDDMMSS
        lon_deg = int(lon_str[:3])
        lon_min = int(lon_str[3:5])
        lon_sec = int(lon_str[5:7])
    elif len(lon_str) == 5:  # DDDMM
        lon_deg = int(lon_str[:3])
        lon_min = int(lon_str[3:5])
        lon_sec = 0
    else:
        return None

    longitude = (lon_deg + lon_min/60.0 + lon_sec/3600.0) * lon_sign

    return (latitude, longitude)

Format Detection

DDMMSS vs DDMM: - Length Check: 6 digits = DDMMSS, 4 digits = DDMM (latitude) - Automatic: No configuration needed - Seconds Default: If DDMM format, seconds = 0

Validation: - Hemisphere indicators must be present (N/S, E/W) - Latitude: -90 to 90 - Longitude: -180 to 180 - Minutes: 0-59 - Seconds: 0-59

Error Handling:

try:
    coords = _parse_coordinates(coord_str)
    if coords is None:
        logger.warning(f"Failed to parse: {coord_str}")
        return None  # Skip this NOTAM
except Exception as e:
    logger.debug(f"Coordinate parse error: {e}")
    return None


Caching Strategy

Why Cache NOTAMs?

Problem: - PIB XML download takes 10-20 seconds - XML file is 2-5 MB - Parsing ~500 NOTAMs takes 1-2 seconds - Total latency: ~12-23 seconds per fetch

Solution: - Cache in database for 24 hours - Check cache validity on project view - Automatic refresh if expired

Benefits: - Performance: <100ms cached lookup vs 12-23s fresh fetch - API Courtesy: Reduces load on NATS servers - User Experience: Fast page loads - Data Freshness: 24-hour balance between freshness and performance

Cache Implementation

Storage:

# Database column (Project model)
notam_data = db.Column(db.JSON, nullable=True)

# Structure:
{
  'success': True,
  'notam_data': { ... },
  'metadata': {
    'fetched_at': '2026-01-02T14:30:00+00:00',
    'cache_valid_until': '2026-01-03T14:30:00+00:00',
    ...
  },
  'error': None
}

Validation:

@classmethod
def is_cache_valid(cls, notam_data_json):
    if not notam_data_json:
        return False

    try:
        cache_expiry = notam_data_json['metadata']['cache_valid_until']
        cache_expiry_dt = datetime.fromisoformat(cache_expiry)
        now_utc = datetime.utcnow()
        return now_utc < cache_expiry_dt
    except Exception:
        return False  # Invalid cache structure

Cache Lifecycle

On Project Creation (routes_new_project.py):

# Step 2: Details form submission
notam_response = NOTAMAPI.fetch_notams(latitude, longitude, flight_date)
session['notam_response'] = notam_response

# Step 4: Final submission
project.notam_data = session.get('notam_response')
db.session.add(project)
db.session.commit()

On Project View (routes_dashboard.py):

# Check cache validity
if not NOTAMAPI.is_cache_valid(project.notam_data):
    logger.info(f"NOTAM cache expired for project {project_id}, refetching")

    notam_response = NOTAMAPI.fetch_notams(
        project.latitude,
        project.longitude,
        project.dateOfFlight.strftime('%Y-%m-%d')
    )

    if notam_response.get('success'):
        project.notam_data = notam_response
        db.session.commit()
        logger.info(f"NOTAM data updated for project {project_id}")

Cache Duration Configuration

Default: 24 hours

CACHE_HOURS = int(os.getenv('NOTAM_CACHE_HOURS', '24'))

Environment Variable:

# .env file
NOTAM_CACHE_HOURS=24  # Adjust as needed

Considerations: - Shorter (6-12 hours): Better freshness, more API calls - Longer (48+ hours): Better performance, stale data risk - Recommended: 24 hours (balance for NOTAM update frequency)


Error Handling and Resilience

Error Types and Responses

Error Cause System Behaviour User Impact
Timeout XML download >30s Return error response, continue No NOTAMs shown, flash message
Network Error Connection failed Return error response, continue No NOTAMs shown, flash message
XML Parse Error Malformed XML Return error response, continue No NOTAMs shown, flash message
Coordinate Parse Error Invalid DDMMSS Skip individual NOTAM, continue NOTAM excluded, operation continues
Date Parse Error Invalid YYMMDDHHmm Skip individual NOTAM, continue NOTAM excluded, operation continues

Graceful Degradation

Philosophy: NOTAM fetch failure should not block project creation

try:
    notam_response = NOTAMAPI.fetch_notams(lat, lon, date)

    if notam_response['success']:
        # Use NOTAM data
        session['notam_response'] = notam_response
    else:
        # Log error, but continue
        logger.warning(f"NOTAM fetch failed: {notam_response['error']}")
        flash('NOTAM data unavailable - please check manually', 'warning')
        session['notam_response'] = None

except Exception as e:
    # Unexpected error - log and continue
    logger.error(f"Unexpected NOTAM error: {e}", exc_info=True)
    session['notam_response'] = None

# Project creation continues regardless

Logging Strategy

Levels: - INFO: Successful fetches, cache hits, statistics - WARNING: Individual parse failures, cache invalidity - ERROR: API failures, network errors, XML parse errors - DEBUG: Detailed filtering, coordinate parsing

Examples:

logger.info(f"Fetching NOTAMs for ({lat}, {lon}) on {date}")
logger.info(f"Extracted {total_count} NOTAMs from XML")
logger.debug(f"Geographic filter: {len(filtered)}/{len(notams)} NOTAMs")
logger.warning(f"Failed to parse coordinates for NOTAM {nof}")
logger.error(f"Timeout fetching NOTAM XML (timeout: {timeout}s)")

User Communication

Success: - No message (silent success) - NOTAMs displayed in project view

Partial Failure: - Flash message: "Some NOTAMs could not be processed" - Display available NOTAMs

Complete Failure: - Flash message: "NOTAM data unavailable - please check manually at pibs.nats.co.uk" - Provide fallback link

Example:

if notam_response['success']:
    if notam_response['notam_data']['has_notams']:
        flash(f"⚠ {count} active NOTAMs found for this location", 'warning')
    else:
        flash('✓ No active NOTAMs for this location/date', 'success')
else:
    flash(f"NOTAM fetch failed: {notam_response['error']}. Check manually.", 'danger')


Performance Optimization

Current Performance Characteristics

Fresh Fetch (no cache): 1. XML Download: 10-20 seconds (network-dependent) 2. XML Parsing: 1-2 seconds (~500 NOTAMs) 3. Geographic Filter: 0.2-0.5 seconds (Haversine calculations) 4. UAS Relevance Filter: <0.1 seconds (string matching) 5. Validity Date Filter: <0.1 seconds (date comparison) 6. Total: ~12-23 seconds

Cached Fetch: 1. Database Lookup: <100ms 2. Cache Validation: <10ms 3. Total: ~100ms

Cache Hit Rate: ~95% (most project views use cache)

Optimization Opportunities

1. XML Streaming Parser - Current: Load entire XML into memory (2-5 MB) - Alternative: Use iterparse() for incremental parsing - Benefit: Lower memory footprint - Trade-off: Slightly slower parsing

# Current approach
root = ET.fromstring(response.content)

# Streaming approach (future)
for event, elem in ET.iterparse(response.raw, events=('start', 'end')):
    if event == 'end' and elem.tag == 'Notam':
        # Process NOTAM
        elem.clear()  # Free memory

2. Parallel NOTAM Parsing - Current: Sequential loop through NOTAMs - Alternative: ThreadPoolExecutor for parallel parsing - Benefit: ~40% faster on multi-core systems - Trade-off: More complex error handling

3. Spatial Indexing - Current: Linear scan for geographic filtering - Alternative: R-tree spatial index - Benefit: O(log n) instead of O(n) for large NOTAM sets - Trade-off: Additional dependency (e.g., rtree library)

4. Incremental Updates - Current: Full XML download every 24 hours - Alternative: If NATS provides delta feed in future - Benefit: Faster updates, less bandwidth - Trade-off: More complex cache management

Memory Usage

Peak Memory (during XML processing): - XML File: 2-5 MB - Parsed Tree: 8-12 MB (ElementTree overhead) - Python Objects: 2-4 MB (NOTAM dicts) - Total: ~12-21 MB

Steady State (after caching): - Database Storage: 5-50 KB per project (JSON compressed) - In-Memory: Minimal (lazy loading)


Security Considerations

Data Source Trustworthiness

NATS as Provider: - National ATC provider (trusted authority) - Official UK aeronautical data source - No authentication required (public data)

XML Injection Risks: - ElementTree parser used (safe against XXE attacks) - No eval() or exec() on XML content - XML structure validated during parsing

Data Validation

Input Validation:

# Latitude/longitude bounds
if not (-90 <= latitude <= 90):
    raise ValueError("Invalid latitude")
if not (-180 <= longitude <= 180):
    raise ValueError("Invalid longitude")

# Date format validation
try:
    datetime.strptime(flight_date, '%Y-%m-%d')
except ValueError:
    raise ValueError("Invalid date format")

Output Sanitization: - NOTAM text displayed in HTML (Jinja2 auto-escapes) - No user-controlled data in NOTAM responses - JSON structure validated before storage

Privacy and Compliance

No Personal Data: - NOTAMs are public aviation safety information - No user tracking in NOTAM API - Cache stored in project table (existing permissions apply)

GDPR Compliance: - No personal data collected - Project-level data (same retention as project) - User can delete project (cascade deletes NOTAM cache)


Testing Strategy

Unit Tests

Coordinate Parsing:

def test_parse_ddmmss_format():
    assert parse_coordinates('5408N00316W') == (54.133333, -3.266667)

def test_parse_ddmm_format():
    assert parse_coordinates('5408N00316W') == (54.133333, -3.266667)

def test_parse_southern_hemisphere():
    assert parse_coordinates('3400S15100E') == (-34.0, 151.0)

Date Parsing:

def test_parse_validity_date():
    assert parse_validity_date('2512290700') == datetime(2025, 12, 29, 7, 0)

def test_parse_invalid_date():
    assert parse_validity_date('999999') is None

Haversine Distance:

def test_haversine_distance():
    # Edinburgh to Glasgow
    dist = haversine_distance(55.9533, -3.1883, 55.8642, -4.2518)
    assert 34 < dist < 36  # ~35 NM

Geographic Filtering:

def test_filter_by_geography_included():
    notams = [{'center_lat': 55.86, 'center_lon': -4.25, 'radius': 50}]
    filtered = filter_by_geography(notams, 55.95, -3.19)
    assert len(filtered) == 1

def test_filter_by_geography_excluded():
    notams = [{'center_lat': 51.50, 'center_lon': -0.12, 'radius': 20}]
    filtered = filter_by_geography(notams, 55.95, -3.19)
    assert len(filtered) == 0

UAS Relevance Filtering:

def test_filter_by_uas_code23_wu():
    notams = [{'code23': 'WU', 'item_e': 'Some text'}]
    filtered = filter_by_uas_relevance(notams)
    assert len(filtered) == 1

def test_filter_by_uas_text_mention():
    notams = [{'code23': 'WR', 'item_e': 'UAS PROHIBITED'}]
    filtered = filter_by_uas_relevance(notams)
    assert len(filtered) == 1

Validity Date Filtering:

def test_filter_by_validity_intersects():
    notams = [{
        'start_validity': '2025-12-29T07:00:00+00:00',
        'end_validity': '2026-01-10T18:00:00+00:00'
    }]
    filtered = filter_by_validity_date(notams, '2026-01-10')
    assert len(filtered) == 1

Integration Tests

Full Fetch Workflow:

def test_fetch_notams_success():
    result = NOTAMAPI.fetch_notams(55.9533, -3.1883, '2026-01-10')
    assert result['success'] is True
    assert 'notam_data' in result
    assert 'metadata' in result

Cache Validation:

def test_cache_valid():
    cache_data = {
        'metadata': {
            'cache_valid_until': (datetime.utcnow() + timedelta(hours=12)).isoformat()
        }
    }
    assert NOTAMAPI.is_cache_valid(cache_data) is True

def test_cache_expired():
    cache_data = {
        'metadata': {
            'cache_valid_until': (datetime.utcnow() - timedelta(hours=1)).isoformat()
        }
    }
    assert NOTAMAPI.is_cache_valid(cache_data) is False

Manual Testing Checklist

  • [ ] Fetch NOTAMs for Edinburgh location (known NOTAM area)
  • [ ] Fetch NOTAMs for remote location (likely no NOTAMs)
  • [ ] Test with flight date in past (no valid NOTAMs)
  • [ ] Test with flight date far future (potential NOTAMs)
  • [ ] Verify cache hit on second project view
  • [ ] Verify cache expiry after 24 hours
  • [ ] Test network timeout behaviour (disconnect network)
  • [ ] Test with malformed coordinates
  • [ ] Verify error messages display correctly
  • [ ] Check database storage format (JSON structure)

Future Enhancements

Planned Improvements

1. Altitude-Based Filtering - Parse ItemF (lower limit) and ItemG (upper limit) - Filter NOTAMs based on project flight altitude - Reduce false positives for low-level UAS operations

2. NOTAM Type Icons - Visual indicators for NOTAM types (WU, WR, WD, etc.) - Colour coding by severity - Map view with NOTAM radius circles

3. Export to PDF - Include NOTAMs in flight brief PDF - Formatted NOTAM list for printing - QR code link to full NOTAM details

4. Email Notifications - Alert when new NOTAMs appear for existing projects - Daily digest of NOTAM updates - Configurable notification preferences

5. Historical NOTAM Archive - Store NOTAM history for post-flight reporting - Compare NOTAMs at creation vs flight date - Regulatory compliance evidence

Potential Features (Long-term)

1. NOTAM Change Detection - Track NOTAM additions/deletions between cache refreshes - Highlight new NOTAMs since last view - Changelog for project timeline

2. Multi-Source Integration - Integrate other NOTAM sources (FAA NOTAM API for international) - Cross-reference NATS vs FAA data - Unified NOTAM view

3. Machine Learning Classification - Auto-classify NOTAM severity for UAS operations - Predictive risk scoring based on NOTAM content - Recommendations for flight adjustment

4. Real-Time Updates - WebSocket connection for live NOTAM updates - Push notifications when viewing project - "NOTAM just issued" alerts


References

Official Documentation

  • NATS PIB: https://pibs.nats.co.uk/
  • ICAO NOTAM Format: https://www.icao.int/safety/OPS/OPS-Tools/Pages/NOTAM-Decode.aspx
  • UK AIP: https://www.aurora.nats.co.uk/htmlAIP/

External Resources

  • Haversine Formula: https://en.wikipedia.org/wiki/Haversine_formula
  • ElementTree XML API: https://docs.python.org/3/library/xml.etree.elementtree.html
  • WGS84 Coordinate System: https://en.wikipedia.org/wiki/World_Geodetic_System

Aviation Authority Resources

  • UK CAA: https://www.caa.co.uk/
  • Drone Safe UK: https://dronesafe.uk/
  • NATS Drone Information: https://www.nats.aero/ae-home/drones/