UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

428 lines (410 loc) 12.5 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 } from './Unit'; function parseLinearUnit(unit) { return new LinearUnit(unit.name, unit.convert); } function parseSRID(authority) { const [name, code] = Object.entries(authority)[0]; return new SRID(name, Number.parseInt(code)); } function getNicename(obj) { if ('name' in obj && typeof obj.name === 'string') { return obj.name; } return '<unknown>'; } function getProjCsInfos(projCs) { 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. */ static epsg3857 = new CoordinateSystem({ name: 'WGS 84 / Pseudo-Mercator', srid: new SRID('EPSG', 3857), horizontal: { unit: LinearUnit.meters }, vertical: { unit: LinearUnit.meters } }); static epsg4326 = new CoordinateSystem({ name: 'WGS 84', srid: new SRID('EPSG', 4326), horizontal: { unit: AngularUnit.degrees }, vertical: { unit: LinearUnit.meters } }); static epsg4978 = new CoordinateSystem({ name: 'WGS 84', srid: new SRID('EPSG', 4978), horizontal: { unit: LinearUnit.meters }, vertical: { unit: LinearUnit.meters } }); static 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. */ static equirectangular = new CoordinateSystem({ name: 'equirectangular', horizontal: { unit: AngularUnit.degrees } }); static unknown = new CoordinateSystem({ name: 'unknown' }); static _registry = 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. */ static register( /** * The ID of the coordinate system. */ id, /** * The WKT or proj definition. */ definition, options) { if (this._registry.has(id)) { return this._registry.get(id); } 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 */ static clearRegistry() { 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. */ static registerCRSWithProjAndOpenLayers(name, value) { 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}`); } } static get(srid) { 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. */ static fromWkt(wkt, overrides) { try { let parsed; 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 = { 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 = undefined; let unit = 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) { console.error(`Failed to parse wkt "${wkt}".`); throw error; } } /** * The readable name of this coordinate system. */ /** * The SRID of this coordinate system. */ /** * Contains metadata about the horizontal component of this coordinate system. */ /** * Contains metadata about the vertical component of this coordinate system. */ /** * The WKT definition of this coordinate system. */ /** * 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. */ get id() { if (typeof this._customId !== 'undefined') { return this._customId; } if (typeof this.srid !== 'undefined') { return this.srid.toString(); } return this.name; } constructor(params) { 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. */ isGeographic() { const unit = this.horizontal?.unit; if (AngularUnit.isAngularUnit(unit)) { return true; } return false; } /** * Returns the conversion factor between horizontal units and meters. */ get metersPerHorizontalUnit() { const unit = this.horizontal?.unit; if (LinearUnit.isLinearUnit(unit)) { return unit.metersPerUnit; } return 1; } /** * Returns the conversion factor between vertical units and meters. */ get metersPerVerticalUnit() { const unit = this.vertical?.unit; if (LinearUnit.isLinearUnit(unit)) { return unit.metersPerUnit; } return this.metersPerHorizontalUnit; } isEpsg(code) { 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). */ isEquirectangular() { return this.name === 'equirectangular'; } /** * Returns `true` if this coordinate system is the special unknown coordinate system (used for non-georeferenced scenes). */ isUnknown() { return this.name === 'unknown' && typeof this.definition === 'undefined'; } /** * Returns `true` if the two coordinate systems are equal. */ equals(other) { return this.id === other.id; } } export default CoordinateSystem;