// overlay_geom.js — vector overlays → line-segment vertex arrays for the globe and flat views import { R_KM, toVec, interpolate, project, EE_XMAX, PC_XMAX } from './geo.js'; const onSeam = (a, b) => Math.abs(a[0]) === 180 && Math.abs(b[0]) === 180; export function linesFromGeoJSON(fc, keep = () => true) { const out = []; const add = coords => { // split at segments that run along the antimeridian cut let cur = []; coords.forEach((c, i) => { if (i > 0 && onSeam(coords[i - 1], c)) { if (cur.length > 1) out.push(cur); cur = []; } cur.push({ lat: c[1], lon: c[0] }); }); if (cur.length > 1) out.push(cur); }; for (const f of fc.features || []) { const g = f.geometry; if (!g || !keep(f.properties || {})) continue; if (g.type === 'LineString') add(g.coordinates); else if (g.type === 'MultiLineString' || g.type === 'Polygon') g.coordinates.forEach(add); else if (g.type === 'MultiPolygon') g.coordinates.forEach(poly => poly.forEach(add)); } return out; } export function densify(lines, stepDeg = 1) { return lines.map(line => { if (line.length < 2) return [...line]; const out = []; for (let i = 0; i + 1 < line.length; i++) out.push(...interpolate(line[i], line[i + 1], stepDeg).slice(i === 0 ? 0 : 1)); return out; }); } export function globeSegments(lines, radius = 1.002, stepDeg = 1) { const pos = []; for (const line of lines) { for (let i = 0; i + 1 < line.length; i++) { const pts = interpolate(line[i], line[i + 1], stepDeg); for (let k = 0; k + 1 < pts.length; k++) { const a = toVec(pts[k].lat, pts[k].lon), b = toVec(pts[k + 1].lat, pts[k + 1].lon); pos.push(a[0] * radius, a[1] * radius, a[2] * radius, b[0] * radius, b[1] * radius, b[2] * radius); } } } return new Float32Array(pos); } // Lines laid on the 3D ground near the camera. Segments with an end inside the cap (cosCap around n, the eye's // direction) are cut into pieces ≈ 1/100 of their distance from the eye; each vertex sits on heightM (the drawn // ground, m), then is pulled 1% of the way toward the eye: the same pixel, a nearer depth, so the line wins over // the ground it lies on but hills in front still hide it. Positions are relative to origin. const PULL = 0.01; export function drapedSegments(lines, { eye, n, cosCap, origin, heightM }) { const pos = [], kmPerDeg = (R_KM * Math.PI) / 180; const inCap = p => { const v = toVec(p.lat, p.lon); return v[0] * n[0] + v[1] * n[1] + v[2] * n[2] >= cosCap; }; const distKm = p => { const v = toVec(p.lat, p.lon); return Math.hypot(v[0] - eye[0], v[1] - eye[1], v[2] - eye[2]) * R_KM; }; const put = p => { const v = toVec(p.lat, p.lon), r = 1 + (Math.max(0, heightM(p.lat, p.lon)) + 1) / (R_KM * 1000); for (let i = 0; i < 3; i++) pos.push(v[i] * r + (eye[i] - v[i] * r) * PULL - origin[i]); }; for (const line of lines) { for (let i = 0; i + 1 < line.length; i++) { const a = line[i], b = line[i + 1]; if (!inCap(a) && !inCap(b)) continue; const pts = interpolate(a, b, Math.max(0.01, Math.min(distKm(a), distKm(b)) / 100) / kmPerDeg); for (let k = 0; k + 1 < pts.length; k++) { put(pts[k]); put(pts[k + 1]); } } } return new Float32Array(pos); } export function flatSegments(lines, view, z = 0.001) { const xmax = view === 'equal_earth' ? EE_XMAX : PC_XMAX; const pos = []; for (const line of lines) { let prev = null; for (const p of line) { const q = project(view, p.lat, p.lon); if (prev && Math.abs(q.x - prev.x) < xmax) pos.push(prev.x, prev.y, z, q.x, q.y, z); prev = q; } } return new Float32Array(pos); } export function graticuleLines(step = 30) { const lines = []; for (let lon = -180; lon <= 180; lon += step) { const l = []; for (let lat = -90; lat <= 90; lat += 2) l.push({ lat, lon }); lines.push(l); } for (let lat = -90 + step; lat < 90; lat += step) { const l = []; for (let lon = -180; lon <= 180; lon += 2) l.push({ lat, lon }); lines.push(l); } return lines; }