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
9 changes: 7 additions & 2 deletions packages/geotiff/src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,13 @@ export type RasterArrayBase = {
*/
transform: Affine;

/** Coordinate reference system information. */
crs: number | ProjJson;
/** Coordinate reference system information.
*
* - If `crs` is a number, it is an EPSG code.
* - If `crs` is an object, it is a PROJJSON object.
* - If `crs` is a string, it is an ESRI WKT (this is rare).
*/
crs: number | ProjJson | string;

/** Nodata value from `GDAL_NODATA` TIFF tag. */
nodata: number | null;
Expand Down
39 changes: 35 additions & 4 deletions packages/geotiff/src/crs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,13 @@ const LINEAR_UNIT: Record<number, string | ProjJsonUnit> = {
* Parse a CRS from a GeoKeyDirectory.
*
* Returns the EPSG code as a number for EPSG-coded CRSes (letting the caller
* decide how to resolve it), or a PROJJSON object built from the geo keys for
* user-defined CRSes.
* decide how to resolve it), a PROJJSON object built from the geo keys for
* user-defined CRSes, or a raw WKT string when the GeoTIFF carries an
* `ESRI PE String = ...` citation under a fully user-defined model type.
*/
export function crsFromGeoKeys(gkd: GeoKeyDirectory): number | ProjJson {
export function crsFromGeoKeys(
gkd: GeoKeyDirectory,
): number | ProjJson | string {
const modelType = gkd.modelType;

if (modelType === MODEL_TYPE_PROJECTED) {
Expand All @@ -88,7 +91,35 @@ export function crsFromGeoKeys(gkd: GeoKeyDirectory): number | ProjJson {
return _geographicCrs(gkd);
}

throw new Error(`Unsupported GeoTIFF model type: ${modelType}`);
if (modelType === USER_DEFINED) {
const wkt = _esriPeString(gkd);
if (wkt !== null) {
return wkt;
}
}

throw new Error(
`Unsupported GeoTIFF CRS definition with model type: ${modelType}`,
);
}

const ESRI_PE_STRING_PREFIX = "ESRI PE String =";

/**
* Extract a WKT string from an `ESRI PE String = ...` ProjectedCitation.
*
* The GeoTIFF spec defines no text-based CRS encoding; `ESRI PE String =` is
* a de facto ArcGIS convention that GDAL/PROJ also read. It is used when the
* model type itself is user-defined (32767) and no projection method/parameter
* geo keys are present — the WKT in the citation is then the only CRS signal.
*/
function _esriPeString(gkd: GeoKeyDirectory): string | null {
const citation = gkd.projectedCitation;
if (citation === null || !citation.includes(ESRI_PE_STRING_PREFIX)) {
return null;
}
const wkt = citation.split(ESRI_PE_STRING_PREFIX, 2)[1]?.trim();
return wkt ? wkt : null;
}

function _geographicCrs(gkd: GeoKeyDirectory): number | GeographicCRS {
Expand Down
2 changes: 1 addition & 1 deletion packages/geotiff/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface HasTiffReference extends HasTransform {
readonly maskImage: TiffImage | null;

/** The coordinate reference system. */
readonly crs: number | ProjJson;
readonly crs: number | ProjJson | string;

/** The height of tiles in pixels. */
readonly tileHeight: number;
Expand Down
14 changes: 7 additions & 7 deletions packages/geotiff/src/geotiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class GeoTIFF {
readonly overviews: Overview[];

/** A cached CRS value. */
private _crs?: number | ProjJson;
private _crs?: number | ProjJson | string;

/** Cached TIFF tags that are pre-fetched when opening the GeoTIFF. */
readonly cachedTags: CachedTags;
Expand Down Expand Up @@ -361,15 +361,15 @@ export class GeoTIFF {

// ── Properties from the primary image ─────────────────────────────────

/**
* The CRS parsed from the GeoKeyDirectory.
/** Coordinate reference system information.
*
* Returns an EPSG code (number) for EPSG-coded CRSes, or a PROJJSON object
* for user-defined CRSes. The result is cached after the first access.
* - If `crs` is a number, it is an EPSG code.
* - If `crs` is an object, it is a PROJJSON object.
* - If `crs` is a string, it is an ESRI WKT (this is rare).
*
* See also {@link GeoTIFF.epsg} for the EPSG code directly from the TIFF tags.
* The result is cached after the first access.
*/
get crs(): number | ProjJson {
get crs(): number | ProjJson | string {
if (this._crs === undefined) {
this._crs = crsFromGeoKeys(this.gkd);
}
Expand Down
12 changes: 6 additions & 6 deletions packages/geotiff/src/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ export class Overview {
this.dataSource = dataSource;
}

/**
* The CRS parsed from the GeoKeyDirectory.
/** Coordinate reference system information.
*
* Returns an EPSG code (number) for EPSG-coded CRSes, or a PROJJSON object
* for user-defined CRSes. The result is cached after the first access.
* - If `crs` is a number, it is an EPSG code.
* - If `crs` is an object, it is a PROJJSON object.
* - If `crs` is a string, it is an ESRI WKT (this is rare).
*
* See also {@link GeoTIFF.epsg} for the EPSG code directly from the TIFF tags.
* The result is cached after the first access.
*/
get crs(): number | ProjJson {
get crs(): number | ProjJson | string {
return this.geotiff.crs;
}

Expand Down
8 changes: 7 additions & 1 deletion packages/geotiff/src/tile-matrix-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ interface ProjectionDefinition {

const SCREEN_PIXEL_SIZE = 0.28e-3;

function buildCrs(crs: number | ProjJson): CRS {
function buildCrs(crs: number | ProjJson | string): CRS {
if (typeof crs === "number") {
return {
uri: `http://www.opengis.net/def/crs/EPSG/0/${crs}`,
};
}

// Raw WKT string (e.g. from an ESRI PE String citation) — the OGC TMS CRS
// type allows a bare string form.
if (typeof crs === "string") {
return crs;
}

// @ts-expect-error - typing issues between different projjson definitions.
return {
wkt: crs,
Expand Down
22 changes: 22 additions & 0 deletions packages/geotiff/tests/crs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,26 @@ describe("test GeoKey CRS parsing", () => {
expect(proj.units).toBe("meter");
expect(proj.projName).toBe("Albers Equal Area");
});

it("can parse an ESRI PE String citation under a user-defined model type", async () => {
// hfp_2017_100m_v1-2_cog: GTModelTypeGeoKey = 32767 (ModelTypeUserDefined),
// CRS only reconstructable from the `ESRI PE String = ...` citation.
const geotiff = await loadGeoTIFF(
"hfp_2017_100m_v1-2_cog",
"source-coop-vizzuality",
);
const crs = geotiff.crs;

if (typeof crs !== "string") {
throw new Error("expected raw WKT string for ESRI PE String CRS");
}
expect(crs.startsWith("PROJCS[")).toBe(true);
expect(crs).toContain("Mollweide");

// wkt-parser must be able to consume the WKT for downstream TMS use.
const proj = parseWkt(crs);
expect(proj.projName).toBe("Mollweide");
expect(proj.units).toBe("meter");
expect(proj.a ?? proj.datum?.a).toBe(6378137);
});
});