Skip to content

Commit d70546c

Browse files
authored
Merge pull request #45 from reiserlab/claude/add-mercator-mol-visualization-ELxsx
visualization + fly eye FOV updates
2 parents 9f39ef3 + c80d276 commit d70546c

3 files changed

Lines changed: 213 additions & 70 deletions

File tree

js/pattern-editor/viewers/mollweide-viewer.js

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* Output coordinates are converted to degrees for consistent axis labeling.
1313
* The projection boundary is an ellipse.
1414
*
15+
* Supports optional eye FOV overlay from CSV data files containing
16+
* Mollweide-projected boundary coordinates (in radians).
17+
*
1518
* @module mollweide-viewer
1619
*/
1720

@@ -23,6 +26,12 @@ const PI = Math.PI;
2326
class MollweideViewer extends ProjectionViewer {
2427
constructor(container) {
2528
super(container, 'mollweide');
29+
30+
// Eye FOV overlay state
31+
this.showEyeFOV = false;
32+
this.eyeFOVLeft = null;
33+
this.eyeFOVRight = null;
34+
this._eyeFOVLoading = false;
2635
}
2736

2837
/**
@@ -107,15 +116,123 @@ class MollweideViewer extends ProjectionViewer {
107116
};
108117
}
109118

119+
// ========================================
120+
// Eye FOV overlay
121+
// ========================================
122+
123+
/**
124+
* Toggle eye FOV overlay visibility.
125+
* Loads CSV data lazily on first enable.
126+
* @param {boolean} show - Whether to show the eye FOV overlay
127+
*/
128+
setShowEyeFOV(show) {
129+
this.showEyeFOV = show;
130+
if (show && !this.eyeFOVLeft && !this._eyeFOVLoading) {
131+
this._loadEyeFOV().then(() => this._render());
132+
} else {
133+
this._render();
134+
}
135+
}
136+
137+
/**
138+
* Fetch and parse eye FOV boundary CSV files.
139+
* CSV files contain Mollweide-projected coordinates in radians (x1, y1).
140+
* Converts to degrees to match the viewer's coordinate system.
141+
*/
142+
async _loadEyeFOV() {
143+
this._eyeFOVLoading = true;
144+
try {
145+
const [leftResp, rightResp] = await Promise.all([
146+
fetch('data/fov_left_Mo.csv'),
147+
fetch('data/fov_right_Mo.csv')
148+
]);
149+
150+
if (!leftResp.ok || !rightResp.ok) {
151+
console.error('Eye FOV: Failed to load CSV files');
152+
this._eyeFOVLoading = false;
153+
return;
154+
}
155+
156+
const [leftText, rightText] = await Promise.all([leftResp.text(), rightResp.text()]);
157+
158+
this.eyeFOVLeft = this._parseEyeFOVCSV(leftText);
159+
this.eyeFOVRight = this._parseEyeFOVCSV(rightText);
160+
161+
console.log(
162+
'Eye FOV loaded:',
163+
this.eyeFOVLeft.length,
164+
'left pts,',
165+
this.eyeFOVRight.length,
166+
'right pts'
167+
);
168+
} catch (err) {
169+
console.error('Eye FOV: Load error:', err.message);
170+
}
171+
this._eyeFOVLoading = false;
172+
}
173+
110174
/**
111-
* Draw Mollweide-specific decorations: the elliptical boundary.
175+
* Parse a CSV string of Mollweide coordinates (radians) into degree points.
176+
* @param {string} csvText - CSV with "x1","y1" header and radian values
177+
* @returns {Array<{x: number, y: number}>} Points in degrees (Mollweide map space)
178+
*/
179+
_parseEyeFOVCSV(csvText) {
180+
const lines = csvText.trim().split('\n');
181+
const points = [];
182+
// Skip header row
183+
for (let i = 1; i < lines.length; i++) {
184+
const line = lines[i].trim();
185+
if (!line) continue;
186+
const parts = line.split(',');
187+
if (parts.length < 2) continue;
188+
const xRad = parseFloat(parts[0]);
189+
const yRad = parseFloat(parts[1]);
190+
if (isNaN(xRad) || isNaN(yRad)) continue;
191+
// Convert from Mollweide radians to degrees
192+
points.push({
193+
x: (xRad * 180) / PI,
194+
y: (yRad * 180) / PI
195+
});
196+
}
197+
return points;
198+
}
199+
200+
/**
201+
* Draw eye FOV boundary polygons on the projection.
202+
* @param {CanvasRenderingContext2D} ctx
203+
* @param {Function} mapToCanvas - Convert (mapX, mapY) => { cx, cy }
204+
*/
205+
_drawEyeFOV(ctx, mapToCanvas) {
206+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
207+
ctx.lineWidth = 2.5;
208+
209+
for (const points of [this.eyeFOVLeft, this.eyeFOVRight]) {
210+
if (!points || points.length < 3) continue;
211+
212+
ctx.beginPath();
213+
for (let i = 0; i < points.length; i++) {
214+
const { cx, cy } = mapToCanvas(points[i].x, points[i].y);
215+
if (i === 0) {
216+
ctx.moveTo(cx, cy);
217+
} else {
218+
ctx.lineTo(cx, cy);
219+
}
220+
}
221+
ctx.closePath();
222+
ctx.stroke();
223+
}
224+
}
225+
226+
// ========================================
227+
// Decorations
228+
// ========================================
229+
230+
/**
231+
* Draw Mollweide-specific decorations: elliptical boundary + optional eye FOV.
112232
*/
113233
_drawDecorations(ctx, mapToCanvas) {
114234
// Draw the full-sphere ellipse outline
115-
// The Mollweide boundary at full FOV is an ellipse:
116-
// x = (2√2/π)·λ·cos(θ), y = √2·sin(θ)
117-
// For the full boundary: λ = ±π, lat varies
118-
ctx.strokeStyle = '#3d4a58';
235+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
119236
ctx.lineWidth = 1;
120237
ctx.beginPath();
121238

@@ -142,6 +259,11 @@ class MollweideViewer extends ProjectionViewer {
142259
}
143260
ctx.closePath();
144261
ctx.stroke();
262+
263+
// Draw eye FOV overlay if enabled and loaded
264+
if (this.showEyeFOV && this.eyeFOVLeft && this.eyeFOVRight) {
265+
this._drawEyeFOV(ctx, mapToCanvas);
266+
}
145267
}
146268
}
147269

0 commit comments

Comments
 (0)