Public
Edited
Jun 11
Insert cell
Insert cell
{
const topojson = await import("https://cdn.skypack.dev/topojson-client@3");
return {
feature: topojson.feature,
mesh: topojson.mesh
};
}


Insert cell
topojson = await import("https://cdn.skypack.dev/topojson-client@3")

Insert cell
world = (await fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")).json()

Insert cell
countries = topojson.feature(world, world.objects.countries)

Insert cell
// Cell 2: ITU-R S.672-4 Constants and Parameters
// Reference: ITU-R Recommendation S.672-4 for satellite service area determination
itu_constants = ({
// Earth parameters
EARTH_RADIUS: 6371, // km
EARTH_FLATTENING: 1/298.257, // WGS84
// Satellite parameters (example geostationary)
SATELLITE_ALTITUDE: 35786, // km above Earth surface
// ITU-R S.672-4 service area parameters
MIN_ELEVATION_ANGLE: 5, // degrees (typical minimum for reliable service)
COVERAGE_CONTOURS: [0, 1, 2, 3, 5, 10, 20], // elevation angle contours in degrees
// Antenna parameters
ANTENNA_PATTERNS: {
"Global Beam": { beamwidth: 17.4 }, // degrees
"Hemi Beam": { beamwidth: 8.7 },
"Zone Beam": { beamwidth: 4.2 },
"Spot Beam": { beamwidth: 1.0 }
}
})

Insert cell
coordinate_transforms = {
const deg2rad = deg => deg * Math.PI / 180;
const rad2deg = rad => rad * 180 / Math.PI;

const haversineDistance = (lat1, lon1, lat2, lon2) => {
const R = itu_constants.EARTH_RADIUS;
const dLat = deg2rad(lat2 - lat1);
const dLon = deg2rad(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
};

const lookAngleToEarth = (satLon, satLat, azimuth, elevation) => {
const satLonRad = deg2rad(satLon);
const satLatRad = deg2rad(satLat);
const azRad = deg2rad(azimuth);
const elRad = deg2rad(elevation);
const earthLat = satLat + Math.cos(azRad) * (90 - elevation) * 0.5;
const earthLon = satLon + Math.sin(azRad) * (90 - elevation) * 0.5 / Math.cos(deg2rad(earthLat));
return [earthLon, earthLat];
};

return {
deg2rad,
rad2deg,
haversineDistance,
lookAngleToEarth
};
}

Insert cell
// Updated ITU-R S.672-4 Satellite Footprint Calculation - Complete Step 1a Implementation
function calculateSatelliteFootprint(satLongitude, satLatitude = 0, antennaType = "Global Beam", config = {}) {
const footprintContours = [];
const antenna = itu_constants.ANTENNA_PATTERNS[antennaType];
// Extract configuration parameters
const minElevation = config.minElevation || 5;
const beamCenterLat = config.beamCenterLat !== undefined ? config.beamCenterLat : satLatitude;
const beamCenterLon = config.beamCenterLon !== undefined ? config.beamCenterLon : satLongitude;
const pointingMode = config.pointingMode || 'nadir';
// Step 1a: Validate beam pointing is within satellite visibility
let validBeamCenter = { lat: beamCenterLat, lon: beamCenterLon };
if (pointingMode === 'variable') {
const isVisible = validateBeamPointing(satLongitude, satLatitude, beamCenterLon, beamCenterLat);
if (!isVisible) {
console.warn("Beam center outside satellite visibility. Adjusting to nearest visible point.");
validBeamCenter = adjustToVisiblePoint(satLongitude, satLatitude, beamCenterLon, beamCenterLat);
}
}
// Filter elevation angles based on minimum elevation setting
const validElevationAngles = itu_constants.COVERAGE_CONTOURS.filter(angle => angle >= minElevation);
// Calculate coverage contours based on ITU-R S.672-4
validElevationAngles.forEach(elevationAngle => {
const contourPoints = [];
if (elevationAngle === 0) {
// For 0° elevation, calculate maximum theoretical visibility from satellite
const maxVisibilityRadius = calculateMaxVisibilityRadius(satLongitude, satLatitude);
// Generate visibility circle around beam center (or satellite for nadir mode)
const centerLat = pointingMode === 'variable' ? validBeamCenter.lat : satLatitude;
const centerLon = pointingMode === 'variable' ? validBeamCenter.lon : satLongitude;
contourPoints.push(...generateSphericalCircle(centerLon, centerLat, maxVisibilityRadius, 36));
} else {
// Calculate Earth central angle based on elevation angle using ITU-R S.672-4 formula
const earthCentralAngle = calculateEarthCentralAngle(elevationAngle);
if (earthCentralAngle <= 0) {
// Elevation angle too high, no coverage possible
return;
}
const radiusDeg = coordinate_transforms.rad2deg(earthCentralAngle);
// Step 1a: Generate coverage contour around beam center (not satellite position)
const centerLat = pointingMode === 'variable' ? validBeamCenter.lat : satLatitude;
const centerLon = pointingMode === 'variable' ? validBeamCenter.lon : satLongitude;
// For variable pointing, we need to account for the off-nadir geometry
if (pointingMode === 'variable') {
contourPoints.push(...calculateOffNadirFootprint(
satLongitude, satLatitude,
centerLon, centerLat,
radiusDeg, elevationAngle
));
} else {
// Standard nadir pointing calculation
contourPoints.push(...generateSphericalCircle(centerLon, centerLat, radiusDeg, 72));
}
}
// Close the polygon and add to results
if (contourPoints.length > 0) {
contourPoints.push(contourPoints[0]);
footprintContours.push({
elevationAngle,
coordinates: contourPoints,
type: "Polygon",
beamCenter: [validBeamCenter.lon, validBeamCenter.lat],
pointingMode: pointingMode,
minElevation: minElevation
});
}
});
return footprintContours;
}

Insert cell
function validateBeamPointing(satLon, satLat, beamLon, beamLat) {
const angularDistance = calculateAngularDistance(satLon, satLat, beamLon, beamLat);
// Maximum theoretical visibility angle for geostationary satellite
// Based on Earth geometry: arccos(Re/(Re+h)) ≈ 81.3°
const maxVisibilityAngle = calculateMaxVisibilityAngle();
return angularDistance <= maxVisibilityAngle;
}

Insert cell
function calculateMaxVisibilityAngle() {
const Re = itu_constants.EARTH_RADIUS;
const h = itu_constants.SATELLITE_ALTITUDE;
// ITU-R S.672-4: Maximum Earth central angle for 0° elevation
return coordinate_transforms.rad2deg(Math.acos(Re / (Re + h)));
}
Insert cell
function adjustToVisiblePoint(satLon, satLat, beamLon, beamLat) {
const maxVisibilityAngle = calculateMaxVisibilityAngle();
const angularDistance = calculateAngularDistance(satLon, satLat, beamLon, beamLat);
if (angularDistance <= maxVisibilityAngle) {
return { lon: beamLon, lat: beamLat };
}
// Calculate bearing from satellite to desired beam center
const bearing = calculateBearing(satLon, satLat, beamLon, beamLat);
// Project to maximum visibility distance
return calculateDestinationPoint(satLon, satLat, bearing, maxVisibilityAngle);
}
Insert cell
function calculateEarthCentralAngle(elevationAngle) {
const h = itu_constants.SATELLITE_ALTITUDE;
const Re = itu_constants.EARTH_RADIUS;
const elRad = coordinate_transforms.deg2rad(elevationAngle);
// ITU-R S.672-4 formula for Earth central angle
const cosEarthAngle = (Re * Math.cos(elRad)) / (Re + h);
if (cosEarthAngle > 1 || cosEarthAngle < -1) {
return 0; // Invalid elevation angle
}
return Math.acos(cosEarthAngle) - elRad;
}
Insert cell
function calculateOffNadirFootprint(satLon, satLat, beamCenterLon, beamCenterLat, radiusDeg, elevationAngle) {
const points = [];
// Calculate the geometric correction for off-nadir pointing
const pointingAngle = calculateAngularDistance(satLon, satLat, beamCenterLon, beamCenterLat);
// For off-nadir pointing, the footprint is distorted due to the slant geometry
// This is a simplified approach - more complex implementations would account for
// the exact 3D geometry and Earth curvature effects
const correctionFactor = Math.cos(coordinate_transforms.deg2rad(pointingAngle * 0.7));
const adjustedRadius = radiusDeg / Math.max(correctionFactor, 0.1);
// Generate elliptical footprint for off-nadir pointing
for (let i = 0; i < 72; i++) {
const bearing = (i * 360) / 72;
const bearingRad = coordinate_transforms.deg2rad(bearing);
// Apply elliptical distortion based on pointing angle
const localRadius = adjustedRadius * (1 + 0.3 * Math.sin(bearingRad) * Math.sin(coordinate_transforms.deg2rad(pointingAngle)));
const point = calculateDestinationPoint(beamCenterLon, beamCenterLat, bearing, localRadius);
// Ensure coordinates are within valid range
const clampedLat = Math.max(-85, Math.min(85, point.lat));
const clampedLon = ((point.lon + 180) % 360) - 180;
points.push([clampedLon, clampedLat]);
}
return points;
}
Insert cell
function generateSphericalCircle(centerLon, centerLat, radiusDeg, numPoints = 72) {
const points = [];
for (let i = 0; i < numPoints; i++) {
const bearing = (i * 360) / numPoints;
const point = calculateDestinationPoint(centerLon, centerLat, bearing, radiusDeg);
// Ensure coordinates are within valid range
const clampedLat = Math.max(-85, Math.min(85, point.lat));
const clampedLon = ((point.lon + 180) % 360) - 180;
points.push([clampedLon, clampedLat]);
}
return points;
}
Insert cell
function calculateAngularDistance(lon1, lat1, lon2, lat2) {
const lat1Rad = coordinate_transforms.deg2rad(lat1);
const lat2Rad = coordinate_transforms.deg2rad(lat2);
const deltaLonRad = coordinate_transforms.deg2rad(lon2 - lon1);
// Haversine formula for great circle distance
const a = Math.sin((lat2Rad - lat1Rad) / 2) ** 2 +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLonRad / 2) ** 2;
return coordinate_transforms.rad2deg(2 * Math.asin(Math.sqrt(a)));
}
Insert cell
function calculateBearing(lon1, lat1, lon2, lat2) {
const lat1Rad = coordinate_transforms.deg2rad(lat1);
const lat2Rad = coordinate_transforms.deg2rad(lat2);
const deltaLonRad = coordinate_transforms.deg2rad(lon2 - lon1);
const y = Math.sin(deltaLonRad) * Math.cos(lat2Rad);
const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLonRad);
const bearingRad = Math.atan2(y, x);
return (coordinate_transforms.rad2deg(bearingRad) + 360) % 360;
}
Insert cell
function calculateDestinationPoint(startLon, startLat, bearing, angularDistance) {
const lat1Rad = coordinate_transforms.deg2rad(startLat);
const lon1Rad = coordinate_transforms.deg2rad(startLon);
const bearingRad = coordinate_transforms.deg2rad(bearing);
const distanceRad = coordinate_transforms.deg2rad(angularDistance);
const lat2Rad = Math.asin(
Math.sin(lat1Rad) * Math.cos(distanceRad) +
Math.cos(lat1Rad) * Math.sin(distanceRad) * Math.cos(bearingRad)
);
const lon2Rad = lon1Rad + Math.atan2(
Math.sin(bearingRad) * Math.sin(distanceRad) * Math.cos(lat1Rad),
Math.cos(distanceRad) - Math.sin(lat1Rad) * Math.sin(lat2Rad)
);
return {
lat: coordinate_transforms.rad2deg(lat2Rad),
lon: coordinate_transforms.rad2deg(lon2Rad)
};
}

// Helper function to calculate maximum visibility radius


Insert cell
Insert cell
function calculateMaxVisibilityRadius(satLon, satLat) {
// Return the maximum theoretical visibility angle
return calculateMaxVisibilityAngle();
}
Insert cell
// Cell 5: GeoJSON Footprint Generation
function generateFootprintGeoJSON(satLongitude, satLatitude = 0, antennaType = "Global Beam") {
const contours = calculateSatelliteFootprint(satLongitude, satLatitude, antennaType);
const features = contours.map(contour => ({
type: "Feature",
properties: {
elevationAngle: contour.elevationAngle,
antennaType: antennaType,
description: `${contour.elevationAngle}° elevation contour`
},
geometry: {
type: "Polygon",
coordinates: [contour.coordinates]
}
}));
return {
type: "FeatureCollection",
features: features
};
}
Insert cell
turfIntersect = (await import("https://cdn.skypack.dev/@turf/intersect")).default


Insert cell
turfArea = (await import("https://cdn.skypack.dev/@turf/area")).default
Insert cell
function calculateLandmassCoverage(footprintGeoJSON, worldTopology) {
const countries = topojson.feature(worldTopology, worldTopology.objects.countries);
const landCoverage = [];
footprintGeoJSON.features.forEach(footprintFeature => {
const elevationAngle = footprintFeature.properties.elevationAngle;
let totalLandArea = 0;
let coveredCountries = [];
// Get footprint coordinates
const footprintCoords = footprintFeature.geometry.coordinates[0];
countries.features.forEach(country => {
let isIntersecting = false;
let intersectionArea = 0;
// Check if any country vertices are inside the footprint
if (country.geometry.type === "Polygon") {
const countryCoords = country.geometry.coordinates[0];
isIntersecting = countryCoords.some(coord =>
isPointInPolygon(coord, footprintCoords)
);
if (isIntersecting) {
// Calculate approximate intersection area
intersectionArea = calculatePolygonIntersectionArea(
countryCoords, footprintCoords, country.properties.NAME || "Unknown"
);
}
} else if (country.geometry.type === "MultiPolygon") {
country.geometry.coordinates.forEach(polygon => {
const polyCoords = polygon[0];
const polyIntersects = polyCoords.some(coord =>
isPointInPolygon(coord, footprintCoords)
);
if (polyIntersects) {
isIntersecting = true;
intersectionArea += calculatePolygonIntersectionArea(
polyCoords, footprintCoords, country.properties.NAME || "Unknown"
);
}
});
}
// Also check if any footprint vertices are inside the country
if (!isIntersecting) {
isIntersecting = footprintCoords.some(coord => {
if (country.geometry.type === "Polygon") {
return isPointInPolygon(coord, country.geometry.coordinates[0]);
} else if (country.geometry.type === "MultiPolygon") {
return country.geometry.coordinates.some(polygon =>
isPointInPolygon(coord, polygon[0])
);
}
return false;
});
// If footprint point is inside country, estimate coverage
if (isIntersecting) {
intersectionArea = estimateCountryFootprintOverlap(country, footprintCoords);
}
}
if (isIntersecting) {
coveredCountries.push({
name: country.properties.NAME || country.properties.name || "Unknown",
iso: country.properties.ISO_A3 || country.properties.iso_a3 || "N/A",
areaKm2: Math.round(intersectionArea)
});
totalLandArea += intersectionArea;
}
});
landCoverage.push({
elevationAngle,
coveredCountries,
countryCount: coveredCountries.length,
totalLandAreaKm2: Math.round(totalLandArea),
footprintAreaKm2: Math.round(calculatePolygonAreaKm2(footprintCoords))
});
});
return landCoverage;
}




// Calculate approximate intersection area between two polygons


// Get polygon bounding box [minLon, minLat, maxLon, maxLat]

// Calculate rectangle area in km² using spherical geometry


// Estimate overlap area when footprint vertices are inside country



Insert cell
function estimateCountryFootprintOverlap(country, footprintCoords) {
// Simple estimation: calculate how many footprint points are inside the country
// and estimate proportional area
let insidePoints = 0;
const totalPoints = footprintCoords.length - 1; // Exclude duplicate last point
footprintCoords.slice(0, -1).forEach(coord => {
if (country.geometry.type === "Polygon") {
if (isPointInPolygon(coord, country.geometry.coordinates[0])) {
insidePoints++;
}
} else if (country.geometry.type === "MultiPolygon") {
const isInside = country.geometry.coordinates.some(polygon =>
isPointInPolygon(coord, polygon[0])
);
if (isInside) insidePoints++;
}
});
// Estimate coverage as proportion of footprint area
const footprintArea = calculatePolygonAreaKm2(footprintCoords);
const coverageRatio = insidePoints / totalPoints;
return footprintArea * coverageRatio;
}
Insert cell
function calculateRectangleAreaKm2(lon1, lat1, lon2, lat2) {
const R = itu_constants.EARTH_RADIUS;
const lat1Rad = coordinate_transforms.deg2rad(lat1);
const lat2Rad = coordinate_transforms.deg2rad(lat2);
const deltaLon = coordinate_transforms.deg2rad(Math.abs(lon2 - lon1));
// Area of spherical rectangle
const area = R * R * Math.abs(deltaLon) * Math.abs(Math.sin(lat2Rad) - Math.sin(lat1Rad));
return area;
}
Insert cell
function getPolygonBounds(coordinates) {
let minLon = Infinity, minLat = Infinity;
let maxLon = -Infinity, maxLat = -Infinity;
coordinates.forEach(([lon, lat]) => {
minLon = Math.min(minLon, lon);
maxLon = Math.max(maxLon, lon);
minLat = Math.min(minLat, lat);
maxLat = Math.max(maxLat, lat);
});
return [minLon, minLat, maxLon, maxLat];
}
Insert cell
function calculatePolygonIntersectionArea(polygon1, polygon2, countryName) {
// Simplified intersection calculation using sampling method
// For production use, consider using turf.js or similar library
// Get bounding box of intersection
const bounds1 = getPolygonBounds(polygon1);
const bounds2 = getPolygonBounds(polygon2);
const intersectionBounds = [
Math.max(bounds1[0], bounds2[0]), // min lon
Math.max(bounds1[1], bounds2[1]), // min lat
Math.min(bounds1[2], bounds2[2]), // max lon
Math.min(bounds1[3], bounds2[3]) // max lat
];
// If no bounding box intersection, return 0
if (intersectionBounds[0] >= intersectionBounds[2] || intersectionBounds[1] >= intersectionBounds[3]) {
return 0;
}
// Sample points within intersection bounds
const samples = 1000; // Adjust for accuracy vs performance
let insideCount = 0;
const [minLon, minLat, maxLon, maxLat] = intersectionBounds;
const lonStep = (maxLon - minLon) / Math.sqrt(samples);
const latStep = (maxLat - minLat) / Math.sqrt(samples);
for (let lon = minLon; lon < maxLon; lon += lonStep) {
for (let lat = minLat; lat < maxLat; lat += latStep) {
const point = [lon, lat];
if (isPointInPolygon(point, polygon1) && isPointInPolygon(point, polygon2)) {
insideCount++;
}
}
}
// Calculate area of intersection bounds
const boundsArea = calculateRectangleAreaKm2(
intersectionBounds[0], intersectionBounds[1],
intersectionBounds[2], intersectionBounds[3]
);
// Estimate intersection area
const totalSamples = Math.pow(Math.sqrt(samples), 2);
return (insideCount / totalSamples) * boundsArea;
}
Insert cell
function calculatePolygonAreaKm2(coordinates) {
if (coordinates.length < 3) return 0;
// Convert to radians and calculate spherical area using Girard's theorem
const R = itu_constants.EARTH_RADIUS; // Earth radius in km
let area = 0;
for (let i = 0; i < coordinates.length - 1; i++) {
const [lon1, lat1] = coordinates[i];
const [lon2, lat2] = coordinates[(i + 1) % (coordinates.length - 1)];
const lat1Rad = coordinate_transforms.deg2rad(lat1);
const lat2Rad = coordinate_transforms.deg2rad(lat2);
const deltaLon = coordinate_transforms.deg2rad(lon2 - lon1);
// Spherical trapezoid area calculation
const E = 2 * Math.atan2(
Math.tan(deltaLon / 2) * (Math.sin(lat1Rad) + Math.sin(lat2Rad)),
2 + Math.sin(lat1Rad) * Math.sin(lat2Rad) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLon)
);
area += E;
}
return Math.abs(area) * R * R;
}
Insert cell
function isPointInPolygon(point, polygon) {
const [x, y] = point;
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const [xi, yi] = polygon[i];
const [xj, yj] = polygon[j];
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
inside = !inside;
}
}
return inside;
}
Insert cell
Insert cell
// Cell 8: Generate Current Footprint Data
currentFootprint = {
const config = satelliteConfig; // Capture the current state
return generateFootprintGeoJSON(
+config.longitude,
+config.latitude,
config.antennaType
);
}
Insert cell
// Cell 9: Calculate Landmass Coverage
landmassCoverage = {
const footprint = currentFootprint; // Capture the current footprint
return calculateLandmassCoverage(footprint, world);
}
Insert cell
create = (await import("https://cdn.skypack.dev/d3@7")).create

Insert cell
geoOrthographic = (await import("https://cdn.skypack.dev/d3@7")).geoOrthographic


Insert cell
geoPath = (await import("https://cdn.skypack.dev/d3@7")).geoPath
Insert cell
scaleOrdinal = (await import("https://cdn.skypack.dev/d3@7")).scaleOrdinal
Insert cell

schemeCategory10 = (await import("https://cdn.skypack.dev/d3@7")).schemeCategory10

Insert cell
geoGraticule = (await import("https://cdn.skypack.dev/d3@7")).geoGraticule
Insert cell
// Cell 10: Main Visualization
{
const width = 960;
const height = 500;
// Create SVG using D3's create function
const svg = create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "#001122");
// Set up projection
const projection = geoOrthographic()
.scale(240)
.translate([width / 2, height / 2])
.rotate([-satelliteConfig.longitude, -satelliteConfig.latitude]);
const path = geoPath(projection);
// Color scale for elevation contours
const colorScale = scaleOrdinal()
.domain(itu_constants.COVERAGE_CONTOURS)
.range(schemeCategory10);
// Draw graticule
svg.append("path")
.datum(geoGraticule())
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#333")
.attr("stroke-width", 0.5);
// Draw countries
svg.append("path")
.datum(topojson.feature(world, world.objects.countries))
.attr("d", path)
.attr("fill", "#404040")
.attr("stroke", "#666")
.attr("stroke-width", 0.5);
// Draw country borders
svg.append("path")
.datum(topojson.mesh(world, world.objects.countries, (a, b) => a !== b))
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "#888")
.attr("stroke-width", 0.5);
// Draw footprint contours
currentFootprint.features.forEach((feature, i) => {
svg.append("path")
.datum(feature)
.attr("d", path)
.attr("fill", colorScale(feature.properties.elevationAngle))
.attr("fill-opacity", 0.3)
.attr("stroke", colorScale(feature.properties.elevationAngle))
.attr("stroke-width", 2)
.append("title")
.text(`${feature.properties.elevationAngle}° elevation contour`);
});
// Draw satellite position
const satPos = projection([+satelliteConfig.longitude, +satelliteConfig.latitude]);
if (satPos) {
svg.append("circle")
.attr("cx", satPos[0])
.attr("cy", satPos[1])
.attr("r", 8)
.attr("fill", "#ff0000")
.attr("stroke", "#fff")
.attr("stroke-width", 2)
.append("title")
.text(`Satellite at ${satelliteConfig.longitude}°, ${satelliteConfig.latitude}°`);
}
// Add legend
const legend = svg.append("g")
.attr("transform", "translate(20, 20)");
itu_constants.COVERAGE_CONTOURS.forEach((angle, i) => {
const legendItem = legend.append("g")
.attr("transform", `translate(0, ${i * 20})`);
legendItem.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", colorScale(angle))
.attr("fill-opacity", 0.7);
legendItem.append("text")
.attr("x", 20)
.attr("y", 12)
.attr("fill", "#fff")
.attr("font-size", "12px")
.text(`${angle}° elevation`);
});
return svg.node();
}

Insert cell
// Cell 11: Coverage Statistics Table
{
const tableData = landmassCoverage.map(coverage => ({
"Elevation Angle": coverage.elevationAngle + "°",
"Countries Covered": coverage.countryCount,
"Total Land Area (km²)": coverage.totalLandAreaKm2?.toLocaleString() || "0",
"Footprint Area (km²)": coverage.footprintAreaKm2?.toLocaleString() || "0",
"Land Coverage %": coverage.footprintAreaKm2 > 0 ?
((coverage.totalLandAreaKm2 / coverage.footprintAreaKm2) * 100).toFixed(1) + "%" : "0%",
"Sample Countries": coverage.coveredCountries.slice(0, 3).map(c =>
`${c.name} (${c.areaKm2?.toLocaleString() || 0} km²)`
).join(", ") + (coverage.coveredCountries.length > 3 ? "..." : "")
}));
return html`
<div style="margin: 20px 0;">
<h3>Coverage Statistics (ITU-R S.672-4 Analysis)</h3>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr style="background: #f0f0f0;">
<th style="border: 1px solid #ddd; padding: 6px;">Elevation Angle</th>
<th style="border: 1px solid #ddd; padding: 6px;">Countries</th>
<th style="border: 1px solid #ddd; padding: 6px;">Total Land Area (km²)</th>
<th style="border: 1px solid #ddd; padding: 6px;">Footprint Area (km²)</th>
<th style="border: 1px solid #ddd; padding: 6px;">Land Coverage %</th>
<th style="border: 1px solid #ddd; padding: 6px;">Sample Countries (Area)</th>
</tr>
</thead>
<tbody>
${tableData.map(row => `
<tr>
<td style="border: 1px solid #ddd; padding: 6px; text-align: center;">${row["Elevation Angle"]}</td>
<td style="border: 1px solid #ddd; padding: 6px; text-align: center;">${row["Countries Covered"]}</td>
<td style="border: 1px solid #ddd; padding: 6px; text-align: right;">${row["Total Land Area (km²)"]}</td>
<td style="border: 1px solid #ddd; padding: 6px; text-align: right;">${row["Footprint Area (km²)"]}</td>
<td style="border: 1px solid #ddd; padding: 6px; text-align: center;">${row["Land Coverage %"]}</td>
<td style="border: 1px solid #ddd; padding: 6px;">${row["Sample Countries"]}</td>
</tr>
`).join("")}
</tbody>
</table>
<div style="margin-top: 15px; padding: 10px; background: #e7f3ff; border-radius: 5px;">
<h4>Area Calculation Methods:</h4>
<ul style="margin: 5px 0; padding-left: 20px; font-size: 11px;">
<li><strong>Footprint Area:</strong> Calculated using spherical geometry (Girard's theorem)</li>
<li><strong>Land Area:</strong> Estimated intersection area using sampling method with ${1000} sample points</li>
<li><strong>Land Coverage %:</strong> Ratio of land area to total footprint area</li>
<li><strong>Individual Country Areas:</strong> Calculated per country intersection with footprint</li>
</ul>
</div>
</div>
`;
}

Insert cell
turfRewind = (await import("https://cdn.skypack.dev/@turf/rewind")).default

Insert cell
// Cell 12: Export GeoJSON Data
{
const exportData = {
footprint: currentFootprint,
coverage: landmassCoverage,
metadata: {
standard: "ITU-R S.672-4",
satellite: {
longitude: +satelliteConfig.longitude,
latitude: +satelliteConfig.latitude,
altitude: itu_constants.SATELLITE_ALTITUDE,
antennaType: satelliteConfig.antennaType
},
generated: new Date().toISOString()
}
};
return html`
<div style="margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 5px;">
<h3>Export Data</h3>
<p>Complete GeoJSON and coverage data following ITU-R S.672-4 standards:</p>
<textarea readonly style="width: 100%; height: 100px; font-family: monospace;">${JSON.stringify(exportData, null, 2)}</textarea>
<br><br>
<button onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">
Copy to Clipboard
</button>
</div>
`;
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more