Skip to content

Commit e2b011a

Browse files
feat: cleanup projections (#58)
* WIP: cleanup projection mess * improve type * some cleanup * fix: umd file name * cache bounds and center for initialized grid * cleanup * fix partials * wip cleanup * wip: better typing * minor improvements * more cleanup * prefer passing the grid in some places * put in separate directory * wip more cleanup * more cleanup * fix test * rename grid.ts to grid-points * update expose of gridpoints * trim utils/math * minor cleanup * rename function * separate files for tests * fix name * add some tests --------- Co-authored-by: Vincent van der Wal <[email protected]>
1 parent 3d4397f commit e2b011a

22 files changed

+696
-908
lines changed

src/utils/domains.ts renamed to src/domains.ts

Lines changed: 61 additions & 210 deletions
Large diffs are not rendered by default.

src/grids/factory.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { GaussianGrid } from './gaussian';
2+
import { GridInterface } from './interface';
3+
import { ProjectionGrid } from './projected';
4+
import { RegularGrid } from './regular';
5+
6+
import { DimensionRange, Domain } from '../types';
7+
8+
export class GridFactory {
9+
static create(data: Domain['grid'], ranges: DimensionRange[] | null = null): GridInterface {
10+
if (data.type === 'gaussian') {
11+
return new GaussianGrid(data, ranges);
12+
} else if (data.type === 'projected') {
13+
return new ProjectionGrid(data, ranges);
14+
} else if (data.type === 'regular') {
15+
return new RegularGrid(data, ranges);
16+
} else {
17+
throw new Error('Unsupported grid type');
18+
}
19+
}
20+
}

src/utils/gaussian.ts renamed to src/grids/gaussian.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,44 @@
1-
import { modPositive } from './math';
1+
import { modPositive } from '../utils/math';
2+
3+
import { GridInterface } from './interface';
4+
5+
import { Bounds, DimensionRange, GaussianGridData } from '../types';
26

37
/**
48
* Implementation of a Gaussian grid projection for mapping, specifically the O1280 version used by ECMWF IFS
59
*/
6-
export class GaussianGrid {
10+
export class GaussianGrid implements GridInterface {
11+
private data: GaussianGridData;
712
private readonly latitudeLines: number;
813

914
/// 1280 for O1280
10-
constructor(latitudeLines: number) {
11-
this.latitudeLines = latitudeLines;
15+
constructor(data: GaussianGridData, _ranges: DimensionRange[] | null = null) {
16+
this.data = data;
17+
this.latitudeLines = data.gaussianGridLatitudeLines;
18+
}
19+
20+
getBounds(): Bounds {
21+
// FIXME: global for now
22+
return [-180, -90, 180, 90];
23+
}
24+
25+
getCenter(): { lng: number; lat: number } {
26+
// FIXME: Center hardcoded for now
27+
return { lng: 0, lat: 0 };
28+
}
29+
30+
getCoveringRanges(
31+
_south: number,
32+
_west: number,
33+
_north: number,
34+
_east: number
35+
): DimensionRange[] {
36+
const ranges = [
37+
{ start: 0, end: this.data.ny },
38+
{ start: 0, end: this.data.nx }
39+
];
40+
41+
return ranges;
1242
}
1343

1444
/**

src/grids/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './interface';
2+
export * from './factory';

src/grids/interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Bounds, DimensionRange } from '../types';
2+
3+
export interface GridInterface {
4+
getLinearInterpolatedValue(values: Float32Array, lat: number, lon: number): number;
5+
6+
getBounds(): Bounds;
7+
getCenter(): { lng: number; lat: number };
8+
getCoveringRanges(south: number, west: number, north: number, east: number): DimensionRange[];
9+
}

src/utils/interpolations.ts renamed to src/grids/interpolations.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ export const interpolateLinear = (
99
index: number,
1010
xFraction: number,
1111
yFraction: number,
12-
ranges: DimensionRange[]
12+
nx: number
1313
): number => {
14-
const nx = ranges[1]['end'] - ranges[1]['start'];
15-
1614
const p0 = Number(values[index]);
1715
let p1 = Number(values[index + 1]);
1816
const p2 = Number(values[index + nx]);

src/grids/projected.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { GridInterface } from './interface';
2+
import { interpolateLinear } from './interpolations';
3+
import { DynamicProjection, Projection, ProjectionName, getRotatedSWNE } from './projections';
4+
5+
import { Bounds, Center, DimensionRange, ProjectedGridData } from '../types';
6+
7+
export class ProjectionGrid implements GridInterface {
8+
private projection: Projection;
9+
private nx: number;
10+
private ny: number;
11+
private origin: [x: number, y: number];
12+
private dx: number; //meters
13+
private dy: number; //meters
14+
private ranges: DimensionRange[];
15+
private bounds?: Bounds;
16+
private center?: { lng: number; lat: number };
17+
18+
constructor(data: ProjectedGridData, ranges: DimensionRange[] | null = null) {
19+
this.projection = new DynamicProjection(
20+
data.projection.name as ProjectionName,
21+
data.projection
22+
) as Projection;
23+
24+
if (!ranges) {
25+
ranges = [
26+
{ start: 0, end: data.ny },
27+
{ start: 0, end: data.nx }
28+
];
29+
}
30+
this.ranges = ranges;
31+
32+
const latitude = data.projection.latitude ?? data.latMin;
33+
const longitude = data.projection.longitude ?? data.lonMin;
34+
const projectOrigin = data.projection.projectOrigin ?? true;
35+
36+
this.nx = data.nx;
37+
this.ny = data.ny;
38+
if (latitude && Array === latitude.constructor && Array === longitude.constructor) {
39+
const sw = this.projection.forward(latitude[0], longitude[0]);
40+
const ne = this.projection.forward(latitude[1], longitude[1]);
41+
this.origin = sw;
42+
this.dx = (ne[0] - sw[0]) / this.nx;
43+
this.dy = (ne[1] - sw[1]) / this.ny;
44+
} else if (projectOrigin) {
45+
this.dx = data.dx;
46+
this.dy = data.dy;
47+
this.origin = this.projection.forward(latitude as number, longitude as number);
48+
} else {
49+
this.dx = data.dx;
50+
this.dy = data.dy;
51+
this.origin = [latitude as number, longitude as number];
52+
}
53+
}
54+
55+
findPointInterpolated(lat: number, lon: number, ranges: DimensionRange[]) {
56+
const [xPos, yPos] = this.projection.forward(lat, lon);
57+
58+
const minX = this.origin[0] + this.dx * ranges[1]['start'];
59+
const minY = this.origin[1] + this.dy * ranges[0]['start'];
60+
61+
const x = (xPos - minX) / this.dx;
62+
const y = (yPos - minY) / this.dy;
63+
64+
const xFraction = x - Math.floor(x);
65+
const yFraction = y - Math.floor(y);
66+
67+
if (
68+
x < 0 ||
69+
x >= ranges[1]['end'] - ranges[1]['start'] ||
70+
y < 0 ||
71+
y >= ranges[0]['end'] - ranges[0]['start']
72+
) {
73+
return { index: NaN, xFraction: 0, yFraction: 0 };
74+
}
75+
const index = Math.floor(y) * (ranges[1]['end'] - ranges[1]['start']) + Math.floor(x);
76+
return { index, xFraction, yFraction };
77+
}
78+
79+
getLinearInterpolatedValue(values: Float32Array, lat: number, lon: number): number {
80+
const idx = this.findPointInterpolated(lat, lon, this.ranges);
81+
return interpolateLinear(
82+
values,
83+
idx.index,
84+
idx.xFraction,
85+
idx.yFraction,
86+
this.ranges[1].end - this.ranges[1].start
87+
);
88+
}
89+
90+
getBorderPoints(): number[][] {
91+
const points = [];
92+
for (let i = 0; i < this.ny; i++) {
93+
points.push([this.origin[0], this.origin[1] + i * this.dy]);
94+
}
95+
for (let i = 0; i < this.nx; i++) {
96+
points.push([this.origin[0] + i * this.dx, this.origin[1] + this.ny * this.dy]);
97+
}
98+
for (let i = this.ny; i >= 0; i--) {
99+
points.push([this.origin[0] + this.nx * this.dx, this.origin[1] + i * this.dy]);
100+
}
101+
for (let i = this.nx; i >= 0; i--) {
102+
points.push([this.origin[0] + i * this.dx, this.origin[1]]);
103+
}
104+
return points;
105+
}
106+
107+
getBoundsFromBorderPoints(borderPoints: number[][]): Bounds {
108+
let minLon = 180;
109+
let minLat = 90;
110+
let maxLon = -180;
111+
let maxLat = -90;
112+
for (const borderPoint of borderPoints) {
113+
const borderPointLatLon = this.projection.reverse(borderPoint[0], borderPoint[1]);
114+
if (borderPointLatLon[0] < minLat) {
115+
minLat = borderPointLatLon[0];
116+
}
117+
if (borderPointLatLon[0] > maxLat) {
118+
maxLat = borderPointLatLon[0];
119+
}
120+
if (borderPointLatLon[1] < minLon) {
121+
minLon = borderPointLatLon[1];
122+
}
123+
if (borderPointLatLon[1] > maxLon) {
124+
maxLon = borderPointLatLon[1];
125+
}
126+
}
127+
return [minLon, minLat, maxLon, maxLat];
128+
}
129+
130+
getCenterFromBounds(bounds: Bounds): Center {
131+
return {
132+
lng: (bounds[2] - bounds[0]) / 2 + bounds[0],
133+
lat: (bounds[3] - bounds[1]) / 2 + bounds[1]
134+
};
135+
}
136+
137+
getBounds(): Bounds {
138+
if (!this.bounds) {
139+
const borderPoints = this.getBorderPoints();
140+
this.bounds = this.getBoundsFromBorderPoints(borderPoints);
141+
}
142+
return this.bounds;
143+
}
144+
145+
getCenter(): { lng: number; lat: number } {
146+
if (!this.center) {
147+
const bounds = this.getBounds();
148+
this.center = this.getCenterFromBounds(bounds);
149+
}
150+
return this.center;
151+
}
152+
153+
getCoveringRanges(south: number, west: number, north: number, east: number): DimensionRange[] {
154+
const dx = this.dx;
155+
const dy = this.dy;
156+
const nx = this.nx;
157+
const ny = this.ny;
158+
159+
let xPrecision, yPrecision;
160+
if (String(dx).split('.')[1]) {
161+
xPrecision = String(dx).split('.')[1].length;
162+
yPrecision = String(dy).split('.')[1].length;
163+
} else {
164+
xPrecision = 2;
165+
yPrecision = 2;
166+
}
167+
168+
let [s, w, n, e] = getRotatedSWNE(this.projection, [south, west, north, east]);
169+
170+
// round to nearest grid point + / - 1
171+
s = Number((s - (s % dy)).toFixed(yPrecision));
172+
w = Number((w - (w % dx)).toFixed(xPrecision));
173+
n = Number((n - (n % dy) + dy).toFixed(yPrecision));
174+
e = Number((e - (e % dx) + dx).toFixed(xPrecision));
175+
176+
const originX = this.origin[0];
177+
const originY = this.origin[1];
178+
179+
let minX: number, minY: number, maxX: number, maxY: number;
180+
181+
if (dx > 0) {
182+
minX = Math.min(Math.max(Math.floor((w - originX) / dx - 1), 0), nx);
183+
maxX = Math.max(Math.min(Math.ceil((e - originX) / dx + 1), nx), 0);
184+
} else {
185+
minX = Math.min(Math.max(Math.floor((e - originX) / dx - 1), 0), nx);
186+
maxX = Math.max(Math.min(Math.ceil((w - originX) / dx + 1), nx), 0);
187+
}
188+
189+
if (dy > 0) {
190+
minY = Math.min(Math.max(Math.floor((s - originY) / dy - 1), 0), ny);
191+
maxY = Math.max(Math.min(Math.ceil((n - originY) / dy + 1), ny), 0);
192+
} else {
193+
minY = Math.min(Math.max(Math.floor((n - originY) / dy - 1), 0), ny);
194+
maxY = Math.max(Math.min(Math.ceil((s - originY) / dy + 1), ny), 0);
195+
}
196+
const ranges = [
197+
{ start: minY, end: maxY },
198+
{ start: minX, end: maxX }
199+
];
200+
return ranges;
201+
}
202+
}

0 commit comments

Comments
 (0)