Skip to content

Commit d210757

Browse files
authored
feat: add GeoTIFF coordinate transformation node (#184)
Add GeoTiffTransformNode to transform GeoTIFF metadata from WGS84 (EPSG:4326) to Web Mercator (EPSG:3857) projection. - Validates CRS is EPSG:4326 before transformation - Transforms bounds array from [west, south, east, north] degrees to [minX, minY, maxX, maxY] meters - Sets output CRS to EPSG:3857
1 parent 4b8c1b8 commit d210757

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { NodeExecution, NodeType } from "@dafthunk/types";
2+
3+
import { ExecutableNode, NodeContext } from "../types";
4+
5+
// Import projection utilities from 3dtiles package
6+
// These will need to be available in the API context
7+
// You may need to adjust the import path based on your workspace setup
8+
9+
export class GeoTiffTransformNode extends ExecutableNode {
10+
public static readonly nodeType: NodeType = {
11+
id: "geotiff-transform",
12+
name: "GeoTIFF Transform to Web Mercator",
13+
type: "geotiff-transform",
14+
description:
15+
"Transform GeoTIFF metadata from WGS84 (EPSG:4326) to Web Mercator (EPSG:3857)",
16+
tags: ["3D", "Geo"],
17+
icon: "map",
18+
inputs: [
19+
{
20+
name: "metadata",
21+
type: "json",
22+
description: "GeoTIFF metadata from GeoTIFF Metadata Reader",
23+
required: true,
24+
},
25+
],
26+
outputs: [
27+
{
28+
name: "transformed",
29+
type: "json",
30+
description: "Metadata with Web Mercator bounds",
31+
},
32+
],
33+
};
34+
35+
public async execute(context: NodeContext): Promise<NodeExecution> {
36+
try {
37+
const { metadata } = context.inputs;
38+
39+
// Validate CRS is EPSG:4326 before transformation
40+
if (metadata.crs && metadata.crs !== "EPSG:4326") {
41+
return this.createErrorResult(
42+
`Cannot transform: expected EPSG:4326, but metadata has CRS: ${metadata.crs}`
43+
);
44+
}
45+
46+
// Extract bounds from metadata
47+
const [west, south, east, north] = metadata.bounds;
48+
49+
// Transform WGS84 bounds to Web Mercator
50+
const [minX, minY] = this.WGS84toEPSG3857(west, south);
51+
const [maxX, maxY] = this.WGS84toEPSG3857(east, north);
52+
53+
// Create transformed metadata with Web Mercator bounds
54+
const transformed = {
55+
...metadata,
56+
bounds: [minX, minY, maxX, maxY],
57+
crs: "EPSG:3857",
58+
};
59+
60+
return this.createSuccessResult({ transformed });
61+
} catch (error) {
62+
const errorMessage =
63+
error instanceof Error ? error.message : "Unknown error";
64+
return this.createErrorResult(
65+
`Failed to transform GeoTIFF metadata: ${errorMessage}`
66+
);
67+
}
68+
}
69+
70+
/**
71+
* Convert WGS84 geographic coordinates to Web Mercator coordinates
72+
*
73+
* Based on proj4 EPSG:3857 transformation.
74+
* Web Mercator is widely used by web mapping services.
75+
*
76+
* @param lon - WGS84 longitude in degrees (-180 to +180)
77+
* @param lat - WGS84 latitude in degrees (-85.0511 to +85.0511)
78+
* @returns Web Mercator coordinates [x, y] in meters
79+
*/
80+
private WGS84toEPSG3857(lon: number, lat: number): [number, number] {
81+
// Web Mercator constants
82+
const EARTH_RADIUS = 6378137; // WGS84 semi-major axis in meters
83+
const MAX_LATITUDE = 85.0511287798; // Max latitude for Web Mercator
84+
85+
// Clamp latitude to valid Web Mercator range
86+
const clampedLat = Math.max(-MAX_LATITUDE, Math.min(MAX_LATITUDE, lat));
87+
88+
// Convert to radians
89+
const lonRad = (lon * Math.PI) / 180;
90+
const latRad = (clampedLat * Math.PI) / 180;
91+
92+
// Web Mercator formulas
93+
const x = EARTH_RADIUS * lonRad;
94+
const y = EARTH_RADIUS * Math.log(Math.tan(Math.PI / 4 + latRad / 2));
95+
96+
return [x, y];
97+
}
98+
}

apps/api/src/nodes/cloudflare-node-registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DemToBufferGeometryNode } from "./3d/dem-to-buffergeometry-node";
33
import { GeoTiffDemQueryNode } from "./3d/geotiff-dem-query-node";
44
import { GeoTiffMetadataReaderNode } from "./3d/geotiff-metadata-reader-node";
55
import { GeoTiffQueryNode } from "./3d/geotiff-query-node";
6+
import { GeoTiffTransformNode } from "./3d/geotiff-transform-node";
67
import { Claude3OpusNode } from "./anthropic/claude-3-opus-node";
78
import { Claude35HaikuNode } from "./anthropic/claude-35-haiku-node";
89
import { Claude35SonnetNode } from "./anthropic/claude-35-sonnet-node";
@@ -480,6 +481,7 @@ export class CloudflareNodeRegistry extends BaseNodeRegistry {
480481
this.registerImplementation(GeoTiffDemQueryNode);
481482
this.registerImplementation(GeoTiffMetadataReaderNode);
482483
this.registerImplementation(GeoTiffQueryNode);
484+
this.registerImplementation(GeoTiffTransformNode);
483485
}
484486

485487
// Geo nodes

0 commit comments

Comments
 (0)