Skip to main content

Phase 2 Implementation Guide

This guide covers the implementation of Phase 2 data enrichment features: elevation data, weather forecasts, and mobile coverage integration.

Overview

Phase 2 adds advanced planning capabilities to Vanroute:

  • Road Elevation & Grades - Calculate steepness, warn about challenging sections
  • Weather Forecasts & Warnings - 7-day forecasts, severe weather alerts
  • Mobile Coverage Maps - Identify coverage gaps by carrier and network type

These features enable the app to provide comprehensive route planning with terrain analysis, weather-based recommendations, and connectivity awareness.

Architecture

┌─────────────────────────────────────────────────────────────┐
│ Phase 2 Data Pipeline │
└─────────────────────────────────────────────────────────────┘

Elevation Data → Processing → road_elevations table → Mobile App
(One-time) (GDAL/WCS) (Grade warnings)

Weather Data → Parsing → weather_forecasts → Mobile App
(Every 6 hrs) (BOM) weather_warnings (Weather alerts)

Coverage Data → Import → mobile_coverage → Mobile App
(Annual) (KML) (Coverage gaps)

Database Setup

Run Migrations

Phase 2 requires two database migrations:

Migration 0008: Create Phase 2 Tables

  • Creates road_elevations, weather_forecasts, weather_warnings, mobile_coverage, elevation_tiles tables
  • Adds spatial indexes for efficient queries
  • Sets up automatic timestamp triggers

Migration 0009: Create Helper Functions

  • Adds spatial query functions for mobile app
  • Functions for elevation, weather, and coverage lookups
  • Route analysis functions

Apply Migrations

Option A: Supabase SQL Editor (Recommended)

  1. Go to: https://app.supabase.com/project/_/sql
  2. Copy contents of packages/database/supabase/migrations/0008_create_phase2_tables.sql
  3. Paste and run in SQL Editor
  4. Repeat for 0009_create_phase2_helper_functions.sql

Option B: Supabase CLI

cd packages/database
npx supabase db push

Verify Setup

-- Check tables exist
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN (
'road_elevations',
'weather_forecasts',
'weather_warnings',
'mobile_coverage',
'elevation_tiles'
);

-- Check functions exist
SELECT routine_name FROM information_schema.routines
WHERE routine_schema = 'public'
AND routine_name LIKE '%_near_point%';

1. Elevation Data Implementation

Overview

Road elevation and grade data helps warn drivers about steep sections that may be challenging for caravans.

Data Source: Geoscience Australia Digital Elevation Models (DEMs)

Resolutions Available:

  • 5m (LiDAR) - Urban and coastal areas (245,000 sq km coverage)
  • 30m (SRTM) - National coverage, recommended for most roads
  • 90m (SRTM) - Complete coverage, suitable for major highways

Implementation Options

Option A: WCS (Web Coverage Service) Integration

Query elevation on-demand from Geoscience Australia:

const WCS_URL = 'https://services.ga.gov.au/gis/services/DEM_SRTM_1Second_over_Bathymetry_Topography/MapServer/WCSServer';

async function getElevationAtPoint(lat: number, lon: number): Promise<number> {
const params = new URLSearchParams({
SERVICE: 'WCS',
VERSION: '2.0.1',
REQUEST: 'GetCoverage',
COVERAGEID: 'DEM_SRTM_1Second_over_Bathymetry_Topography',
SUBSET: `Lat(${lat})`,
SUBSET: `Long(${lon})`,
FORMAT: 'image/tiff'
});

const response = await fetch(`${WCS_URL}?${params}`);
// Parse GeoTIFF response to extract elevation value
// ... implementation
}

Pros:

  • No storage required
  • Always up-to-date
  • Easy implementation

Cons:

  • Requires internet connection
  • Slower for bulk processing
  • API rate limits

Option B: Local DEM Tile Processing

Download and process DEM tiles locally using GDAL:

# Install GDAL
brew install gdal # macOS
apt-get install gdal-bin # Ubuntu

# Install Node.js GDAL bindings
npm install gdal-async
import gdal from 'gdal-async';

async function processElevationTile(tilePath: string) {
const dataset = await gdal.openAsync(tilePath);
const band = await dataset.bands.getAsync(1);

// Sample elevation at road segment points
for (const segment of roadSegments) {
const elevation = await band.pixels.getAsync(x, y);
// Calculate grades...
}
}

Download DEM Tiles:

# National DEM tiles (30m SRTM)
wget https://elevation-direct-downloads.s3-ap-southeast-2.amazonaws.com/30m-dem/national_utm_mosaics/nationalz56_ag.zip

# Extract
unzip nationalz56_ag.zip

Pros:

  • Fast bulk processing
  • No API limits
  • Offline capability

Cons:

  • Large file sizes (2-5 GB)
  • Storage requirements
  • More complex implementation

Processing Script Usage

# Process entire state
npm run db:process-elevation -- --state NSW --resolution 30

# Process specific bounding box
npm run db:process-elevation -- --bounds -34,150,-33,151 --resolution 5

# Show help
npm run db:process-elevation -- --help

Grade Calculation

Grades are calculated as:

Grade % = (Rise / Horizontal Distance) × 100

Categories:

  • Flat (0-3%): Easy for all vehicles
  • Gentle (3-6%): No issues for caravans
  • Moderate (6-10%): Caution with heavy loads
  • Steep (10-15%): Challenging for caravans
  • Very Steep (>15%): Avoid with caravans

Implementation Status

⚠️ Requires Production Implementation

The script framework is complete, but requires:

  1. WCS integration OR GDAL tile processing
  2. Elevation sampling along road segments
  3. Grade calculation between consecutive points
  4. Database import of calculated values

See packages/database/scripts/process-elevation-data.ts for framework.

2. Weather Data Implementation

Overview

Weather forecasts and warnings help drivers plan routes around adverse conditions.

Data Source: Bureau of Meteorology (BOM)

Coverage:

  • 7-day forecasts for 32 major locations
  • Severe weather warnings (national coverage)
  • Road impact assessments

BOM Data Access

BOM provides data via FTP and RSS feeds (no official REST API):

FTP Access:

ftp://ftp.bom.gov.au/anon/gen/

Available Formats:

  • XML forecast products
  • JSON observations
  • RSS warning feeds
  • CAP (Common Alerting Protocol) warnings

Implementation

Forecast Parsing

import { parseStringPromise } from 'xml2js';

async function fetchBOMForecasts(locationCode: string): Promise<Forecast[]> {
// Connect to FTP
const ftpUrl = `ftp://ftp.bom.gov.au/anon/gen/fwo/${locationCode}.xml`;
const response = await fetch(ftpUrl);
const xml = await response.text();

// Parse XML
const parsed = await parseStringPromise(xml);

// Extract forecast data
const forecasts = parsed.product.forecast.map(f => ({
date: f.$.['forecast-period'],
min_temp: parseFloat(f.element.find(e => e.$.type === 'air_temperature_minimum')?.text),
max_temp: parseFloat(f.element.find(e => e.$.type === 'air_temperature_maximum')?.text),
// ... more fields
}));

return forecasts;
}

Warning Parsing

async function fetchBOMWarnings(): Promise<Warning[]> {
// Subscribe to RSS feeds
const rssFeeds = [
'http://www.bom.gov.au/fwo/IDZ00057.warnings_national.xml',
'http://www.bom.gov.au/fwo/IDN00100.weather.xml'
];

const warnings: Warning[] = [];

for (const feed of rssFeeds) {
const response = await fetch(feed);
const xml = await response.text();
const parsed = await parseStringPromise(xml);

// Extract warnings from RSS
for (const item of parsed.rss.channel[0].item) {
warnings.push({
title: item.title[0],
description: item.description[0],
issued: new Date(item.pubDate[0]),
// ... more fields
});
}
}

return warnings;
}

Automation

Weather data should be polled every 6 hours:

GitHub Actions:

# .github/workflows/poll-weather.yml
name: Poll Weather Data

on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch:

jobs:
poll-weather:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
working-directory: ./packages/database
- run: npm run db:poll-weather
working-directory: ./packages/database
env:
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}

Cron:

0 */6 * * * cd /path/to/vanroute/packages/database && npm run db:poll-weather >> /var/log/weather-poll.log 2>&1

Script Usage

# Poll BOM weather
npm run db:poll-weather

Implementation Status

⚠️ Sample Data Only

The script generates sample forecasts. For production:

  1. Implement FTP connection
  2. Parse XML forecast products
  3. Subscribe to RSS warning feeds
  4. Extract and store forecast data

See packages/database/scripts/poll-bom-weather.ts for framework.

3. Mobile Coverage Implementation

Overview

Mobile coverage maps help identify areas without connectivity, crucial for remote travel safety.

Data Source: ACCC Mobile Infrastructure Report

Coverage:

  • Telstra (3G, 4G, 5G)
  • Optus (3G, 4G, 5G)
  • Vodafone (3G, 4G, 5G)
  • TPG (4G, 5G)

Update Frequency: Annual

Data Download

  1. Visit: https://data.gov.au/data/dataset/accc-mobile-infrastructure-report-data-release
  2. Download KML files for each carrier and network type
  3. Example files:
    • coverage-map-telstra-4g-outdoor-2024.kml
    • coverage-map-optus-5g-outdoor-2024.kml
    • etc.

KML Parsing

Install parser library:

npm install @tmcw/togeojson

Parse KML to GeoJSON:

import { kml } from '@tmcw/togeojson';
import { DOMParser } from 'xmldom';

async function parseKMLCoverage(kmlPath: string): Promise<CoverageArea> {
const kmlText = readFileSync(kmlPath, 'utf-8');
const kmlDom = new DOMParser().parseFromString(kmlText);
const geojson = kml(kmlDom);

// Extract metadata from filename
const fileName = kmlPath.split('/').pop();
const [, carrier, network, type, year] = fileName.match(
/coverage-map-(\w+)-(3g|4g|5g)-(outdoor|indoor)-(\d{4})/
);

return {
carrier: carrier.toLowerCase(),
network_type: network.toLowerCase(),
coverage_type: type.toLowerCase(),
geometry: geojson.features[0].geometry,
source_year: parseInt(year)
};
}

Import Script Usage

# Import directory of KML files
npm run db:import-coverage -- ~/Downloads/coverage/

# Import single file
npm run db:import-coverage -- coverage-telstra-4g-2024.kml

Implementation Status

⚠️ Requires KML Parser

The script framework exists, but requires:

  1. Install @tmcw/togeojson library
  2. Download ACCC KML files
  3. Parse KML coordinates to GeoJSON
  4. Import coverage polygons

See packages/database/scripts/import-mobile-coverage.ts for framework.

Mobile App Integration

Service Layer

Create packages/mobile/services/phase2Service.ts:

import { supabase } from './supabase';

// Check steep grades
export async function getSteepGradesNearLocation(
lat: number,
lon: number,
radiusKm: number = 50,
minGrade: number = 8.0
) {
const { data, error } = await supabase.rpc('steep_grades_near_point', {
lat,
lon,
radius_meters: radiusKm * 1000,
min_grade_percent: minGrade
});

if (error) throw error;
return data;
}

// Check weather forecast
export async function getWeatherForecast(
lat: number,
lon: number,
radiusKm: number = 100,
daysAhead: number = 7
) {
const { data, error } = await supabase.rpc('weather_forecast_near_point', {
lat,
lon,
radius_meters: radiusKm * 1000,
days_ahead: daysAhead
});

if (error) throw error;
return data;
}

// Check weather warnings
export async function getWeatherWarnings(
lat: number,
lon: number,
radiusKm: number = 100
) {
const { data, error } = await supabase.rpc('weather_warnings_for_area', {
lat,
lon,
radius_meters: radiusKm * 1000
});

if (error) throw error;
return data;
}

// Check mobile coverage
export async function checkCoverage(
lat: number,
lon: number,
carrier: string = 'telstra',
networkType: string = '4g'
) {
const { data, error } = await supabase.rpc('mobile_coverage_at_point', {
lat,
lon,
carrier,
network_type: networkType
});

if (error) throw error;
return data && data.length > 0;
}

// Get route conditions summary
export async function getRouteConditions(
lat: number,
lon: number,
radiusKm: number = 100
) {
const { data, error } = await supabase.rpc('route_conditions_summary', {
lat,
lon,
radius_meters: radiusKm * 1000
});

if (error) throw error;
return data;
}

UI Components

Grade Warning Component

import { getSteepGradesNearLocation } from '@/services/phase2Service';

export function GradeWarnings({ lat, lon }) {
const [grades, setGrades] = useState([]);

useEffect(() => {
getSteepGradesNearLocation(lat, lon, 50, 8.0).then(setGrades);
}, [lat, lon]);

return (
<View>
{grades.map(grade => (
<Alert
key={grade.id}
severity={grade.grade_category === 'very_steep' ? 'error' : 'warning'}
>
{grade.road_name || grade.road_number}: {grade.max_grade_percent}% grade
</Alert>
))}
</View>
);
}

Weather Warning Component

import { getWeatherWarnings } from '@/services/phase2Service';

export function WeatherWarnings({ lat, lon }) {
const [warnings, setWarnings] = useState([]);

useEffect(() => {
getWeatherWarnings(lat, lon, 100).then(setWarnings);
}, [lat, lon]);

return (
<View>
{warnings.map(warning => (
<Alert
key={warning.id}
severity={warning.severity === 'emergency' ? 'error' : 'warning'}
>
<Text>{warning.title}</Text>
<Text>{warning.description}</Text>
{warning.road_impact_description && (
<Text>Road Impact: {warning.road_impact_description}</Text>
)}
</Alert>
))}
</View>
);
}

Coverage Indicator Component

import { checkCoverage } from '@/services/phase2Service';

export function CoverageIndicator({ lat, lon }) {
const [has4G, setHas4G] = useState(false);
const [has5G, setHas5G] = useState(false);

useEffect(() => {
Promise.all([
checkCoverage(lat, lon, 'telstra', '4g'),
checkCoverage(lat, lon, 'telstra', '5g')
]).then(([g4, g5]) => {
setHas4G(g4);
setHas5G(g5);
});
}, [lat, lon]);

return (
<View>
{has5G && <Badge>5G</Badge>}
{has4G && !has5G && <Badge>4G</Badge>}
{!has4G && !has5G && <Badge color="error">No Coverage</Badge>}
</View>
);
}

Testing

Test Database Functions

-- Test steep grades
SELECT * FROM steep_grades_near_point(-33.8688, 151.2093, 50000, 8.0);

-- Test weather forecast
SELECT * FROM weather_forecast_near_point(-33.8688, 151.2093, 100000, 7);

-- Test weather warnings
SELECT * FROM weather_warnings_for_area(-33.8688, 151.2093, 100000);

-- Test coverage
SELECT * FROM mobile_coverage_at_point(-33.8688, 151.2093, 'telstra', '4g');

-- Test route summary
SELECT * FROM route_conditions_summary(-33.8688, 151.2093, 100000);

Test Scripts

# Weather polling (sample data)
npm run db:poll-weather

# Coverage import (requires downloaded files)
npm run db:import-coverage -- ~/Downloads/coverage/

# Elevation processing (framework only)
npm run db:process-elevation -- --help

Production Checklist

Before deploying Phase 2:

  • Run database migrations (0008, 0009)
  • Implement elevation processing (WCS or GDAL)
  • Implement BOM weather parsing (FTP/RSS)
  • Implement mobile coverage import (KML parser)
  • Set up weather polling automation (GitHub Actions or cron)
  • Test all helper functions with real data
  • Create mobile app service layer
  • Implement UI components for warnings
  • Test with production data
  • Set up monitoring and alerting

Performance Considerations

Database Optimization

All tables have appropriate indexes:

  • Spatial indexes (GIST) on geometry columns
  • B-tree indexes on frequently queried columns
  • Partial indexes for filtered queries

Caching Strategy

Recommended cache durations:

  • Elevation data: Indefinite (static)
  • Weather forecasts: 1 hour
  • Weather warnings: 15 minutes
  • Mobile coverage: Indefinite (annual updates)

Query Optimization

Use appropriate radius parameters:

  • Steep grades: 50km radius
  • Weather forecasts: 100km radius
  • Mobile coverage: Point-in-polygon (no radius)

Known Limitations

Elevation Data

  • 5m LiDAR limited to urban/coastal areas
  • Processing is computationally intensive
  • Requires GDAL or WCS implementation

Weather Data

  • No official BOM REST API
  • Requires XML/RSS parsing
  • Sample data only in current implementation

Mobile Coverage

  • Based on theoretical coverage
  • Actual coverage may vary
  • Annual update cycle
  • Requires KML parser library

Support


Status: ⚠️ Partial Implementation - Framework Complete, Production Data Integration Required Last Updated: October 2025 Next: Phase 3 - User Experience Enhancements