UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

498 lines (452 loc) 16.4 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { register } from 'ol/proj/proj4.js'; import proj4 from 'proj4'; // @ts-expect-error no types import parseCode from 'proj4/lib/parseCode'; // @ts-expect-error no types import wktParser from 'wkt-parser'; import SRID from './SRID'; import { LinearUnit, AngularUnit, parseUnit, type Unit } from './Unit'; type ID = Record<string, number>; interface WktUnit { name: string; convert: number; AUTHORITY?: object; } interface ProjCS { type: 'PROJCS'; name: string; UNIT: WktUnit; AUTHORITY?: object; } interface VertCS { UNIT: WktUnit; } interface ProjCRS { ID: ID; } interface CompoundCS { type: 'COMPD_CS'; PROJCS: ProjCS; VERT_CS: VertCS; } function parseLinearUnit(unit: WktUnit): LinearUnit { return new LinearUnit(unit.name, unit.convert); } function parseSRID(authority: object): SRID { const [name, code] = Object.entries(authority)[0]; return new SRID(name, Number.parseInt(code)); } function getNicename(obj: object): string { if ('name' in obj && typeof obj.name === 'string') { return obj.name; } return '<unknown>'; } interface ProjCSInfos { name: string; srid?: SRID; unit: LinearUnit; } function getProjCsInfos(projCs: ProjCS): ProjCSInfos { const name = getNicename(projCs); const unit = parseLinearUnit(projCs.UNIT); if (projCs.AUTHORITY) { const authority = parseSRID(projCs.AUTHORITY); return { name, srid: authority, unit }; } return { name, unit }; } /** * Contains information about coordinate systems, as well as methods to register new coordinate systems. */ export class CoordinateSystem { /** * The EPSG:3857 / pseudo-mercator coordinate systems. */ public static readonly epsg3857 = new CoordinateSystem({ name: 'WGS 84 / Pseudo-Mercator', srid: new SRID('EPSG', 3857), horizontal: { unit: LinearUnit.meters }, vertical: { unit: LinearUnit.meters }, }); public static readonly epsg4326 = new CoordinateSystem({ name: 'WGS 84', srid: new SRID('EPSG', 4326), horizontal: { unit: AngularUnit.degrees }, vertical: { unit: LinearUnit.meters }, }); public static readonly epsg4978 = new CoordinateSystem({ name: 'WGS 84', srid: new SRID('EPSG', 4978), horizontal: { unit: LinearUnit.meters }, vertical: { unit: LinearUnit.meters }, }); public static readonly epsg4979 = new CoordinateSystem({ name: 'WGS 84', srid: new SRID('EPSG', 4979), horizontal: { unit: AngularUnit.degrees }, vertical: { unit: LinearUnit.meters }, }); /** * A special coordinate system used for spherical projections. */ public static readonly equirectangular = new CoordinateSystem({ name: 'equirectangular', horizontal: { unit: AngularUnit.degrees }, }); public static readonly unknown = new CoordinateSystem({ name: 'unknown' }); private static readonly _registry: Map<string, CoordinateSystem> = new Map([ ['EPSG:3857', CoordinateSystem.epsg3857], ['EPSG:4326', CoordinateSystem.epsg4326], ['EPSG:4978', CoordinateSystem.epsg4978], ['EPSG:4979', CoordinateSystem.epsg4979], ['equirectangular', CoordinateSystem.equirectangular], ['unknown', CoordinateSystem.unknown], ]); /** * Registers a coordinate system with the underlying proj and OpenLayers libraries. * * Note: it is recommended to provide WKT definitions instead of proj strings, since * they provide more metadata about the CRS (such as name, SRID, etc). * * Note 2: some coordinate systems definitions (such as WKT 2's `COMPOUNDCRS`) are * not supported by the underlying proj library. However, if you are not planning * to use any feature of Giro3D that requires the proj library, you may ignore * failures and warnings. * * @param id - The id of the coordinate system. * @param definition - The WKT or proj definition. * @param options - Registration options. * @example * const wkt = \` * PROJCS["RGF93 v1 / Lambert-93", * GEOGCS["RGF93 v1", * DATUM["Reseau_Geodesique_Francais_1993_v1", * SPHEROID["GRS 1980",6378137,298.257222101], * TOWGS84[0,0,0,0,0,0,0]], * PRIMEM["Greenwich",0, * AUTHORITY["EPSG","8901"]], * UNIT["degree",0.0174532925199433, * AUTHORITY["EPSG","9122"]], * AUTHORITY["EPSG","4171"]], * PROJECTION["Lambert_Conformal_Conic_2SP"], * PARAMETER["latitude_of_origin",46.5], * PARAMETER["central_meridian",3], * PARAMETER["standard_parallel_1",49], * PARAMETER["standard_parallel_2",44], * PARAMETER["false_easting",700000], * PARAMETER["false_northing",6600000], * UNIT["metre",1, * AUTHORITY["EPSG","9001"]], * AXIS["Easting",EAST], * AXIS["Northing",NORTH], * AUTHORITY["EPSG","2154"]] * \`; * * const crs = CoordinateSystem.register('EPSG:2154', wkt); * console.log(crs.name); * @returns A {@link CoordinateSystem} instance. */ public static register( /** * The ID of the coordinate system. */ id: string, /** * The WKT or proj definition. */ definition: string, options?: { /** * If true, any error that occurs when registering the * coordinate system definition with proj4.js is re-thrown. * Otherwise, a simple warning is logged instead. */ throwIfFailedToRegisterWithProj?: boolean; }, ): CoordinateSystem { if (this._registry.has(id)) { return this._registry.get(id) as CoordinateSystem; } try { this.registerCRSWithProjAndOpenLayers(id, definition); } catch (error) { // proj4.js is not able to parse all WKT definitions, especially compound CRSes. // this does not mean that the coordinate system cannot be used at all, just that // it cannot be used by proj4.js or OpenLayers. // In other words, if the Giro3D scene is purely 3D without any mapping component // that will use proj4.js, then it should be fine. if (options?.throwIfFailedToRegisterWithProj === true) { throw error; } else { console.warn(error); } } const crs = CoordinateSystem.fromWkt(definition, { id }); this._registry.set(id, crs); return crs; } /** * Mostly used for unit testing. * @internal */ public static clearRegistry(): void { this._registry.clear(); this._registry.set('EPSG:3857', CoordinateSystem.epsg3857); this._registry.set('EPSG:4326', CoordinateSystem.epsg4326); this._registry.set('EPSG:4978', CoordinateSystem.epsg4978); this._registry.set('EPSG:4979', CoordinateSystem.epsg4979); this._registry.set('equirectangular', CoordinateSystem.equirectangular); this._registry.set('unknown', CoordinateSystem.unknown); } /** * @param name - the short name, or EPSG code to identify this CRS. * @param value - the CRS definition, either in proj syntax, or in WKT syntax. */ private static registerCRSWithProjAndOpenLayers(name: string, value: string): void { if (!name || name === '') { throw new Error('missing CRS name'); } if (!value || value === '') { throw new Error('missing CRS PROJ string'); } try { // define the CRS with PROJ proj4.defs(name, value); } catch (e) { let message = ''; if (e instanceof Error) { message = ': ' + e.message; } throw new Error(`failed to register PROJ definition for ${name}${message}`); } try { // register this CRS with OpenLayers register(proj4); } catch (e) { let message = ''; if (e instanceof Error) { message = ': ' + e.message; } throw new Error(`failed to register PROJ definitions in OpenLayers${message}`); } } public static get(srid: string): CoordinateSystem { const crs = this._registry.get(srid); if (crs) { return crs; } throw new Error(`coordinate system not found: ${srid}`); } /** * Creates a {@link CoordinateSystem} from its WKT definition. * * Note: this does not register the coordinate system with proj4.js. Use {@link register} instead. * @param wkt - The WKT 1 or WKT 2 definition. * @returns The created coordinate system, or throws an error if the definition could not be parsed. */ public static fromWkt(wkt: string, overrides?: { id?: string }): CoordinateSystem { try { let parsed: ProjCRS | ProjCS | CompoundCS | object; try { // We use the wkt-parser package directly because it provides better // information, especially correct SRID, but only works for WKT. // For a proj string, we have to fallback to parseCode() parsed = wktParser(wkt); } catch { parsed = parseCode(wkt); } if ('ID' in parsed) { // WKT 2 / PROJCRS return new CoordinateSystem({ id: overrides?.id, name: getNicename(parsed), srid: parseSRID(parsed.ID), definition: wkt, }); } else if ('PROJCS' in parsed) { // WKT 1 / COMPD_CS const projCsInfos = getProjCsInfos(parsed['PROJCS']); const parameters: ConstructorParameters<typeof CoordinateSystem>[0] = { id: overrides?.id, name: projCsInfos.name, srid: projCsInfos.srid, definition: wkt, horizontal: { unit: projCsInfos.unit }, }; if ('VERT_CS' in parsed) { parameters.vertical = { unit: parseLinearUnit(parsed.VERT_CS.UNIT) }; } return new CoordinateSystem(parameters); } else if ('type' in parsed && parsed.type === 'PROJCS') { // WKT 1 / PROJCS const projCsInfos = getProjCsInfos(parsed); return new CoordinateSystem({ id: overrides?.id, name: projCsInfos.name, srid: projCsInfos.srid, definition: wkt, horizontal: { unit: projCsInfos.unit }, }); } else { let srid: SRID | undefined = undefined; let unit: Unit | undefined = undefined; if ( 'AUTHORITY' in parsed && typeof parsed.AUTHORITY === 'object' && parsed.AUTHORITY ) { srid = parseSRID(parsed.AUTHORITY); } if ('title' in parsed && typeof parsed.title === 'string') { srid = SRID.parse(parsed.title); } if ('units' in parsed && typeof parsed.units === 'string') { unit = parseUnit(parsed.units); } return new CoordinateSystem({ id: overrides?.id, name: getNicename(parsed), srid: srid, horizontal: unit != null ? { unit } : undefined, }); } } catch (error: unknown) { console.error(`Failed to parse wkt "${wkt}".`); throw error; } } private readonly _customId?: string; /** * The readable name of this coordinate system. */ public readonly name: string; /** * The SRID of this coordinate system. */ public readonly srid?: SRID; /** * Contains metadata about the horizontal component of this coordinate system. */ public readonly horizontal?: { readonly unit: Unit }; /** * Contains metadata about the vertical component of this coordinate system. */ public readonly vertical?: { readonly unit: LinearUnit }; /** * The WKT definition of this coordinate system. */ public readonly definition?: string; /** * The internal identifier of this coordinate system. Used as a key in the coordinate system registry. * By order of priority, will return: the custom identifier, the SRID, then the name. */ public get id(): string { if (typeof this._customId !== 'undefined') { return this._customId; } if (typeof this.srid !== 'undefined') { return this.srid.toString(); } return this.name; } public constructor(params: { /** * The name of the coordinate system. */ name: string; /** * The optional SRID of this coordinate system. */ srid?: SRID; /** * The id of this coordinate system. If unspecified, will use the SRID or name, if available. */ id?: string; /** * The horizontal component of the coordinate system. */ horizontal?: { unit: Unit }; /** * The vertical component of the coordinate system. */ vertical?: { unit: LinearUnit }; /** * The WKT definition of the coordinate system. */ definition?: string; }) { this.name = params.name; this.srid = params.srid; this._customId = params.id; if (typeof params.horizontal !== 'undefined') { this.horizontal = params.horizontal; } if (typeof params.vertical !== 'undefined') { this.vertical = params.vertical; } if (typeof params.definition !== 'undefined') { this.definition = params.definition; } } /** * Returns true if this coordinate system has angular units. */ public isGeographic(): boolean { const unit = this.horizontal?.unit; if (AngularUnit.isAngularUnit(unit)) { return true; } return false; } /** * Returns the conversion factor between horizontal units and meters. */ public get metersPerHorizontalUnit(): number { const unit = this.horizontal?.unit; if (LinearUnit.isLinearUnit(unit)) { return unit.metersPerUnit; } return 1; } /** * Returns the conversion factor between vertical units and meters. */ public get metersPerVerticalUnit(): number { const unit = this.vertical?.unit; if (LinearUnit.isLinearUnit(unit)) { return unit.metersPerUnit; } return this.metersPerHorizontalUnit; } public isEpsg(code: number): boolean { if (typeof this.srid !== 'undefined') { return this.srid.isEpsg(code); } return false; } /** * Returns `true` if this coordinate system is the special equirectangular coordinate system (used for spherical mapping). */ public isEquirectangular(): boolean { return this.name === 'equirectangular'; } /** * Returns `true` if this coordinate system is the special unknown coordinate system (used for non-georeferenced scenes). */ public isUnknown(): boolean { return this.name === 'unknown' && typeof this.definition === 'undefined'; } /** * Returns `true` if the two coordinate systems are equal. */ public equals(other: CoordinateSystem): boolean { return this.id === other.id; } } export default CoordinateSystem;