Interpolate sparse horizon CSVs instead of rejecting them

Any CSV with ≥ 2 az_deg,alt_deg rows now works. Points are sorted by
azimuth, then linearly interpolated to fill all 360 degrees. A 28-point
sparse horizon profile interpolates cleanly; an exact 360-point file is
used as-is. Also resets the file input after upload so re-uploading the
same file works without picking a different one first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 14:09:59 +02:00
parent e9f7741feb
commit 53662ac36f
+32 -8
View File
@@ -359,20 +359,44 @@ export default function Settings() {
const handleHorizonCSV = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleHorizonCSV = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
// Reset input so the same file can be re-uploaded after a reset
e.target.value = '';
const text = await file.text(); const text = await file.text();
const lines = text.trim().split('\n').slice(1); // skip header const raw: HorizonPoint[] = [];
const points: HorizonPoint[] = []; for (const line of text.trim().split('\n')) {
for (const line of lines) {
const [az, alt] = line.split(',').map(Number); const [az, alt] = line.split(',').map(Number);
if (!isNaN(az) && !isNaN(alt)) { if (!isNaN(az) && !isNaN(alt)) {
points.push({ az_deg: Math.round(az) % 360, alt_deg: Math.max(0, Math.min(90, alt)) }); raw.push({ az_deg: ((Math.round(az) % 360) + 360) % 360, alt_deg: Math.max(0, Math.min(90, alt)) });
} }
} }
if (points.length === 360) { if (raw.length < 2) {
setHorizon.mutate(points); alert('CSV must have at least 2 valid az_deg,alt_deg rows.');
return;
}
// Sort by azimuth
raw.sort((a, b) => a.az_deg - b.az_deg);
// If already 360 points, use as-is; otherwise interpolate to fill every degree
let full: HorizonPoint[];
if (raw.length === 360) {
full = raw;
} else { } else {
alert(`CSV must have exactly 360 rows (got ${points.length}). Format: az_deg,alt_deg`); // Wrap around: append first point at az+360 to close the circle
const pts = [...raw, { az_deg: raw[0].az_deg + 360, alt_deg: raw[0].alt_deg }];
full = Array.from({ length: 360 }, (_, deg) => {
// Find the two surrounding control points
let lo = pts[0], hi = pts[pts.length - 1];
for (let i = 0; i < pts.length - 1; i++) {
if (pts[i].az_deg <= deg && pts[i + 1].az_deg >= deg) {
lo = pts[i]; hi = pts[i + 1]; break;
} }
}
const span = hi.az_deg - lo.az_deg;
const t = span > 0 ? (deg - lo.az_deg) / span : 0;
return { az_deg: deg, alt_deg: Math.max(0, lo.alt_deg + t * (hi.alt_deg - lo.alt_deg)) };
});
}
setHorizon.mutate(full);
}; };
const resetHorizon = () => { const resetHorizon = () => {
@@ -396,7 +420,7 @@ export default function Settings() {
{horizonData?.points && <HorizonPolarChart points={horizonData.points} />} {horizonData?.points && <HorizonPolarChart points={horizonData.points} />}
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{ fontSize: 12, color: 'var(--text-mid)', maxWidth: 300 }}> <div style={{ fontSize: 12, color: 'var(--text-mid)', maxWidth: 300 }}>
Upload a CSV file with columns <code>az_deg,alt_deg</code>, one row per degree (360 rows total). Upload a CSV with columns <code>az_deg,alt_deg</code>. Sparse files (any number of points 2) are linearly interpolated to 360°.
</div> </div>
<label style={{ <label style={{
display: 'inline-flex', display: 'inline-flex',