Skip to content

ATC Location Lookup System - Design Document

Executive Summary

Overview

The ATC Location Lookup System automatically identifies nearby aerodromes with Air Traffic Control (ATC) services within 50km of drone flight locations. It provides pilots with essential contact information (radio frequencies and phone numbers) for coordinating with ATC when operating near controlled airspace or aerodromes. The system uses a hybrid discovery approach, combining OpenStreetMap for aerodrome discovery with NATS Aurora AIP scraping for detailed ATC data.

Business Value

Safety Enhancement: - Identifies aerodromes requiring ATC coordination before flight operations - Provides direct contact information (frequencies/phone numbers) for ATC communication - Warns about military aerodromes where operations may be restricted - Reduces risk of unauthorized airspace incursions near aerodromes

User Experience: - Automatic discovery - No manual aerodrome lookup required - Comprehensive coverage - Discovers all UK aerodromes (civilian + military) via OSM - Rich context - Displays aerodrome names, distances, ATC frequencies, phone numbers - Graceful degradation - Shows basic info even when detailed data unavailable - Visual warnings - Clear indicators for placeholder data and military aerodromes

Operational Efficiency: - Saves ~10-15 minutes per project (manual ATC contact lookup) - Provides auditable ATC contact records for compliance documentation - Automatic data caching reduces repeated scraping overhead

Key Capabilities

  1. Hybrid Aerodrome Discovery
  2. OpenStreetMap Overpass API queries for aerodrome locations
  3. NATS AIP scraping for detailed ATC data (frequencies, phone numbers)
  4. Smart caching with 60-day staleness detection
  5. Fallback to placeholder records when NATS data unavailable

  6. Military Aerodrome Detection

  7. Automatic detection via OSM tags (military=airfield, RAF prefix)
  8. Special handling for military bases (not in civilian AIP)
  9. Clear [Military] designation in UI

  10. Admin Management

  11. View all cached aerodromes with data quality scores
  12. Manual refresh for individual aerodromes
  13. Bulk refresh all aerodromes
  14. Add new aerodromes by ICAO code
  15. Data completeness indicators (Placeholder, Military badges)

  16. Data Quality Tracking

  17. Quality scores (20-100) based on data completeness
  18. Placeholder warnings when NATS scraping fails
  19. Staleness alerts for data >60 days old

Architecture Overview

High-Level System Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    USER INTERACTION LAYER                        │
├─────────────────────────────────────────────────────────────────┤
│  Project Creation Wizard                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ Location │→ │ Viability│→ │  Review  │→ │  Submit  │       │
│  │  Select  │  │ Display  │  │ Warnings │  │  Create  │       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
│       ↓             ↓              ↓             ↓              │
│  [Discover    [Display     [Show        [Link                  │
│   Aerodromes]  ATC Info]    Placeholders] Aerodromes]          │
└─────────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────────┐
│                  APPLICATION LOGIC LAYER                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ATCAPI (app/utils/atc_api.py)                                 │
│  ┌────────────────────────────────────────────────┐            │
│  │  find_nearby_aerodromes(lat, lon, radius=50)  │            │
│  │    1. Query Overpass API for aerodromes        │            │
│  │    2. Extract ICAO codes from OSM              │            │
│  │    3. Check database cache                     │            │
│  │    4. Scrape NATS if missing/stale             │            │
│  │    5. Store placeholder if scraping fails      │            │
│  │    6. Return sorted by distance                │            │
│  └────────────────────────────────────────────────┘            │
│                                                                  │
│  NATSAIPScraper                                                 │
│  ┌────────────────────────────────────────────────┐            │
│  │  • Construct NATS AIP URL (AIRAC dated)        │            │
│  │  • Fetch HTML page                             │            │
│  │  • Parse coordinates (DMS to decimal)          │            │
│  │  • Extract ATC frequencies                     │            │
│  │  • Extract phone numbers                       │            │
│  │  • Calculate data quality score                │            │
│  └────────────────────────────────────────────────┘            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────────┐
│                      DATA STORAGE LAYER                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Database (MariaDB) - PlaceOfInterest Table                     │
│  ┌────────────────────────────────────────────────┐            │
│  │  • osm_id: "aerodrome/{ICAO}"                  │            │
│  │  • place_type: "aerodrome"                     │            │
│  │  • name: "Edinburgh Airport"                   │            │
│  │  • latitude, longitude (decimal degrees)       │            │
│  │  • phone: Main contact number                  │            │
│  │  • data_quality_score: 20-100                  │            │
│  │  • last_updated: Timestamp                     │            │
│  │  • extra_data: JSON                            │            │
│  │    {                                            │            │
│  │      "icao_code": "EGPH",                      │            │
│  │      "frequencies": {...},                     │            │
│  │      "phone_numbers": {...},                   │            │
│  │      "is_military": false,                     │            │
│  │      "is_placeholder": false,                  │            │
│  │      "airac_cycle": "2026-01-22-AIRAC"         │            │
│  │    }                                            │            │
│  └────────────────────────────────────────────────┘            │
│                                                                  │
│  ProjectPlace Junction Table                                    │
│  ┌────────────────────────────────────────────────┐            │
│  │  • Links aerodromes to projects                │            │
│  │  • All aerodromes within 50km linked           │            │
│  │  • No relevance filtering (all are relevant)   │            │
│  └────────────────────────────────────────────────┘            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────────┐
│                     EXTERNAL DATA SOURCES                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  OpenStreetMap Overpass API                                     │
│  ┌────────────────────────────────────────────────┐            │
│  │  • Discover aerodromes by location             │            │
│  │  • Extract ICAO codes                          │            │
│  │  • Detect military tags                        │            │
│  │  • Get basic coordinates/names                 │            │
│  └────────────────────────────────────────────────┘            │
│                          ↓                                       │
│  NATS Aurora AIP (HTML scraping)                                │
│  ┌────────────────────────────────────────────────┐            │
│  │  • Civilian aerodrome ATC data                 │            │
│  │  • ATC frequencies and callsigns               │            │
│  │  • Contact phone numbers                       │            │
│  │  • Operating hours                             │            │
│  │  • Military aerodromes → 404 (not published)   │            │
│  └────────────────────────────────────────────────┘            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Data Flow Sequence

USER                 APPLICATION           OSM API        NATS AIP      DATABASE
 │                       │                   │               │             │
 │  Create Project       │                   │               │             │
 │  (Location Selected)  │                   │               │             │
 ├──────────────────────>│                   │               │             │
 │                       │                   │               │             │
 │                       │  Query Overpass   │               │             │
 │                       │  (50km radius)    │               │             │
 │                       ├──────────────────>│               │             │
 │                       │<──────────────────┤               │             │
 │                       │  [EGPH, EGQS]     │               │             │
 │                       │  + OSM tags       │               │             │
 │                       │                   │               │             │
 │                       │  Check Cache      │               │             │
 │                       │  (EGPH)           │               │             │
 │                       ├──────────────────────────────────────────────>│
 │                       │<──────────────────────────────────────────────┤
 │                       │  EGPH: Cached (55d old, stale)                │
 │                       │                   │               │             │
 │                       │  Refresh EGPH     │               │             │
 │                       │  from NATS        │               │             │
 │                       ├──────────────────────────────────>│             │
 │                       │<──────────────────────────────────┤             │
 │                       │  200 OK + HTML    │               │             │
 │                       │                   │               │             │
 │                       │  Parse + Update   │               │             │
 │                       ├──────────────────────────────────────────────>│
 │                       │                   │               │             │
 │                       │  Check Cache      │               │             │
 │                       │  (EGQS)           │               │             │
 │                       ├──────────────────────────────────────────────>│
 │                       │<──────────────────────────────────────────────┤
 │                       │  EGQS: Not Found  │               │             │
 │                       │                   │               │             │
 │                       │  Fetch EGQS       │               │             │
 │                       │  from NATS        │               │             │
 │                       ├──────────────────────────────────>│             │
 │                       │<──────────────────────────────────┤             │
 │                       │  404 Not Found    │               │             │
 │                       │  (Military base)  │               │             │
 │                       │                   │               │             │
 │                       │  Store Placeholder│               │             │
 │                       │  (OSM data only)  │               │             │
 │                       ├──────────────────────────────────────────────>│
 │                       │                   │               │             │
 │  Display Results      │                   │               │             │
 │  • EGPH: Full data    │                   │               │             │
 │  • EGQS: Placeholder  │                   │               │             │
 │    [Military] warning │                   │               │             │
 │<──────────────────────┤                   │               │             │
 │                       │                   │               │             │

Component Details

1. ATCAPI Class (app/utils/atc_api.py)

Core Methods:

class ATCAPI:
    """Client for UK Aerodrome ATC Information"""

    DEFAULT_SEARCH_RADIUS_KM = 50
    MAX_RESULTS = 5
    STALENESS_THRESHOLD_DAYS = 60

    @classmethod
    def find_nearby_aerodromes(cls, latitude, longitude, radius_km=None):
        """
        Hybrid discovery approach:
        1. Query Overpass API for aerodromes
        2. For each: check cache, refresh if stale, scrape if new
        3. Store placeholder if NATS fails
        4. Return sorted by distance
        """

    @classmethod
    def _discover_aerodromes_via_overpass(cls, latitude, longitude, radius_km):
        """Query OpenStreetMap for aeroway=aerodrome features"""

    @classmethod
    def update_aerodrome_data(cls, icao_code, airac_date=None):
        """Scrape NATS AIP and update database"""

    @classmethod
    def store_placeholder_aerodrome(cls, osm_data):
        """Store basic OSM data when NATS scraping fails"""

    @classmethod
    def get_aerodrome_by_icao(cls, icao_code):
        """Retrieve cached aerodrome from database"""

    @classmethod
    def link_aerodromes_to_project(cls, project_id, aerodromes_data):
        """Create ProjectPlace links for all nearby aerodromes"""

Key Configuration: - Search radius: 50km (vs 10km for general places of interest) - Max results: 5 closest aerodromes - Staleness: 60 days before refresh required - Rate limiting: 1 second delay between NATS requests

2. NATSAIPScraper Class

Core Methods:

class NATSAIPScraper:
    """Scraper for NATS Aurora AIP HTML pages"""

    NATS_AIP_BASE_URL = "https://www.aurora.nats.co.uk/htmlAIP/Publications/{airac_date}/html/eAIP/EG-AD-2.{icao}-en-GB.html"

    @classmethod
    def scrape_aerodrome_page(cls, icao_code, airac_date):
        """
        Fetch and parse NATS AIP HTML page

        Returns:
            {
                'success': True/False,
                'data': {
                    'name': 'Edinburgh Airport',
                    'latitude': 55.9050,
                    'longitude': -3.5017,
                    'frequencies': {'tower': ['118.705']},
                    'phone_numbers': {'main': '03300-271262'}
                },
                'error': None
            }
        """

    @classmethod
    def _extract_coordinates(cls, soup):
        """Extract coordinates with flexible regex (handles hidden HTML)"""

    @classmethod
    def _extract_aerodrome_name(cls, soup, icao_code):
        """Extract name from page title/headings (4 methods + fallback)"""

    @classmethod
    def _extract_frequencies(cls, soup):
        """Parse ATC frequencies from AD 2.18 tables"""

    @classmethod
    def _extract_phone_numbers(cls, soup):
        """Extract phone numbers via regex patterns"""

    @classmethod
    def get_current_airac_date(cls):
        """Calculate current AIRAC cycle date (28-day cycles)"""

3. Route Integration

routes_new_project.py - Project creation workflow integration:

# POST /new-project/location
# After location selection, before viability display

try:
    aerodromes_data = ATCAPI.find_nearby_aerodromes(
        latitude, longitude, radius_km=50
    )
    session['nearby_aerodromes'] = aerodromes_data
except Exception as e:
    logger.error(f"Aerodrome lookup error: {e}")
    session['nearby_aerodromes'] = {
        'success': False,
        'error': str(e),
        'aerodromes': []
    }

# GET /new-project/viability
# Display aerodrome information

nearby_aerodromes = session.get('nearby_aerodromes')
return render_template('new_project_viability.html',
                      nearby_aerodromes=nearby_aerodromes)

# POST /new-project/viability
# Link aerodromes to project after creation

aerodromes_data = session.get('nearby_aerodromes')
if aerodromes_data and aerodromes_data.get('success'):
    ATCAPI.link_aerodromes_to_project(new_project.id, aerodromes_data)

routes_admin.py - Admin management routes:

# GET /admin/aerodromes
# List all cached aerodromes with quality indicators

# POST /admin/aerodromes/refresh/<icao_code>
# Manual refresh single aerodrome

# POST /admin/aerodromes/refresh-all
# Bulk refresh all aerodromes (rate-limited)

# POST /admin/aerodromes/add
# Add new aerodrome by ICAO code

# POST /admin/aerodromes/delete/<icao_code>
# Delete aerodrome from cache

Data Models

PlaceOfInterest Table

Aerodrome-Specific Fields:

CREATE TABLE place_of_interest (
    id INT PRIMARY KEY AUTO_INCREMENT,
    osm_id VARCHAR(255) UNIQUE,           -- "aerodrome/EGPH"
    osm_type VARCHAR(50),                 -- "aerodrome"
    place_type VARCHAR(50),               -- "aerodrome"
    place_category VARCHAR(50),           -- "aviation"
    name VARCHAR(255),                    -- "Edinburgh Airport"
    latitude DECIMAL(10, 8),              -- 55.90500000
    longitude DECIMAL(11, 8),             -- -3.50170000
    phone VARCHAR(255),                   -- "03300-271262"
    extra_data JSON,                      -- ATC details (see below)
    last_updated DATETIME,                -- Last NATS scrape
    data_quality_score INT,               -- 20-100
    -- ... other standard fields
);

extra_data JSON Structure:

{
  "icao_code": "EGPH",
  "aerodrome_name": "Edinburgh Airport",
  "atc_callsign": "",
  "frequencies": {
    "tower": ["118.705"],
    "approach": ["121.200"],
    "ground": ["121.750"]
  },
  "phone_numbers": {
    "main": "03300-271262"
  },
  "airac_cycle": "2026-01-22-AIRAC",
  "data_source": "nats_aip",
  "is_military": false,
  "is_placeholder": false,
  "placeholder_reason": null,
  "osm_tags": {},
  "last_scraped": "2026-01-13T10:00:00Z"
}

Placeholder Record Example (Military Aerodrome):

{
  "icao_code": "EGQS",
  "aerodrome_name": "RAF Lossiemouth [Military]",
  "atc_callsign": "",
  "frequencies": {},
  "phone_numbers": {},
  "airac_cycle": null,
  "data_source": "osm_only",
  "is_military": true,
  "is_placeholder": true,
  "placeholder_reason": "NATS AIP scraping failed - data from OSM only",
  "osm_tags": {
    "aeroway": "aerodrome",
    "military": "airfield",
    "landuse": "military",
    "name": "RAF Lossiemouth",
    "contact:email": "LOS-A3Ops@mod.gov.uk"
  },
  "last_scraped": "2026-01-13T07:31:00Z"
}

ProjectPlace Junction Table

Purpose: Link aerodromes to projects

CREATE TABLE project_place (
    id INT PRIMARY KEY AUTO_INCREMENT,
    project_id INT,
    place_id INT,
    FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE,
    FOREIGN KEY (place_id) REFERENCES place_of_interest(id) ON DELETE CASCADE
);

Linking Strategy: All aerodromes within 50km are linked (no relevance filtering - all aerodromes are potentially relevant for ATC coordination).


User Interface

Project Viability Template

Display Sections (new_project_viability.html):

  1. Success Case (aerodromes found):

    <div class="card">
        <h5>Nearby Aerodromes with ATC</h5>
        <p>2 aerodrome(s) with ATC services found within 50km</p>
    
        <!-- For each aerodrome -->
        <div class="aerodrome-card">
            <span class="badge">EGPH</span> Edinburgh Airport
            <span class="distance">8.5 km away</span>
    
            <!-- Frequencies -->
            <ul>
                <li>Tower: 118.705 MHz</li>
                <li>Approach: 121.200 MHz</li>
            </ul>
    
            <!-- Phone -->
            <p>Contact: <a href="tel:03300271262">03300-271262</a></p>
    
            <!-- View AIP button -->
            <a href="https://www.aurora.nats.co.uk/.../EGPH..." target="_blank">
                View AIP
            </a>
        </div>
    </div>
    

  2. Placeholder Warning (NATS scraping failed):

    <div class="alert alert-danger">
        <strong>Limited Data:</strong> NATS AIP scraping failed.
        Only basic information from OpenStreetMap is available.
        <small>This may be a military aerodrome. Civilian AIP data not available.</small>
    </div>
    

  3. Military Indicator (detected from OSM):

    <div class="alert alert-info">
        <strong>Military Aerodrome:</strong> Contact ATC before operations in this area.
    </div>
    

  4. Stale Data Warning (>60 days old):

    <div class="alert alert-warning">
        Data is 75 days old. Verify with current AIP.
    </div>
    

Admin Panel

Aerodrome List (admin_panel/aerodromes.html):

Features: - DataTables with search/sort - Visual indicators: - Red row: Placeholder data - Yellow row: Stale data (>60 days) - Badges: [Placeholder], [Military] - Actions: View, Refresh, Delete - Bulk "Refresh All" button - "Add Aerodrome" form (ICAO input)

Quality Score Display:

<div class="progress">
    <div class="progress-bar bg-success" style="width: 85%"></div>
</div>
<span>85/100</span>

Colour coding: - Green (≥75): Full data - Yellow (50-74): Partial data - Red (<50): Placeholder/minimal data


Implementation Decisions

Why 50km Radius?

Rationale: - UK drone operations limited to 400ft AGL - Controlled airspace (CTR/TMA) typically extends 10-20km from aerodromes - 50km provides awareness of aerodromes in adjacent controlled zones - Balance between comprehensiveness and performance

Comparison: - General places of interest: 10km (immediate vicinity) - Aerodromes: 50km (broader awareness)

Why Hybrid Discovery?

OSM First: - Comprehensive coverage (civilian + military) - Fast spatial queries - Includes military aerodromes not in civilian AIP - Provides fallback coordinates

NATS Enrichment: - Authoritative ATC data - Current AIRAC cycle frequencies - Official contact numbers - Regulatory compliance

Benefits: - No missed aerodromes (OSM discovers all) - Rich data when available (NATS provides details) - Graceful degradation (placeholder when NATS fails)

Why Database Caching?

Alternative Approaches Considered: 1. Real-time scraping only (slow, unreliable) 2. File-based cache (no relational queries) 3. No caching (wasteful, poor UX)

Chosen Approach: Database Cache: - Fast queries by ICAO code - Reuse existing PlaceOfInterest infrastructure - Supports spatial queries (distance calculations) - Admin interface can easily list/manage - Automatic staleness tracking via timestamps

Cache Refresh Strategy: - Automatic: On first lookup if missing or stale - Manual: Admin "Refresh" button - Bulk: Admin "Refresh All" (rate-limited 1/sec)

Why 60-Day Staleness?

Rationale: - AIRAC cycle: 28 days (official update frequency) - Grace period: Allow 2 cycles (56 days) before flagging stale - Practicality: ATC frequencies rarely change mid-cycle - User experience: Avoid excessive warnings

Staleness Handling: - Still usable after 60 days (better than no data) - Visual warning displayed to user - Auto-refresh attempted on next lookup - Admin can force refresh


Data Quality Scoring

Score Components

Base Score (50 points always): - Name present: +25 points - Coordinates present: +25 points (from OSM)

Additional Points (up to +50): - Has phone: +15 points - Has frequencies: +30 points (10 per type, max 30) - Has ATC callsign: +5 points

Score Ranges: - 85-100: Full data (all fields present) - 55-84: Good data (some ATC info) - 40-54: Basic data (name + coords only) - 20-39: Placeholder (OSM only, likely military)

Examples:

EGPH (Edinburgh):
  Name ✓ (25) + Coords ✓ (25) + Phone ✓ (15) +
  Frequencies ✓ (20) = 85/100

EGQS (RAF Lossiemouth - Placeholder):
  Name ✓ (25) + Coords ✓ (25) = 50/100
  But marked as placeholder → Score = 20/100


Error Handling

Network Failures

Scenario Response User Impact
OSM timeout Skip aerodrome discovery, show "No data available" Project creation continues
NATS timeout (30s) Store placeholder with OSM data Show placeholder warning
NATS 404 Store placeholder (military/non-existent) Show placeholder warning with military indicator
NATS 500 Retry once, then skip Log error, aerodrome not shown

Data Parsing Failures

Scenario Response Fallback
Coordinates not found Try fallback dictionary Use known coordinates for 15 common aerodromes
Name extraction fails Try fallback dictionary Use "{ICAO} Airport" format
No frequencies found Continue with empty dict Display "No frequency data available"
Invalid HTML structure Log warning, skip aerodrome Don't break entire lookup

User Experience Degradation Levels

Level 1: Full Success - All aerodromes discovered - All NATS data scraped successfully - No warnings displayed

Level 2: Partial Success - Some aerodromes have full data - Some have placeholders (military) - Warning badges displayed - Project creation unaffected

Level 3: Graceful Failure - OSM discovery succeeds but all NATS scraping fails - All aerodromes shown as placeholders - Major warning displayed - Project creation still succeeds

Level 4: Complete Failure - OSM discovery fails - "No aerodrome data available" message - Project creation succeeds - Feature gracefully absent


Security Considerations

Input Validation

ICAO Code Validation:

def _validate_icao_code(icao_code):
    """Validate 4-character UK ICAO code"""
    if not icao_code or len(icao_code) != 4:
        return False
    if not icao_code.startswith('EG'):
        return False
    if not icao_code.isalpha():
        return False
    return True

Coordinate Validation:

# Validate reasonable UK bounds
if not (-10 <= longitude <= 2):  # UK longitude range
    raise ValueError("Invalid longitude")
if not (49 <= latitude <= 61):   # UK latitude range
    raise ValueError("Invalid latitude")

HTTP Request Safety

Timeouts: - OSM Overpass API: 15 seconds - NATS AIP: 30 seconds

Rate Limiting: - 1 second delay between NATS requests (admin bulk refresh) - Prevent DDOS of NATS servers

SSL Verification: - All HTTPS requests verify certificates - No insecure connections allowed

Data Sanitization

HTML Parsing: - Use BeautifulSoup for safe HTML parsing - No eval() or exec() of scraped content - Strip potentially dangerous tags

Database Storage: - JSON validation before storage - Parameterized queries (SQLAlchemy ORM) - No raw SQL string concatenation


Testing Strategy

Unit Tests

Test Coverage (to be implemented):

# test_atc_api.py

def test_calculate_distance():
    """Test Haversine distance calculation"""
    # EGPH to Edinburgh Napier ≈ 8km
    distance = ATCAPI._calculate_distance(
        55.9533, -3.1883,  # Napier
        55.9050, -3.5017   # EGPH
    )
    assert 7000 < distance < 9000  # meters

def test_validate_icao_code():
    """Test ICAO code validation"""
    assert ATCAPI._validate_icao_code('EGPH') == True
    assert ATCAPI._validate_icao_code('EGQS') == True
    assert ATCAPI._validate_icao_code('KJFK') == False  # US code
    assert ATCAPI._validate_icao_code('EGP') == False   # Too short

def test_dms_to_decimal():
    """Test coordinate conversion"""
    # 57°28'50"N = 57.480556
    lat = NATSAIPScraper._parse_coordinate_dms('572850', 'N')
    assert abs(lat - 57.480556) < 0.0001

Integration Tests

End-to-End Workflow Tests:

# test_atc_integration.py

def test_project_creation_with_aerodromes():
    """Test full project creation with aerodrome lookup"""
    # Create project at Edinburgh Napier
    response = client.post('/new-project/location', data={
        'latitude': 55.9533,
        'longitude': -3.1883,
        # ... other fields
    })

    # Verify EGPH discovered and linked
    project = Project.query.filter_by(title='Test Project').first()
    aerodromes = project.places_of_interest.filter_by(
        place_type='aerodrome'
    ).all()

    assert len(aerodromes) >= 1
    assert any(a.extra_data.get('icao_code') == 'EGPH' for a in aerodromes)

def test_placeholder_creation_for_military():
    """Test placeholder handling for military aerodromes"""
    # Near RAF Lossiemouth
    result = ATCAPI.find_nearby_aerodromes(57.7172, -3.3389, radius_km=20)

    egqs = next((a for a in result['aerodromes'] if a['icao_code'] == 'EGQS'), None)
    assert egqs is not None
    assert egqs['is_military'] == True
    assert egqs['is_placeholder'] == True
    assert 'RAF Lossiemouth' in egqs['name']

Manual Test Locations

Test Case 1: Edinburgh Area - Location: 55.9533, -3.1883 (Edinburgh Napier) - Expected: EGPH (Edinburgh Airport) ~8km - Data Type: Full (NATS successful) - Verify: Frequencies, phone, quality score 85+

Test Case 2: Outer Hebrides - Location: 57.4669, -7.3704 (Benbecula) - Expected: EGPL (1.6km), EGPR (49.6km) - Data Type: Full (both civilian operations) - Verify: Beach runway note for Barra

Test Case 3: Military Base - Location: 57.7172, -3.3389 (RAF Lossiemouth) - Expected: EGQS (1.2km) - Data Type: Placeholder (military) - Verify: [Military] badge, placeholder warning, quality score 20

Test Case 4: Remote Location - Location: 57.5000, -5.0000 (Scottish Highlands) - Expected: No aerodromes within 50km - Verify: "No aerodromes within 50km" message


Admin Operations

Seeding Scottish Aerodromes

Script: scripts/seed_aerodromes.py

Usage:

# Seed 10 Scottish aerodromes
python scripts/seed_aerodromes.py

# Verify seeding
python scripts/seed_aerodromes.py --verify

Default Aerodromes: 1. EGPH - Edinburgh Airport 2. EGPF - Glasgow Airport 3. EGPK - Glasgow Prestwick Airport 4. EGPN - Dundee Airport 5. EGPE - Inverness Airport 6. EGPA - Kirkwall Airport 7. EGPO - Stornoway Airport 8. EGPB - Sumburgh Airport 9. EGEC - Campbeltown Airport 10. EGPI - Islay Airport

Maintenance Tasks

Manual Refresh:

Admin → Aerodromes → [Select aerodrome] → Refresh

Bulk Refresh:

Admin → Aerodromes → Refresh All (rate-limited 1/sec)

Add New Aerodrome:

Admin → Aerodromes → Add Aerodrome → Enter ICAO code

Monitor Data Quality:

Admin → Aerodromes → Sort by Quality Score → Address low scores


Future Enhancements

Phase 2: UK-Wide Coverage

Objective: Expand beyond Scotland to full UK

Tasks: - Add England, Wales, Northern Ireland aerodromes to seed script (~40-50 more) - Test coverage with projects across UK regions - Verify NATS AIP availability for all civilian aerodromes

Phase 3: European Coverage

Objective: Support drone operations in European countries

Challenges: - Each country has different AIP systems (not all use NATS Aurora) - ICAO codes vary by country (FR, DE, NL, etc.) - Language considerations (multi-language AIP pages) - May require country-specific scrapers

Alternatives: - Eurocontrol AIS Database (if API available) - ICAO Data+ (commercial subscription)

Phase 4: Real-Time NOTAM Integration

Objective: Show active NOTAMs per aerodrome

Benefits: - Alert to temporary closures - Warn about special procedures - Display active runway closures

Integration Point: Cross-reference aerodrome ICAO with NOTAM API

Phase 5: Frequency Validation

Objective: Verify frequencies against Ofcom database

Rationale: - Catch data entry errors in NATS AIP - Identify expired frequency allocations - Provide confidence indicators


References

Official Documentation

Technical Resources


Version History

Version 1.0 - 2026-01-13 - Initial design for ATC location lookup system - Hybrid OpenStreetMap + NATS AIP approach - Military aerodrome detection and placeholder handling - Data quality scoring and staleness tracking - Admin management interface - Scottish aerodromes seeding