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:
- Geographic Filter: Reduce to NOTAMs within their specified radius (~50-100 remain)
- UAS Relevance Filter: Reduce to UAS-specific or UAS-mentioned NOTAMs (~10-30 remain)
- 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/
Related Documentation¶
- NATS NOTAM API Documentation - API client usage and reference
- Project Model Schema - Database schema including notam_data column
- Weather Forecast Integration - Similar caching pattern
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/