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:
@@ -359,20 +359,44 @@ export default function Settings() {
|
||||
const handleHorizonCSV = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
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 lines = text.trim().split('\n').slice(1); // skip header
|
||||
const points: HorizonPoint[] = [];
|
||||
for (const line of lines) {
|
||||
const raw: HorizonPoint[] = [];
|
||||
for (const line of text.trim().split('\n')) {
|
||||
const [az, alt] = line.split(',').map(Number);
|
||||
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) {
|
||||
setHorizon.mutate(points);
|
||||
} else {
|
||||
alert(`CSV must have exactly 360 rows (got ${points.length}). Format: az_deg,alt_deg`);
|
||||
if (raw.length < 2) {
|
||||
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 {
|
||||
// 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 = () => {
|
||||
@@ -396,7 +420,7 @@ export default function Settings() {
|
||||
{horizonData?.points && <HorizonPolarChart points={horizonData.points} />}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<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>
|
||||
<label style={{
|
||||
display: 'inline-flex',
|
||||
|
||||
Reference in New Issue
Block a user