Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions js/pattern-editor/viewers/mercator-viewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Mercator (equirectangular) projection viewer.
*
* Plots arena pixels on a longitude × latitude grid, matching
* the MATLAB PatternPreviewerApp Mercator view.
*
* In this projection:
* x = longitude (degrees)
* y = latitude (degrees)
* which is technically an equirectangular (Plate Carrée) projection.
* This matches the companion MATLAB implementation.
*
* @module mercator-viewer
*/

import { ProjectionViewer } from './projection-viewer.js';

class MercatorViewer extends ProjectionViewer {
constructor(container) {
super(container, 'mercator');
}

/**
* Forward project: longitude/latitude directly map to x/y.
* @param {number} lonDeg - Longitude in degrees
* @param {number} latDeg - Latitude in degrees
* @returns {{x: number, y: number}}
*/
_forwardProject(lonDeg, latDeg) {
return { x: lonDeg, y: latDeg };
}

/**
* Get map bounds for current FOV.
* @returns {{xMin: number, xMax: number, yMin: number, yMax: number}}
*/
_getMapBounds() {
return {
xMin: this.lonCenter - this.lonFOV,
xMax: this.lonCenter + this.lonFOV,
yMin: this.latCenter - this.latFOV,
yMax: this.latCenter + this.latFOV
};
}

/**
* Draw Mercator-specific decorations.
*/
_drawDecorations(ctx, mapToCanvas) {
// Mercator has a simple rectangular boundary — no special decoration needed
// Draw a thin border around the visible area
const w = this.canvas.width;
const h = this.canvas.height;
ctx.strokeStyle = '#2d3640';
ctx.lineWidth = 1;
ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
}
}

export default MercatorViewer;
export { MercatorViewer };
149 changes: 149 additions & 0 deletions js/pattern-editor/viewers/mollweide-viewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Mollweide (equal-area) projection viewer.
*
* Plots arena pixels using the Mollweide pseudocylindrical projection,
* matching the MATLAB PatternPreviewerApp Mollweide view.
*
* The Mollweide projection:
* 1. Solve iteratively: 2θ + sin(2θ) = π·sin(latitude)
* 2. x = (2√2/π) · longitude · cos(θ)
* 3. y = √2 · sin(θ)
*
* Output coordinates are converted to degrees for consistent axis labeling.
* The projection boundary is an ellipse.
*
* @module mollweide-viewer
*/

import { ProjectionViewer } from './projection-viewer.js';

const SQRT2 = Math.SQRT2;
const PI = Math.PI;

class MollweideViewer extends ProjectionViewer {
constructor(container) {
super(container, 'mollweide');
}

/**
* Compute the Mollweide auxiliary angle θ for a given latitude.
* Solves 2θ + sin(2θ) = π·sin(lat) using Newton-Raphson.
* Matches MATLAB computeMollweideTheta exactly.
*
* @param {number} latRad - Latitude in radians
* @returns {number} Auxiliary angle θ in radians
*/
_computeTheta(latRad) {
// Special cases at poles
if (Math.abs(latRad) >= PI / 2 - 1e-10) {
return latRad > 0 ? PI / 2 : -PI / 2;
}

let theta = latRad; // initial guess
const target = PI * Math.sin(latRad);
for (let i = 0; i < 10; i++) {
const delta =
-(2 * theta + Math.sin(2 * theta) - target) / (2 + 2 * Math.cos(2 * theta));
theta = theta + delta;
if (Math.abs(delta) < 1e-6) break;
}
return theta;
}

/**
* Forward project longitude/latitude to Mollweide map coordinates.
* Returns coordinates in degrees for consistent labeling with Mercator.
*
* @param {number} lonDeg - Longitude in degrees
* @param {number} latDeg - Latitude in degrees
* @returns {{x: number, y: number}} Map coordinates in degrees
*/
_forwardProject(lonDeg, latDeg) {
const lonRad = (lonDeg * PI) / 180;
const latRad = (latDeg * PI) / 180;

const theta = this._computeTheta(latRad);

// Mollweide formulas (in radians)
const xRad = ((2 * SQRT2) / PI) * lonRad * Math.cos(theta);
const yRad = SQRT2 * Math.sin(theta);

// Convert to degrees for axis labeling
return {
x: (xRad * 180) / PI,
y: (yRad * 180) / PI
};
}

/**
* Get map bounds for current FOV.
* Applies the Mollweide transform to the FOV limits.
*/
_getMapBounds() {
// Transform FOV limits through Mollweide
const xScale = (2 * SQRT2) / PI;
const yScale = SQRT2;

// X limit from longitude FOV
const lonRad = (this.lonFOV * PI) / 180;
const xLimRad = xScale * lonRad; // at equator, cos(theta)=1
let xLimDeg = (xLimRad * 180) / PI;

// Y limit from latitude FOV
const latRad = (this.latFOV * PI) / 180;
const thetaLim = this._computeTheta(latRad);
const yLimRad = yScale * Math.sin(thetaLim);
let yLimDeg = (yLimRad * 180) / PI;

// Safety bounds
if (xLimDeg <= 0 || !isFinite(xLimDeg)) xLimDeg = 180;
if (yLimDeg <= 0 || !isFinite(yLimDeg)) yLimDeg = 90;

return {
xMin: -xLimDeg,
xMax: xLimDeg,
yMin: -yLimDeg,
yMax: yLimDeg
};
}

/**
* Draw Mollweide-specific decorations: the elliptical boundary.
*/
_drawDecorations(ctx, mapToCanvas) {
// Draw the full-sphere ellipse outline
// The Mollweide boundary at full FOV is an ellipse:
// x = (2√2/π)·λ·cos(θ), y = √2·sin(θ)
// For the full boundary: λ = ±π, lat varies
ctx.strokeStyle = '#3d4a58';
ctx.lineWidth = 1;
ctx.beginPath();

const steps = 100;
for (let s = 0; s <= steps; s++) {
const latDeg = -90 + (180 / steps) * s;
// Right boundary (lon = +180)
const proj = this._forwardProject(180, latDeg);
if (!proj) continue;
const { cx, cy } = mapToCanvas(proj.x, proj.y);
if (s === 0) {
ctx.moveTo(cx, cy);
} else {
ctx.lineTo(cx, cy);
}
}
// Continue with left boundary (lon = -180), going back down
for (let s = steps; s >= 0; s--) {
const latDeg = -90 + (180 / steps) * s;
const proj = this._forwardProject(-180, latDeg);
if (!proj) continue;
const { cx, cy } = mapToCanvas(proj.x, proj.y);
ctx.lineTo(cx, cy);
}
ctx.closePath();
ctx.stroke();
}
}

export default MollweideViewer;
export { MollweideViewer };
Loading