The app is a React 19 + Three.js 3D globe visualization (src/App.jsx is the main monolithic component). There is already a timezones layer that loads /public/timezones.geojson, renders GeoJSON polygon boundaries as THREE.Line objects on the globe, and supports interactive hover highlighting + tooltip. A new file /public/fir-regions.geojson has been added and needs to appear as an identical layer.
- Render
fir-regions.geojsonas a layer exactly like the timezones one (boundary lines on the globe). - No UI toggle (no checkbox in desktop overlay or mobile menu).
- Visibility is controlled only by the
Fkeystroke (press to show, press again to hide). - The layer should follow all existing patterns: fade-in/out animation, proper Three.js disposal on hide, support for BW mode colors, correct radius placement above the Earth sphere.
- On hover, show a tooltip with the FIR name (and ICAO code), plus highlight the hovered region — exactly like the timezone layer does.
Each feature in fir-regions.geojson has these properties:
FIR_NAME— the FIR name, e.g."WASHINGTON","ALGER","TRIPOLI"(null for 35 out of 332 features)ICAO_CODE— the ICAO identifier, e.g."KZDC","DAAA"(null when FIR_NAME is null)TYPE— always"FIR"FID— numeric feature ID
All geometries are MultiPolygon. Total: 332 features (297 named, 35 unnamed).
All changes are in src/App.jsx only (no CSS changes needed since there's no UI toggle).
Around line 72, next to the existing showTimezones state, add:
const [showFirRegions, setShowFirRegions] = useState(false)Add a data ref to cache the parsed GeoJSON (same pattern as timezoneDataRef):
const firDataRef = useRef(null)Add a ref to track the currently highlighted FIR feature index (same pattern as highlightedTimezoneRef):
const highlightedFirRef = useRef(null)Model it exactly on the timezone rendering effect (lines ~2096–2279), but without the date line or date line label. Create a new useEffect with dependency [showFirRegions]:
- Cleanup: Find the existing group by name
'fir-boundaries'. If it exists, fade its opacity from 0.3 → 0 usinganimateValue, then remove from scene and dispose all geometries/materials. - Early exit: If
!showFirRegions, return. - Fetch & parse:
fetch('/fir-regions.geojson')→.json(). Store the result infirDataRef.current. - Build Three.js lines: For each GeoJSON feature:
- Handle both
PolygonandMultiPolygongeometry types (all features are MultiPolygon in practice). - For each ring, convert
[lon, lat]coordinates to 3D vectors usinglatLonToVector3(lat, lon, 2.005)(same radius as timezones). - Create a
THREE.BufferGeometryfrom the vectors. - Create a
THREE.Linewith aTHREE.LineBasicMaterial:color:isBWMode ? 0x0f0f0f : 0xfffffftransparent: trueopacity: 0
- Store
line.userData.featureIndex = ion each line. - Add line to a
THREE.Group.
- Handle both
- Name the group
'fir-boundaries'and add it tosceneRef.current. - Animate in: Use
animateValue(0, 0.3, ...)to fade all materials' opacity from 0 → 0.3.
Important: reference isBWMode via isBWModeRef.current (the ref, not the state directly) to avoid adding it as a dependency, consistent with how the timezone effect works.
Model this exactly on the timezone tooltip/highlight effect (lines ~2281–2412). Create a new useEffect with dependencies [showFirRegions, isMobile, isBWMode]:
- Early exit if
!showFirRegionsorisMobile. handleMouseMove: On mouse move over the canvas:- Raycast against the Earth mesh to find the surface intersection point.
- Convert the intersection point to
{ lat, lon }using the existingvector3ToLatLonhelper. - Tooltip text: Look up the FIR region using point-in-polygon (same
pointInPolygonfrompoint-in-polygon-haoalready imported). Iterate throughfirDataRef.current.features, testing each polygon. When a match is found, display the tooltip using the existingtooltipRef:- If
FIR_NAMEis not null: show"FIR_NAME · ICAO_CODE"(e.g."WASHINGTON · KZDC") - If
FIR_NAMEis null: show"Unknown FIR"or hide tooltip
- If
- Position tooltip at
clientX + 16, clientY + 16(same offset as timezone tooltip). - Highlight: Same throttling pattern (3px minimum movement). When the matched feature index changes:
- Set matched feature lines to
opacity: 1.0 - Dim all other feature lines to
opacity: 0.1 - Track in
highlightedFirRef.current
- Set matched feature lines to
resetHighlight: Reset all lines in'fir-boundaries'group back toopacity: 0.3and default color. ClearhighlightedFirRef.current.handleMouseLeave: Hide tooltip, callresetHighlight.- Cleanup: Remove event listeners on effect cleanup.
Important: reuse the same tooltipRef element that the timezone layer uses — do not create a second tooltip div. The two layers won't be active simultaneously in practice, but even if they were, only one tooltip should show.
In the existing keydown event listener (around line 2550+), add a block for the F key:
if (e.key === 'f' || e.key === 'F') {
setShowFirRegions(prev => !prev)
}Unlike timezones, there is no mutual exclusivity with graticule or any other layer — it's a simple toggle.
Make sure the FIR group is disposed if the component unmounts. In the main cleanup return function of the scene setup effect, add disposal logic for 'fir-boundaries' (same pattern as 'timezone-boundaries').
Some FIR regions (e.g. ANCHORAGE OCEANIC / PAZA) straddle the International Date Line and are stored as separate features in the GeoJSON — one with longitudes near +180°, the other near -180°. They share the same FIR_NAME + ICAO_CODE. These must not be merged in the GeoJSON (stitching coordinates across ±180° creates lines that wrap around the entire globe).
Instead, handle this in code at two levels:
After parsing the GeoJSON, build a lookup map that groups feature indices by their FIR_NAME + ICAO_CODE key:
const firGroupMap = {} // key: "FIR_NAME|ICAO_CODE" → value: [featureIndex, featureIndex, ...]
data.features.forEach((f, i) => {
const name = f.properties.FIR_NAME
const icao = f.properties.ICAO_CODE
if (name && icao) {
const key = `${name}|${icao}`
if (!firGroupMap[key]) firGroupMap[key] = []
firGroupMap[key].push(i)
}
})Store this map in a ref (firGroupMapRef) so the hover effect can use it.
When creating Three.js lines, store both line.userData.featureIndex = i (for individual identification) and line.userData.groupKey = key (for group highlighting).
When a point-in-polygon match is found for feature index i:
- Look up its
groupKeyfrom the matched feature's properties (FIR_NAME|ICAO_CODE). - Get all sibling indices from
firGroupMapRef.current[groupKey]. - Highlight all siblings together — set opacity 1.0 on every line whose
userData.featureIndexis in the sibling list, and dim all others to 0.1. - Track the
groupKey(not just the feature index) inhighlightedFirRef.currentto avoid redundant updates when the mouse moves between sibling polygons.
This way, hovering over either half of a date-line-split FIR highlights the entire region as one.
- Do not add any UI checkbox (no desktop overlay, no mobile menu item).
- Do not create a separate component — keep everything in
App.jsx, following the existing pattern. - The
latLonToVector3helper already exists (around line 170) — reuse it. - The
vector3ToLatLonhelper already exists — reuse it for converting raycast hit to lat/lon. - The
animateValuehelper already exists — reuse it. isBWModeRefalready exists — use it for color decisions inside the effect.pointInPolygonfrompoint-in-polygon-haois already imported — reuse it.- Reuse the existing
tooltipRefelement — do not create a second tooltip DOM node. - Use
THREE.Groupnamed'fir-boundaries'so it can be found/removed by name. - The GeoJSON contains only
MultiPolygongeometries, but handlePolygonas well for safety.