From 551dfd3af6245647e360b92a55d1f32ffde4ebe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20M=C3=BCller?= Date: Wed, 21 Jan 2026 17:09:35 +0100 Subject: [PATCH] WKT2: Complete parsing/model/serialization implemented New object-oriented WKT2 CRS model introduced (including for GEOGCRS, PROJCRS, VERTCRS, COMPOUNDCRS, BOUNDCRS, ENGCRS, PARAMETRICCRS) with all components. The WKT2 reader has been fundamentally implemented and now supports parsing into the new model as well as conversion to/from ProjNet objects. A new writer enables serialization back to WKT2. Extensive unit tests for parsing, roundtrip, and model structure added. This enables complete, roundtrip-capable WKT2 parsing and serialization in ProjNet for the first time. References #83 and #86 --- .../Projections/ProjectionsRegistry.cs | 260 ++-- .../Wkt2/Wkt2AbridgedTransformation.cs | 48 + .../CoordinateSystems/Wkt2/Wkt2Axis.cs | 47 + .../CoordinateSystems/Wkt2/Wkt2BoundCrs.cs | 56 + .../CoordinateSystems/Wkt2/Wkt2CompoundCrs.cs | 43 + .../CoordinateSystems/Wkt2/Wkt2Conversion.cs | 48 + .../CoordinateSystems/Wkt2/Wkt2Conversions.cs | 224 +++ .../Wkt2/Wkt2CoordinateSystem.cs | 48 + .../CoordinateSystems/Wkt2/Wkt2CrsBase.cs | 40 + .../CoordinateSystems/Wkt2/Wkt2Ellipsoid.cs | 49 + .../CoordinateSystems/Wkt2/Wkt2EngCrs.cs | 49 + .../Wkt2/Wkt2EngineeringDatum.cs | 42 + .../Wkt2/Wkt2GeodeticDatum.cs | 44 + .../CoordinateSystems/Wkt2/Wkt2GeogCrs.cs | 54 + src/ProjNet/CoordinateSystems/Wkt2/Wkt2Id.cs | 42 + .../CoordinateSystems/Wkt2/Wkt2Parameter.cs | 37 + .../Wkt2/Wkt2ParametricCrs.cs | 49 + .../Wkt2/Wkt2ParametricDatum.cs | 42 + .../Wkt2/Wkt2PrimeMeridian.cs | 42 + .../CoordinateSystems/Wkt2/Wkt2ProjCrs.cs | 56 + .../CoordinateSystems/Wkt2/Wkt2Unit.cs | 44 + .../CoordinateSystems/Wkt2/Wkt2VertCrs.cs | 49 + .../Wkt2/Wkt2VerticalDatum.cs | 42 + .../CoordinateSystemWkt2Reader.cs | 1254 +++++++++++++++++ .../CoordinateSystemWkt2Writer.cs | 608 ++++++++ .../CoordinateSystemWktReader.cs | 19 + .../ProjNet.Tests/CoordinateTransformTests.cs | 17 + test/ProjNet.Tests/WKT/WKT2BoundCrsTests.cs | 62 + .../ProjNet.Tests/WKT/WKT2CompoundCrsTests.cs | 51 + test/ProjNet.Tests/WKT/WKT2EngCrsTests.cs | 43 + .../WKT/WKT2Nad83NewJerseyFtUsTests.cs | 67 + .../WKT/WKT2ParametricCrsTests.cs | 43 + test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs | 80 ++ .../WKT/WKT2Rgf93Lambert93Tests.cs | 70 + test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs | 79 ++ test/ProjNet.Tests/WKT/WKT2VertCrsTests.cs | 40 + 36 files changed, 3758 insertions(+), 130 deletions(-) create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2AbridgedTransformation.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Axis.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2BoundCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2CompoundCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversion.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversions.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateSystem.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2CrsBase.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Ellipsoid.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngineeringDatum.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeodeticDatum.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeogCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Id.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Parameter.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricDatum.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2PrimeMeridian.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2ProjCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2Unit.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2VertCrs.cs create mode 100644 src/ProjNet/CoordinateSystems/Wkt2/Wkt2VerticalDatum.cs create mode 100644 src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Reader.cs create mode 100644 src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2BoundCrsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2CompoundCrsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2EngCrsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2Nad83NewJerseyFtUsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2ParametricCrsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs create mode 100644 test/ProjNet.Tests/WKT/WKT2VertCrsTests.cs diff --git a/src/ProjNet/CoordinateSystems/Projections/ProjectionsRegistry.cs b/src/ProjNet/CoordinateSystems/Projections/ProjectionsRegistry.cs index 7ca6186..d3e315e 100644 --- a/src/ProjNet/CoordinateSystems/Projections/ProjectionsRegistry.cs +++ b/src/ProjNet/CoordinateSystems/Projections/ProjectionsRegistry.cs @@ -1,35 +1,35 @@ -using ProjNet.CoordinateSystems.Transformations; -using System; -using System.Collections.Generic; - -namespace ProjNet.CoordinateSystems.Projections -{ - /// - /// Registry class for all known s. - /// - public class ProjectionsRegistry - { - private static readonly Dictionary TypeRegistry = new Dictionary(); - private static readonly Dictionary ConstructorRegistry = new Dictionary(); - - private static readonly object RegistryLock = new object(); - - /// - /// Static constructor - /// - static ProjectionsRegistry() - { - Register("mercator", typeof(Mercator)); - Register("mercator_1sp", typeof(Mercator)); +using ProjNet.CoordinateSystems.Transformations; +using System; +using System.Collections.Generic; + +namespace ProjNet.CoordinateSystems.Projections +{ + /// + /// Registry class for all known s. + /// + public class ProjectionsRegistry + { + private static readonly Dictionary TypeRegistry = new Dictionary(); + private static readonly Dictionary ConstructorRegistry = new Dictionary(); + + private static readonly object RegistryLock = new object(); + + /// + /// Static constructor + /// + static ProjectionsRegistry() + { + Register("mercator", typeof(Mercator)); + Register("mercator_1sp", typeof(Mercator)); Register("mercator_2sp", typeof(Mercator)); Register("mercator_auxiliary_sphere", typeof(MercatorAuxiliarySphere)); - Register("pseudo_mercator", typeof(PseudoMercator)); - Register("popular_visualisation_pseudo_mercator", typeof(PseudoMercator)); + Register("pseudo_mercator", typeof(PseudoMercator)); + Register("popular_visualisation_pseudo_mercator", typeof(PseudoMercator)); Register("google_mercator", typeof(PseudoMercator)); - Register("transverse_mercator", typeof(TransverseMercator)); - Register("gauss_kruger", typeof(TransverseMercator)); - + Register("transverse_mercator", typeof(TransverseMercator)); + Register("gauss_kruger", typeof(TransverseMercator)); + Register("albers", typeof(AlbersProjection)); Register("albers_conic_equal_area", typeof(AlbersProjection)); @@ -39,66 +39,66 @@ static ProjectionsRegistry() Register("lambert_conformal_conic", typeof(LambertConformalConic2SP)); Register("lambert_conformal_conic_2sp", typeof(LambertConformalConic2SP)); - Register("lambert_conic_conformal_(2sp)", typeof(LambertConformalConic2SP)); - Register("lambert_tangential_conformal_conic_projection", typeof(LambertConformalConic2SP)); - - Register("lambert_azimuthal_equal_area", typeof(LambertAzimuthalEqualAreaProjection)); - - Register("cassini_soldner", typeof(CassiniSoldnerProjection)); - Register("hotine_oblique_mercator", typeof(HotineObliqueMercatorProjection)); - Register("hotine_oblique_mercator_azimuth_center", typeof(HotineObliqueMercatorProjection)); - Register("oblique_mercator", typeof(ObliqueMercatorProjection)); - Register("oblique_stereographic", typeof(ObliqueStereographicProjection)); - Register("orthographic", typeof(OrthographicProjection)); - Register("polar_stereographic", typeof(PolarStereographicProjection)); - } - - /// - /// Method to register a new Map - /// - /// - /// - public static void Register(string name, Type type) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name)); - - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (!typeof(MathTransform).IsAssignableFrom(type)) - throw new ArgumentException("The provided type does not implement 'GeoAPI.CoordinateSystems.Transformations.IMathTransform'!", nameof(type)); - - var ci = CheckConstructor(type); - if (ci == null) - throw new ArgumentException("The provided type is lacking a suitable constructor", nameof(type)); - - string key = ProjectionNameToRegistryKey(name); - lock (RegistryLock) - { - if (TypeRegistry.ContainsKey(key)) - { - var rt = TypeRegistry[key]; - if (ReferenceEquals(type, rt)) - return; - throw new ArgumentException("A different projection type has been registered with this name", "name"); - } - - TypeRegistry.Add(key, type); - ConstructorRegistry.Add(key, ci); - } - } - + Register("lambert_conic_conformal_(2sp)", typeof(LambertConformalConic2SP)); + Register("lambert_tangential_conformal_conic_projection", typeof(LambertConformalConic2SP)); + + Register("lambert_azimuthal_equal_area", typeof(LambertAzimuthalEqualAreaProjection)); + + Register("cassini_soldner", typeof(CassiniSoldnerProjection)); + Register("hotine_oblique_mercator", typeof(HotineObliqueMercatorProjection)); + Register("hotine_oblique_mercator_azimuth_center", typeof(HotineObliqueMercatorProjection)); + Register("oblique_mercator", typeof(ObliqueMercatorProjection)); + Register("oblique_stereographic", typeof(ObliqueStereographicProjection)); + Register("orthographic", typeof(OrthographicProjection)); + Register("polar_stereographic", typeof(PolarStereographicProjection)); + } + + /// + /// Method to register a new Map + /// + /// + /// + public static void Register(string name, Type type) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (!typeof(MathTransform).IsAssignableFrom(type)) + throw new ArgumentException("The provided type does not implement 'GeoAPI.CoordinateSystems.Transformations.IMathTransform'!", nameof(type)); + + var ci = CheckConstructor(type); + if (ci == null) + throw new ArgumentException("The provided type is lacking a suitable constructor", nameof(type)); + + string key = ProjectionNameToRegistryKey(name); + lock (RegistryLock) + { + if (TypeRegistry.ContainsKey(key)) + { + var rt = TypeRegistry[key]; + if (ReferenceEquals(type, rt)) + return; + throw new ArgumentException("A different projection type has been registered with this name", "name"); + } + + TypeRegistry.Add(key, type); + ConstructorRegistry.Add(key, ci); + } + } + private static string ProjectionNameToRegistryKey(string name) { return name.ToLowerInvariant().Replace(' ', '_').Replace("-", "_"); - } - + } + /// /// Register an alias for an existing Map. /// /// - /// + /// public static void RegisterAlias(string aliasName, string existingName) { lock (RegistryLock) @@ -110,52 +110,52 @@ public static void RegisterAlias(string aliasName, string existingName) Register(aliasName, existingProjectionType); } - } - - private static Type CheckConstructor(Type type) - { - // find a constructor that accepts exactly one parameter that's an - // instance of List, and then return the exact - // parameter type so that we can create instances of this type with - // minimal copying in the future, when possible. - foreach (var c in type.GetConstructors()) - { - var parameters = c.GetParameters(); - if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableFrom(typeof(List))) - { - return parameters[0].ParameterType; - } - } - - return null; - } - - internal static MathTransform CreateProjection(string className, IEnumerable parameters) - { - string key = ProjectionNameToRegistryKey(className); - - Type projectionType; - Type ci; - - lock (RegistryLock) - { - if (!TypeRegistry.TryGetValue(key, out projectionType)) - throw new NotSupportedException($"Projection {className} is not supported."); - ci = ConstructorRegistry[key]; - } - - if (!ci.IsInstanceOfType(parameters)) - { - parameters = new List(parameters); - } - - var res = (MapProjection)Activator.CreateInstance(projectionType, parameters); - if (!res.Name.Equals(className, StringComparison.InvariantCultureIgnoreCase)) - { - res.Alias = res.Name; - res.Name = className; - } - return res; - } - } -} + } + + private static Type CheckConstructor(Type type) + { + // find a constructor that accepts exactly one parameter that's an + // instance of List, and then return the exact + // parameter type so that we can create instances of this type with + // minimal copying in the future, when possible. + foreach (var c in type.GetConstructors()) + { + var parameters = c.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableFrom(typeof(List))) + { + return parameters[0].ParameterType; + } + } + + return null; + } + + internal static MathTransform CreateProjection(string className, IEnumerable parameters) + { + string key = ProjectionNameToRegistryKey(className); + + Type projectionType; + Type ci; + + lock (RegistryLock) + { + if (!TypeRegistry.TryGetValue(key, out projectionType)) + throw new NotSupportedException($"Projection {className} is not supported."); + ci = ConstructorRegistry[key]; + } + + if (!ci.IsInstanceOfType(parameters)) + { + parameters = new List(parameters); + } + + var res = (MapProjection)Activator.CreateInstance(projectionType, parameters); + if (!res.Name.Equals(className, StringComparison.InvariantCultureIgnoreCase)) + { + res.Alias = res.Name; + res.Name = className; + } + return res; + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2AbridgedTransformation.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2AbridgedTransformation.cs new file mode 100644 index 0000000..6f0c5b9 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2AbridgedTransformation.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Abridged transformation as used in WKT2 BOUNDCRS. + /// + [Serializable] + public sealed class Wkt2AbridgedTransformation + { + /// + /// Initializes a new instance. + /// + /// Transformation name. + /// Transformation method name. + public Wkt2AbridgedTransformation(string name, string methodName) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + } + + /// + /// Gets the transformation name. + /// + public string Name { get; } + + /// + /// Gets the transformation method name. + /// + public string MethodName { get; } + + /// + /// Gets the transformation parameters. + /// + public List Parameters { get; } = new List(); + + /// + /// Gets or sets the transformation identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Axis.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Axis.cs new file mode 100644 index 0000000..8fd7c75 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Axis.cs @@ -0,0 +1,47 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Axis element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2Axis + { + /// + /// Initializes a new instance. + /// + /// Axis name. + /// Axis direction (e.g. north, east). + public Wkt2Axis(string name, string direction) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Direction = direction ?? throw new ArgumentNullException(nameof(direction)); + } + + /// + /// Gets the axis name. + /// + public string Name { get; } + + /// + /// Gets the axis direction. + /// + public string Direction { get; } + + /// + /// Gets or sets the axis order. + /// + public int? Order { get; set; } + + /// + /// Gets or sets an optional axis unit. + /// + public Wkt2Unit Unit { get; set; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2BoundCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2BoundCrs.cs new file mode 100644 index 0000000..c2ca554 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2BoundCrs.cs @@ -0,0 +1,56 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Bound CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2BoundCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. BOUNDCRS). + /// CRS name. + /// Source CRS. + /// Target CRS. + /// Abridged transformation description. + public Wkt2BoundCrs(string keyword, string name, Wkt2CrsBase sourceCrs, Wkt2CrsBase targetCrs, Wkt2AbridgedTransformation transformation) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + SourceCrs = sourceCrs ?? throw new ArgumentNullException(nameof(sourceCrs)); + TargetCrs = targetCrs ?? throw new ArgumentNullException(nameof(targetCrs)); + Transformation = transformation ?? throw new ArgumentNullException(nameof(transformation)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the source CRS. + /// + public Wkt2CrsBase SourceCrs { get; } + + /// + /// Gets the target CRS. + /// + public Wkt2CrsBase TargetCrs { get; } + + /// + /// Gets the abridged transformation. + /// + public Wkt2AbridgedTransformation Transformation { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CompoundCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CompoundCrs.cs new file mode 100644 index 0000000..51b1cd6 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CompoundCrs.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Compound CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2CompoundCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. COMPOUNDCRS). + /// CRS name. + /// Component CRS list. + public Wkt2CompoundCrs(string keyword, string name, IEnumerable components) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Components = new List(components ?? throw new ArgumentNullException(nameof(components))); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the component CRSs. + /// + public List Components { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversion.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversion.cs new file mode 100644 index 0000000..653f016 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversion.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Conversion element as used in WKT2 projected CRSs. + /// + [Serializable] + public sealed class Wkt2Conversion + { + /// + /// Initializes a new instance. + /// + /// Conversion name. + /// Method name. + public Wkt2Conversion(string name, string methodName) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + MethodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); + } + + /// + /// Gets the conversion name. + /// + public string Name { get; } + + /// + /// Gets the conversion method name. + /// + public string MethodName { get; } + + /// + /// Gets the conversion parameters. + /// + public List Parameters { get; } = new List(); + + /// + /// Gets or sets the conversion identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversions.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversions.cs new file mode 100644 index 0000000..40841ad --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversions.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using ProjNet.CoordinateSystems; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Conversion helpers between the WKT2 model types and the existing ProjNet coordinate system model. + /// + public static class Wkt2Conversions + { + /// + /// Converts a WKT2 projected CRS model to a ProjNet . + /// + /// The WKT2 projected CRS. + /// A ProjNet projected coordinate system. + public static ProjectedCoordinateSystem ToProjNetProjectedCoordinateSystem(this Wkt2ProjCrs crs) + { + if (crs == null) throw new ArgumentNullException(nameof(crs)); + + var baseGcs = crs.BaseCrs.ToProjNetGeographicCoordinateSystem(); + + // Projection method mapping is best-effort; WKT2 method names vary. + string method = MapProjectionMethodName(crs.Conversion.MethodName); + + var parameters = new List(); + foreach (var p in crs.Conversion.Parameters) + parameters.Add(new ProjectionParameter(p.Name, p.Value)); + + var projection = new Projection(method, parameters, crs.Conversion.Name, string.Empty, -1, string.Empty, string.Empty, string.Empty); + + var linearUnit = LinearUnit.Metre; + if (crs.CoordinateSystem != null && crs.CoordinateSystem.Unit != null) + { + // ProjNet `LinearUnit` expects meters per unit. + // WKT2 LENGTHUNIT factor is in meters per unit. + linearUnit = new LinearUnit(crs.CoordinateSystem.Unit.ConversionFactor, crs.CoordinateSystem.Unit.Name, string.Empty, -1, string.Empty, string.Empty, string.Empty); + } + + var axes = new List(2) + { + new AxisInfo("East", AxisOrientationEnum.East), + new AxisInfo("North", AxisOrientationEnum.North) + }; + + // Best-effort for IDs + string authority = crs.Id != null ? crs.Id.Authority : string.Empty; + long authorityCode = -1; + if (crs.Id != null) + long.TryParse(crs.Id.Code, out authorityCode); + + return new ProjectedCoordinateSystem(baseGcs.HorizontalDatum, baseGcs, linearUnit, projection, axes, + crs.Name, authority, authorityCode, string.Empty, string.Empty, string.Empty); + } + + /// + /// Converts a ProjNet to a WKT2 projected CRS model. + /// + /// The ProjNet projected coordinate system. + /// A WKT2 projected CRS model. + public static Wkt2ProjCrs FromProjNetProjectedCoordinateSystem(this ProjectedCoordinateSystem pcs) + { + if (pcs == null) throw new ArgumentNullException(nameof(pcs)); + + var baseCrs = pcs.GeographicCoordinateSystem.FromProjNetGeographicCoordinateSystem(); + + var conversion = new Wkt2Conversion(pcs.Projection.Name, pcs.Projection.ClassName); + for (int i = 0; i < pcs.Projection.NumParameters; i++) + { + var p = pcs.Projection.GetParameter(i); + conversion.Parameters.Add(new Wkt2Parameter(p.Name, p.Value)); + } + + var unit = new Wkt2Unit("LENGTHUNIT", pcs.LinearUnit.Name, pcs.LinearUnit.MetersPerUnit); + var cs = new Wkt2CoordinateSystem("cartesian", 2) { Unit = unit }; + cs.Axes.Add(new Wkt2Axis("easting", "east") { Order = 1 }); + cs.Axes.Add(new Wkt2Axis("northing", "north") { Order = 2 }); + + var crs = new Wkt2ProjCrs("PROJCRS", pcs.Name, baseCrs, conversion, cs); + if (!string.IsNullOrWhiteSpace(pcs.Authority) && pcs.AuthorityCode > 0) + crs.Id = new Wkt2Id(pcs.Authority, pcs.AuthorityCode.ToString()); + + return crs; + } + + /// + /// Converts a WKT2 geographic CRS model to a ProjNet . + /// + /// The WKT2 geographic CRS. + /// A ProjNet geographic coordinate system. + public static GeographicCoordinateSystem ToProjNetGeographicCoordinateSystem(this Wkt2GeogCrs crs) + { + if (crs == null) throw new ArgumentNullException(nameof(crs)); + + // Normalize WKT2 -> ProjNet conventions: + // - ProjNet GCS is horizontal (2D) + // - ProjNet expects Lon/East then Lat/North axis order + // - Per-axis units are not supported; use CS unit (ANGLEUNIT) if available + + var ellipsoid = new Ellipsoid( + crs.Datum.Ellipsoid.SemiMajorAxis, + 0.0, + crs.Datum.Ellipsoid.InverseFlattening, + true, + LinearUnit.Metre, + crs.Datum.Ellipsoid.Name, + string.Empty, + -1, + string.Empty, + string.Empty, + string.Empty); + + var datum = new HorizontalDatum( + ellipsoid, + null, + DatumType.HD_Geocentric, + crs.Datum.Name, + string.Empty, + -1, + string.Empty, + string.Empty, + string.Empty); + + var angUnit = AngularUnit.Degrees; + if (crs.CoordinateSystem != null && crs.CoordinateSystem.Unit != null) + { + angUnit = new AngularUnit( + crs.CoordinateSystem.Unit.ConversionFactor, + crs.CoordinateSystem.Unit.Name, + string.Empty, + -1, + string.Empty, + string.Empty, + string.Empty); + } + + PrimeMeridian pm; + if (crs.PrimeMeridian != null) + { + pm = new PrimeMeridian(crs.PrimeMeridian.Longitude, angUnit, crs.PrimeMeridian.Name, string.Empty, -1, string.Empty, string.Empty, string.Empty); + } + else + { + pm = new PrimeMeridian(0.0, angUnit, "Greenwich", string.Empty, -1, string.Empty, string.Empty, string.Empty); + } + + var axes = new List(2) + { + new AxisInfo("Lon", AxisOrientationEnum.East), + new AxisInfo("Lat", AxisOrientationEnum.North) + }; + + // Best-effort for IDs + string authority = crs.Id != null ? crs.Id.Authority : string.Empty; + long authorityCode = -1; + if (crs.Id != null) + long.TryParse(crs.Id.Code, out authorityCode); + + return new GeographicCoordinateSystem( + angUnit, + datum, + pm, + axes, + crs.Name, + authority, + authorityCode, + string.Empty, + string.Empty, + string.Empty); + } + + /// + /// Converts a ProjNet to a WKT2 geographic CRS model. + /// + /// The ProjNet geographic coordinate system. + /// A WKT2 geographic CRS model. + public static Wkt2GeogCrs FromProjNetGeographicCoordinateSystem(this GeographicCoordinateSystem gcs) + { + if (gcs == null) throw new ArgumentNullException(nameof(gcs)); + + var unit = new Wkt2Unit("ANGLEUNIT", gcs.AngularUnit.Name, gcs.AngularUnit.RadiansPerUnit); + + var cs = new Wkt2CoordinateSystem("ellipsoidal", 2) + { + Unit = unit + }; + cs.Axes.Add(new Wkt2Axis("longitude", "east") { Order = 1 }); + cs.Axes.Add(new Wkt2Axis("latitude", "north") { Order = 2 }); + + var ellipsoid = new Wkt2Ellipsoid(gcs.HorizontalDatum.Ellipsoid.Name, gcs.HorizontalDatum.Ellipsoid.SemiMajorAxis, gcs.HorizontalDatum.Ellipsoid.InverseFlattening) + { + LengthUnit = new Wkt2Unit("LENGTHUNIT", "metre", 1.0) + }; + + var datum = new Wkt2GeodeticDatum("DATUM", gcs.HorizontalDatum.Name, ellipsoid); + + var crs = new Wkt2GeogCrs("GEOGCRS", gcs.Name, datum, cs) + { + PrimeMeridian = new Wkt2PrimeMeridian(gcs.PrimeMeridian.Name, gcs.PrimeMeridian.Longitude) { AngleUnit = unit } + }; + + if (!string.IsNullOrWhiteSpace(gcs.Authority) && gcs.AuthorityCode > 0) + crs.Id = new Wkt2Id(gcs.Authority, gcs.AuthorityCode.ToString()); + + return crs; + } + + private static string MapProjectionMethodName(string wkt2Method) + { + if (string.IsNullOrWhiteSpace(wkt2Method)) + return ""; + + string m = wkt2Method.Trim(); + + // Most common mappings for EPSG exports. + if (m.Equals("Transverse Mercator", StringComparison.OrdinalIgnoreCase)) return "Transverse_Mercator"; + if (m.Equals("Mercator", StringComparison.OrdinalIgnoreCase)) return "Mercator_1SP"; + if (m.Equals("Lambert Conic Conformal (2SP)", StringComparison.OrdinalIgnoreCase)) return "lambert_conformal_conic_2sp"; + + // Fallback: keep original. + return m; + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateSystem.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateSystem.cs new file mode 100644 index 0000000..64b3d7e --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateSystem.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Coordinate system element as used in WKT2 (CS plus axes and units). + /// + [Serializable] + public sealed class Wkt2CoordinateSystem + { + /// + /// Initializes a new instance. + /// + /// Coordinate system type (e.g. ellipsoidal, cartesian). + /// Number of dimensions. + public Wkt2CoordinateSystem(string type, int dimension) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + Dimension = dimension; + } + + /// + /// Gets the coordinate system type. + /// + public string Type { get; } + + /// + /// Gets the dimension. + /// + public int Dimension { get; } + + /// + /// Gets the axes. + /// + public List Axes { get; } = new List(); + + /// + /// Gets or sets the coordinate system unit. + /// + public Wkt2Unit Unit { get; set; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CrsBase.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CrsBase.cs new file mode 100644 index 0000000..b900790 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CrsBase.cs @@ -0,0 +1,40 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Base class for WKT2 CRS model objects. + /// + [Serializable] + public abstract class Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS name. + protected Wkt2CrsBase(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the CRS name. + /// + public string Name { get; } + + /// + /// Gets or sets the CRS identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + + /// + /// Serializes the model back to a WKT2 string. + /// + public abstract string ToWkt2String(); + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Ellipsoid.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Ellipsoid.cs new file mode 100644 index 0000000..7952dc0 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Ellipsoid.cs @@ -0,0 +1,49 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Ellipsoid element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2Ellipsoid + { + /// + /// Initializes a new instance. + /// + /// Ellipsoid name. + /// Semi-major axis length. + /// Inverse flattening. + public Wkt2Ellipsoid(string name, double semiMajorAxis, double inverseFlattening) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + SemiMajorAxis = semiMajorAxis; + InverseFlattening = inverseFlattening; + } + + /// + /// Gets the ellipsoid name. + /// + public string Name { get; } + + /// + /// Gets the semi-major axis. + /// + public double SemiMajorAxis { get; } + + /// + /// Gets the inverse flattening. + /// + public double InverseFlattening { get; } + + /// + /// Gets or sets an optional length unit. + /// + public Wkt2Unit LengthUnit { get; set; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngCrs.cs new file mode 100644 index 0000000..483bb73 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngCrs.cs @@ -0,0 +1,49 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Engineering CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2EngCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. ENGCRS). + /// CRS name. + /// Engineering datum. + /// Coordinate system. + public Wkt2EngCrs(string keyword, string name, Wkt2EngineeringDatum datum, Wkt2CoordinateSystem coordinateSystem) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Datum = datum ?? throw new ArgumentNullException(nameof(datum)); + CoordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum. + /// + public Wkt2EngineeringDatum Datum { get; } + + /// + /// Gets the coordinate system. + /// + public Wkt2CoordinateSystem CoordinateSystem { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngineeringDatum.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngineeringDatum.cs new file mode 100644 index 0000000..2c8386d --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngineeringDatum.cs @@ -0,0 +1,42 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Engineering datum element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2EngineeringDatum + { + /// + /// Initializes a new instance. + /// + /// Datum keyword (e.g. EDATUM). + /// Datum name. + public Wkt2EngineeringDatum(string keyword, string name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the datum keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum name. + /// + public string Name { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeodeticDatum.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeodeticDatum.cs new file mode 100644 index 0000000..93a3aa4 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeodeticDatum.cs @@ -0,0 +1,44 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Geodetic datum element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2GeodeticDatum + { + /// + /// Initializes a new instance. + /// + /// Datum keyword (e.g. DATUM or TRF). + /// Datum name. + /// Associated ellipsoid. + public Wkt2GeodeticDatum(string keyword, string name, Wkt2Ellipsoid ellipsoid) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + Ellipsoid = ellipsoid ?? throw new ArgumentNullException(nameof(ellipsoid)); + } + + /// + /// Gets the datum keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum name. + /// + public string Name { get; } + + /// + /// Gets the ellipsoid. + /// + public Wkt2Ellipsoid Ellipsoid { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeogCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeogCrs.cs new file mode 100644 index 0000000..ff9a2d9 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeogCrs.cs @@ -0,0 +1,54 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Geographic CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2GeogCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. GEOGCRS). + /// CRS name. + /// Geodetic datum. + /// Coordinate system. + public Wkt2GeogCrs(string keyword, string name, Wkt2GeodeticDatum datum, Wkt2CoordinateSystem coordinateSystem) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Datum = datum ?? throw new ArgumentNullException(nameof(datum)); + CoordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum. + /// + public Wkt2GeodeticDatum Datum { get; } + + /// + /// Gets or sets the prime meridian. + /// + public Wkt2PrimeMeridian PrimeMeridian { get; set; } + + /// + /// Gets the coordinate system. + /// + public Wkt2CoordinateSystem CoordinateSystem { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Id.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Id.cs new file mode 100644 index 0000000..69580ca --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Id.cs @@ -0,0 +1,42 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Identifier element as used in WKT2 (e.g. ID["EPSG",4326]). + /// + [Serializable] + public sealed class Wkt2Id + { + /// + /// Initializes a new instance. + /// + /// Authority name. + /// Authority code. + public Wkt2Id(string authority, string code) + { + Authority = authority ?? throw new ArgumentNullException(nameof(authority)); + Code = code ?? throw new ArgumentNullException(nameof(code)); + } + + /// + /// Gets the authority name. + /// + public string Authority { get; } + + /// + /// Gets the authority code. + /// + public string Code { get; } + + /// + /// Gets or sets an optional URI. + /// + public string Uri { get; set; } + + /// + /// Gets or sets an optional version string. + /// + public string Version { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Parameter.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Parameter.cs new file mode 100644 index 0000000..dbbcb5f --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Parameter.cs @@ -0,0 +1,37 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Parameter element as used in WKT2 conversions/transformations. + /// + [Serializable] + public sealed class Wkt2Parameter + { + /// + /// Initializes a new instance. + /// + /// Parameter name. + /// Parameter value. + public Wkt2Parameter(string name, double value) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Value = value; + } + + /// + /// Gets the parameter name. + /// + public string Name { get; } + + /// + /// Gets the parameter value. + /// + public double Value { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricCrs.cs new file mode 100644 index 0000000..c01fe67 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricCrs.cs @@ -0,0 +1,49 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Parametric CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2ParametricCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. PARAMETRICCRS). + /// CRS name. + /// Parametric datum. + /// Coordinate system. + public Wkt2ParametricCrs(string keyword, string name, Wkt2ParametricDatum datum, Wkt2CoordinateSystem coordinateSystem) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Datum = datum ?? throw new ArgumentNullException(nameof(datum)); + CoordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the parametric datum. + /// + public Wkt2ParametricDatum Datum { get; } + + /// + /// Gets the parametric CRS coordinate system. + /// + public Wkt2CoordinateSystem CoordinateSystem { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricDatum.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricDatum.cs new file mode 100644 index 0000000..9802cf0 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricDatum.cs @@ -0,0 +1,42 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Parametric datum element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2ParametricDatum + { + /// + /// Initializes a new instance. + /// + /// Datum keyword (e.g. PDATUM). + /// Datum name. + public Wkt2ParametricDatum(string keyword, string name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the datum keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum name. + /// + public string Name { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2PrimeMeridian.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2PrimeMeridian.cs new file mode 100644 index 0000000..3a48a40 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2PrimeMeridian.cs @@ -0,0 +1,42 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Prime meridian element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2PrimeMeridian + { + /// + /// Initializes a new instance. + /// + /// Prime meridian name. + /// Longitude value. + public Wkt2PrimeMeridian(string name, double longitude) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Longitude = longitude; + } + + /// + /// Gets the prime meridian name. + /// + public string Name { get; } + + /// + /// Gets the longitude. + /// + public double Longitude { get; } + + /// + /// Gets or sets an optional angle unit. + /// + public Wkt2Unit AngleUnit { get; set; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ProjCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ProjCrs.cs new file mode 100644 index 0000000..6f41821 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ProjCrs.cs @@ -0,0 +1,56 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Projected CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2ProjCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. PROJCRS). + /// CRS name. + /// Base geographic CRS. + /// Conversion. + /// Coordinate system. + public Wkt2ProjCrs(string keyword, string name, Wkt2GeogCrs baseCrs, Wkt2Conversion conversion, Wkt2CoordinateSystem coordinateSystem) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + BaseCrs = baseCrs ?? throw new ArgumentNullException(nameof(baseCrs)); + Conversion = conversion ?? throw new ArgumentNullException(nameof(conversion)); + CoordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the base geographic CRS. + /// + public Wkt2GeogCrs BaseCrs { get; } + + /// + /// Gets the defining conversion (map projection). + /// + public Wkt2Conversion Conversion { get; } + + /// + /// Gets the projected CRS coordinate system. + /// + public Wkt2CoordinateSystem CoordinateSystem { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Unit.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Unit.cs new file mode 100644 index 0000000..7c84aa4 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Unit.cs @@ -0,0 +1,44 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Unit element as used in WKT2 (e.g. ANGLEUNIT, LENGTHUNIT). + /// + [Serializable] + public sealed class Wkt2Unit + { + /// + /// Initializes a new instance. + /// + /// Unit keyword (e.g. ANGLEUNIT). + /// Unit name. + /// Conversion factor to the SI base unit. + public Wkt2Unit(string keyword, string name, double conversionFactor) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + ConversionFactor = conversionFactor; + } + + /// + /// Gets the unit keyword. + /// + public string Keyword { get; } + + /// + /// Gets the unit name. + /// + public string Name { get; } + + /// + /// Gets the conversion factor. + /// + public double ConversionFactor { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VertCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VertCrs.cs new file mode 100644 index 0000000..0042880 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VertCrs.cs @@ -0,0 +1,49 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Vertical CRS element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2VertCrs : Wkt2CrsBase + { + /// + /// Initializes a new instance. + /// + /// CRS keyword (e.g. VERTCRS). + /// CRS name. + /// Vertical datum. + /// Coordinate system. + public Wkt2VertCrs(string keyword, string name, Wkt2VerticalDatum datum, Wkt2CoordinateSystem coordinateSystem) + : base(name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Datum = datum ?? throw new ArgumentNullException(nameof(datum)); + CoordinateSystem = coordinateSystem ?? throw new ArgumentNullException(nameof(coordinateSystem)); + } + + /// + /// Gets the WKT2 keyword. + /// + public string Keyword { get; } + + /// + /// Gets the vertical datum. + /// + public Wkt2VerticalDatum Datum { get; } + + /// + /// Gets the vertical CRS coordinate system. + /// + public Wkt2CoordinateSystem CoordinateSystem { get; } + + /// + /// Serializes the CRS back to WKT2. + /// + public override string ToWkt2String() + { + return IO.CoordinateSystems.CoordinateSystemWkt2Writer.Write(this); + } + } +} diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VerticalDatum.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VerticalDatum.cs new file mode 100644 index 0000000..59a4548 --- /dev/null +++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VerticalDatum.cs @@ -0,0 +1,42 @@ +using System; + +namespace ProjNet.CoordinateSystems.Wkt2 +{ + /// + /// Vertical datum element as used in WKT2. + /// + [Serializable] + public sealed class Wkt2VerticalDatum + { + /// + /// Initializes a new instance. + /// + /// Datum keyword (e.g. VDATUM). + /// Datum name. + public Wkt2VerticalDatum(string keyword, string name) + { + Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword)); + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the datum keyword. + /// + public string Keyword { get; } + + /// + /// Gets the datum name. + /// + public string Name { get; } + + /// + /// Gets or sets an optional identifier. + /// + public Wkt2Id Id { get; set; } + + /// + /// Gets or sets an optional remark. + /// + public string Remark { get; set; } + } +} diff --git a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Reader.cs b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Reader.cs new file mode 100644 index 0000000..ca0091a --- /dev/null +++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Reader.cs @@ -0,0 +1,1254 @@ +using System; +using System.IO; +using ProjNet.CoordinateSystems; +using ProjNet.CoordinateSystems.Wkt2; + +namespace ProjNet.IO.CoordinateSystems +{ + /// + /// Reads and parses WKT2 (OGC 18-010r7 / ISO 19162:2019) CRS definitions. + /// + public static class CoordinateSystemWkt2Reader + { + /// + /// Parses WKT2 into a native WKT2 model. + /// + public static Wkt2CrsBase ParseCrs(string wkt) + { + if (string.IsNullOrWhiteSpace(wkt)) + throw new ArgumentNullException(nameof(wkt)); + + using (TextReader reader = new StringReader(wkt)) + { + var tokenizer = new WktStreamTokenizer(reader); + tokenizer.NextToken(); + + string rootKeyword = tokenizer.GetStringValue(); + switch (rootKeyword.ToUpperInvariant()) + { + case "GEOGCRS": + case "GEOGRAPHICCRS": + return ReadGeogCrs(rootKeyword, tokenizer); + case "PROJCRS": + case "PROJECTEDCRS": + return ReadProjCrs(rootKeyword, tokenizer); + case "VERTCRS": + case "VERTICALCRS": + return ReadVertCrs(rootKeyword, tokenizer); + case "COMPOUNDCRS": + return ReadCompoundCrs(rootKeyword, tokenizer); + case "BOUNDCRS": + return ReadBoundCrs(rootKeyword, tokenizer); + case "ENGCRS": + case "ENGINEERINGCRS": + return ReadEngCrs(rootKeyword, tokenizer); + case "PARAMETRICCRS": + return ReadParametricCrs(rootKeyword, tokenizer); + default: + throw new ArgumentException($"'{rootKeyword}' is not recognized as a supported WKT2 CRS."); + } + } + } + + /// + /// Parses WKT2 and converts to existing ProjNet model (normalized to ProjNet conventions). + /// + public static IInfo Parse(string wkt) + { + var crs = ParseCrs(wkt); + switch (crs) + { + case Wkt2GeogCrs geog: + return Wkt2Conversions.ToProjNetGeographicCoordinateSystem(geog); + case Wkt2ProjCrs proj: + return Wkt2Conversions.ToProjNetProjectedCoordinateSystem(proj); + default: + throw new NotSupportedException($"WKT2 CRS model '{crs.GetType().Name}' is not supported for conversion."); + } + + } + + private static Wkt2EngCrs ReadEngCrs(string keyword, WktStreamTokenizer tokenizer) + { + // ENGCRS["name", EDATUM/DATUM[...], CS[...], AXIS..., UNIT..., ID..., ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2EngineeringDatum datum = null; + Wkt2CoordinateSystem cs = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "EDATUM": + case "DATUM": + datum = ReadEngineeringDatum(element, tokenizer); + break; + case "CS": + cs = ReadCoordinateSystem(tokenizer); + break; + case "AXIS": + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + cs.Axes.Add(ReadAxis(tokenizer)); + break; + case "LENGTHUNIT": + case "UNIT": + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + cs.Unit = ReadUnit(element, tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (datum == null) + throw new ArgumentException("ENGCRS is missing EDATUM/DATUM."); + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + + var crs = new Wkt2EngCrs(keyword, name, datum, cs) + { + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2EngineeringDatum ReadEngineeringDatum(string keyword, WktStreamTokenizer tokenizer) + { + // EDATUM/DATUM["name", ID[...], REMARK[...]] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + Wkt2Id id = null; + string remark = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + id = ReadId(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2EngineeringDatum(keyword, name) { Id = id, Remark = remark }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2ParametricCrs ReadParametricCrs(string keyword, WktStreamTokenizer tokenizer) + { + // PARAMETRICCRS["name", PDATUM/DATUM[...], CS[parametric,1], AXIS..., (PARAMETRICUNIT|UNIT)...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2ParametricDatum datum = null; + Wkt2CoordinateSystem cs = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "PDATUM": + case "DATUM": + datum = ReadParametricDatum(element, tokenizer); + break; + case "CS": + cs = ReadCoordinateSystem(tokenizer); + break; + case "AXIS": + if (cs == null) + cs = new Wkt2CoordinateSystem("parametric", 1); + cs.Axes.Add(ReadAxis(tokenizer)); + break; + case "PARAMETRICUNIT": + case "UNIT": + if (cs == null) + cs = new Wkt2CoordinateSystem("parametric", 1); + cs.Unit = ReadUnit(element, tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (datum == null) + throw new ArgumentException("PARAMETRICCRS is missing PDATUM/DATUM."); + if (cs == null) + cs = new Wkt2CoordinateSystem("parametric", 1); + + var crs = new Wkt2ParametricCrs(keyword, name, datum, cs) + { + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2ParametricDatum ReadParametricDatum(string keyword, WktStreamTokenizer tokenizer) + { + // PDATUM/DATUM["name", ID[...], REMARK[...]] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + Wkt2Id id = null; + string remark = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + id = ReadId(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2ParametricDatum(keyword, name) { Id = id, Remark = remark }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2BoundCrs ReadBoundCrs(string keyword, WktStreamTokenizer tokenizer) + { + // BOUNDCRS["name", SOURCECRS[...], TARGETCRS[...], ABRIDGEDTRANSFORMATION[...], ID[..]?, REMARK[..]?] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2CrsBase sourceCrs = null; + Wkt2CrsBase targetCrs = null; + Wkt2AbridgedTransformation transformation = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "SOURCECRS": + sourceCrs = ReadBoundCrsChildCrs(tokenizer); + break; + case "TARGETCRS": + targetCrs = ReadBoundCrsChildCrs(tokenizer); + break; + case "ABRIDGEDTRANSFORMATION": + transformation = ReadAbridgedTransformation(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (sourceCrs == null) + throw new ArgumentException("BOUNDCRS is missing SOURCECRS."); + if (targetCrs == null) + throw new ArgumentException("BOUNDCRS is missing TARGETCRS."); + if (transformation == null) + throw new ArgumentException("BOUNDCRS is missing ABRIDGEDTRANSFORMATION."); + + var crs = new Wkt2BoundCrs(keyword, name, sourceCrs, targetCrs, transformation) + { + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2CrsBase ReadBoundCrsChildCrs(WktStreamTokenizer tokenizer) + { + // SOURCECRS[TARGETCRS] wraps a CRS inside its own brackets. + var bracket = tokenizer.ReadOpener(); + tokenizer.NextToken(); + + Wkt2CrsBase crs = null; + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "GEOGCRS": + case "GEOGRAPHICCRS": + crs = ReadGeogCrs(element, tokenizer); + break; + case "PROJCRS": + case "PROJECTEDCRS": + crs = ReadProjCrs(element, tokenizer); + break; + case "VERTCRS": + case "VERTICALCRS": + crs = ReadVertCrs(element, tokenizer); + break; + case "COMPOUNDCRS": + crs = ReadCompoundCrs(element, tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (crs == null) + throw new ArgumentException("SOURCECRS/TARGETCRS has no CRS."); + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2AbridgedTransformation ReadAbridgedTransformation(WktStreamTokenizer tokenizer) + { + // ABRIDGEDTRANSFORMATION["name", METHOD["..."], PARAMETER[...], ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + string methodName = null; + var transform = new Wkt2AbridgedTransformation(name, string.Empty); + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "METHOD": + methodName = ReadMethodName(tokenizer); + transform = new Wkt2AbridgedTransformation(name, methodName); + break; + case "PARAMETER": + transform.Parameters.Add(ReadParameter(tokenizer)); + break; + case "ID": + transform.Id = ReadId(tokenizer); + break; + case "REMARK": + transform.Remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (string.IsNullOrWhiteSpace(transform.MethodName)) + throw new ArgumentException("ABRIDGEDTRANSFORMATION is missing METHOD."); + return transform; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2CompoundCrs ReadCompoundCrs(string keyword, WktStreamTokenizer tokenizer) + { + // COMPOUNDCRS["name", , , ... , ID[..]?, REMARK[..]?] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + var components = new System.Collections.Generic.List(); + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "GEOGCRS": + case "GEOGRAPHICCRS": + components.Add(ReadGeogCrs(element, tokenizer)); + break; + case "PROJCRS": + case "PROJECTEDCRS": + components.Add(ReadProjCrs(element, tokenizer)); + break; + case "VERTCRS": + case "VERTICALCRS": + components.Add(ReadVertCrs(element, tokenizer)); + break; + case "ID": + id = ReadId(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (components.Count == 0) + throw new ArgumentException("COMPOUNDCRS has no component CRS."); + + var crs = new Wkt2CompoundCrs(keyword, name, components) + { + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + + tokenizer.NextToken(); + } + } + + private static Wkt2VertCrs ReadVertCrs(string keyword, WktStreamTokenizer tokenizer) + { + // VERTCRS["name", VDATUM/DATUM[...], CS[vertical,1], AXIS[...], LENGTHUNIT[...], ID[...], ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2VerticalDatum datum = null; + Wkt2CoordinateSystem cs = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "VDATUM": + case "DATUM": + datum = ReadVerticalDatum(element, tokenizer); + break; + case "CS": + cs = ReadCoordinateSystem(tokenizer); + break; + case "AXIS": + if (cs == null) + cs = new Wkt2CoordinateSystem("vertical", 1); + cs.Axes.Add(ReadAxis(tokenizer)); + break; + case "LENGTHUNIT": + case "UNIT": + if (cs == null) + cs = new Wkt2CoordinateSystem("vertical", 1); + cs.Unit = ReadUnit(element, tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + + if (datum == null) + throw new ArgumentException("VERTCRS is missing VDATUM/DATUM."); + if (cs == null) + cs = new Wkt2CoordinateSystem("vertical", 1); + + var crs = new Wkt2VertCrs(keyword, name, datum, cs) + { + Id = id, + Remark = remark + }; + return crs; + + default: + SkipUnknownElement(tokenizer); + break; + } + + tokenizer.NextToken(); + } + } + + private static Wkt2VerticalDatum ReadVerticalDatum(string keyword, WktStreamTokenizer tokenizer) + { + // VDATUM/DATUM["name", ID[...], REMARK[...]] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + Wkt2Id id = null; + string remark = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + id = ReadId(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2VerticalDatum(keyword, name) { Id = id, Remark = remark }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2ProjCrs ReadProjCrs(string keyword, WktStreamTokenizer tokenizer) + { + // PROJCRS["name", BASEGEOGCRS[...]|GEOGCRS[...], CONVERSION[...], CS[...], AXIS..., UNIT..., ID...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2GeogCrs baseCrs = null; + Wkt2Conversion conversion = null; + Wkt2CoordinateSystem cs = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "BASEGEOGCRS": + case "BASEGEODCRS": + baseCrs = ReadBaseGeogCrs(tokenizer); + break; + case "GEOGCRS": + case "GEOGRAPHICCRS": + baseCrs = ReadGeogCrs(element, tokenizer); + break; + case "CONVERSION": + conversion = ReadConversion(tokenizer); + break; + case "CS": + cs = ReadCoordinateSystem(tokenizer); + break; + case "AXIS": + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + cs.Axes.Add(ReadAxis(tokenizer)); + break; + case "LENGTHUNIT": + case "UNIT": + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + cs.Unit = ReadUnit(element, tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (baseCrs == null) + throw new ArgumentException("PROJCRS is missing BASEGEOGCRS/GEOGCRS."); + if (conversion == null) + throw new ArgumentException("PROJCRS is missing CONVERSION."); + if (cs == null) + cs = new Wkt2CoordinateSystem("cartesian", 2); + + var crs = new Wkt2ProjCrs(keyword, name, baseCrs, conversion, cs) + { + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + + tokenizer.NextToken(); + } + } + + private static Wkt2GeogCrs ReadBaseGeogCrs(WktStreamTokenizer tokenizer) + { + // BASEGEOGCRS["name", DATUM/TRF[...], PRIMEM[...]?, ID...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2GeodeticDatum datum = null; + Wkt2PrimeMeridian primeMeridian = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "DATUM": + case "TRF": + case "GEODETICDATUM": + datum = ReadGeodeticDatum(element, tokenizer); + break; + case "PRIMEM": + case "PRIMEMERIDIAN": + primeMeridian = ReadPrimeMeridian(tokenizer); + break; + case "REMARK": + remark = ReadRemark(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (datum == null) + throw new ArgumentException("BASEGEOGCRS is missing DATUM/TRF."); + + var cs = new Wkt2CoordinateSystem("ellipsoidal", 2); + var crs = new Wkt2GeogCrs("GEOGCRS", name, datum, cs) + { + PrimeMeridian = primeMeridian, + Id = id, + Remark = remark + }; + return crs; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2Conversion ReadConversion(WktStreamTokenizer tokenizer) + { + // CONVERSION["name", METHOD["..."], PARAMETER[...], ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + string methodName = null; + var conversion = new Wkt2Conversion(name, string.Empty); + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "METHOD": + methodName = ReadMethodName(tokenizer); + conversion = new Wkt2Conversion(name, methodName); + break; + case "PARAMETER": + conversion.Parameters.Add(ReadParameter(tokenizer)); + break; + case "ID": + conversion.Id = ReadId(tokenizer); + break; + case "REMARK": + conversion.Remark = ReadRemark(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (string.IsNullOrWhiteSpace(conversion.MethodName)) + throw new ArgumentException("CONVERSION is missing METHOD."); + return conversion; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static string ReadMethodName(WktStreamTokenizer tokenizer) + { + // METHOD["...", ID[...], REMARK[...], ...] + var bracket = tokenizer.ReadOpener(); + string methodName = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + case "REMARK": + case ",": + SkipUnknownElement(tokenizer); + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return methodName; + default: + SkipUnknownElement(tokenizer); + break; + } + + tokenizer.NextToken(); + } + } + + private static Wkt2Parameter ReadParameter(WktStreamTokenizer tokenizer) + { + // PARAMETER["name", value, (UNIT[...]?) (ID[...]?)] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + double value = tokenizer.GetNumericValue(); + + Wkt2Id id = null; + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + id = ReadId(tokenizer); + break; + case "UNIT": + case "LENGTHUNIT": + case "ANGLEUNIT": + case "SCALEUNIT": + case "TIMEUNIT": + // parsed but not stored yet + ReadUnit(element, tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2Parameter(name, value) { Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2GeogCrs ReadGeogCrs(string keyword, WktStreamTokenizer tokenizer) + { + // GEOGCRS["name", DATUM/TRF[...], PRIMEM[...]?, CS[...], AXIS..., (cs unit), ... ID[...] ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2GeodeticDatum datum = null; + Wkt2PrimeMeridian primeMeridian = null; + Wkt2CoordinateSystem cs = null; + Wkt2Id id = null; + string remark = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "DATUM": + case "TRF": + case "GEODETICDATUM": + datum = ReadGeodeticDatum(element, tokenizer); + break; + + case "PRIMEM": + case "PRIMEMERIDIAN": + primeMeridian = ReadPrimeMeridian(tokenizer); + break; + + case "CS": + cs = ReadCoordinateSystem(tokenizer); + break; + + case "AXIS": + if (cs == null) + cs = new Wkt2CoordinateSystem("ellipsoidal", 2); + cs.Axes.Add(ReadAxis(tokenizer)); + break; + + case "ANGLEUNIT": + case "LENGTHUNIT": + case "UNIT": + if (cs == null) + cs = new Wkt2CoordinateSystem("ellipsoidal", 2); + cs.Unit = ReadUnit(element, tokenizer); + break; + + case "REMARK": + remark = ReadRemark(tokenizer); + break; + + case "ID": + id = ReadId(tokenizer); + break; + + case ",": + break; + + case "]": + case ")": + tokenizer.CheckCloser(bracket); + + if (datum == null) + throw new ArgumentException("GEOGCRS is missing DATUM/TRF."); + + if (cs == null) + cs = new Wkt2CoordinateSystem("ellipsoidal", 2); + + var crs = new Wkt2GeogCrs(keyword, name, datum, cs) + { + PrimeMeridian = primeMeridian, + Id = id, + Remark = remark + }; + return crs; + + default: + SkipUnknownElement(tokenizer); + break; + } + + tokenizer.NextToken(); + } + } + + private static Wkt2GeodeticDatum ReadGeodeticDatum(string keyword, WktStreamTokenizer tokenizer) + { + // DATUM["name", ELLIPSOID[...], ...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + Wkt2Ellipsoid ellipsoid = null; + Wkt2Id id = null; + + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ELLIPSOID": + case "SPHEROID": + ellipsoid = ReadEllipsoid(tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + if (ellipsoid == null) + throw new ArgumentException("DATUM/TRF missing ELLIPSOID."); + return new Wkt2GeodeticDatum(keyword, name, ellipsoid) { Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2Ellipsoid ReadEllipsoid(WktStreamTokenizer tokenizer) + { + // ELLIPSOID["name", a, invf, LENGTHUNIT[...], ID[...]] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + double semiMajorAxis = tokenizer.GetNumericValue(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + double invFlattening = tokenizer.GetNumericValue(); + + Wkt2Unit lengthUnit = null; + Wkt2Id id = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "LENGTHUNIT": + case "UNIT": + lengthUnit = ReadUnit(element, tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2Ellipsoid(name, semiMajorAxis, invFlattening) { LengthUnit = lengthUnit, Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2PrimeMeridian ReadPrimeMeridian(WktStreamTokenizer tokenizer) + { + // PRIMEM["name", longitude, (ANGLEUNIT[...]?) (ID[...]?) ] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + double longitude = tokenizer.GetNumericValue(); + + Wkt2Unit unit = null; + Wkt2Id id = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ANGLEUNIT": + case "UNIT": + unit = ReadUnit(element, tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2PrimeMeridian(name, longitude) { AngleUnit = unit, Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2CoordinateSystem ReadCoordinateSystem(WktStreamTokenizer tokenizer) + { + // CS[ellipsoidal,2|3] (plus optional ID) + var bracket = tokenizer.ReadOpener(); + tokenizer.NextToken(); + string csType = tokenizer.GetStringValue(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + int dimension = (int)tokenizer.GetNumericValue(); + + Wkt2Id id = null; + tokenizer.NextToken(); + while (tokenizer.GetStringValue() != "]" && tokenizer.GetStringValue() != ")") + { + if (tokenizer.GetStringValue().Equals("ID", StringComparison.OrdinalIgnoreCase)) + { + id = ReadId(tokenizer); + } + else + { + SkipUnknownElement(tokenizer); + } + tokenizer.NextToken(); + } + tokenizer.CheckCloser(bracket); + + return new Wkt2CoordinateSystem(csType, dimension) { Id = id }; + } + + private static Wkt2Axis ReadAxis(WktStreamTokenizer tokenizer) + { + // AXIS["name",direction,(ORDER[...])?,(UNIT[...]|ANGLEUNIT[...]|LENGTHUNIT[...])?,(ID[...])?] + var bracket = tokenizer.ReadOpener(); + string axisName = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + string direction = tokenizer.GetStringValue(); + + int? order = null; + Wkt2Unit unit = null; + Wkt2Id id = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ORDER": + { + var b = tokenizer.ReadOpener(); + tokenizer.NextToken(); + order = (int)tokenizer.GetNumericValue(); + tokenizer.ReadCloser(b); + break; + } + case "ANGLEUNIT": + case "LENGTHUNIT": + case "UNIT": + unit = ReadUnit(element, tokenizer); + break; + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2Axis(axisName, direction) { Order = order, Unit = unit, Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2Unit ReadUnit(string unitKeyword, WktStreamTokenizer tokenizer) + { + // ANGLEUNIT/LENGTHUNIT/UNIT["name",factor,(ID[...])...] + var bracket = tokenizer.ReadOpener(); + string name = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + double factor = tokenizer.GetNumericValue(); + + Wkt2Id id = null; + + tokenizer.NextToken(); + while (true) + { + string element = tokenizer.GetStringValue(); + switch (element.ToUpperInvariant()) + { + case "ID": + id = ReadId(tokenizer); + break; + case ",": + break; + case "]": + case ")": + tokenizer.CheckCloser(bracket); + return new Wkt2Unit(unitKeyword, name, factor) { Id = id }; + default: + SkipUnknownElement(tokenizer); + break; + } + tokenizer.NextToken(); + } + } + + private static Wkt2Id ReadId(WktStreamTokenizer tokenizer) + { + // ID["EPSG",4326,("version")?,URI[...]*] + if (!tokenizer.GetStringValue().Equals("ID", StringComparison.OrdinalIgnoreCase)) + tokenizer.ReadToken("ID"); + + var bracket = tokenizer.ReadOpener(); + string authority = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadToken(","); + tokenizer.NextToken(); + + string code; + if (tokenizer.GetTokenType() == TokenType.Number) + code = ((long)tokenizer.GetNumericValue()).ToString(); + else + code = tokenizer.ReadDoubleQuotedWord(); + + string version = null; + string uri = null; + + tokenizer.NextToken(); + while (tokenizer.GetStringValue() != "]" && tokenizer.GetStringValue() != ")") + { + if (tokenizer.GetStringValue() == ",") + { + } + else if (tokenizer.GetStringValue().Equals("URI", StringComparison.OrdinalIgnoreCase)) + { + var u = tokenizer.ReadOpener(); + string v = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadCloser(u); + uri = v; + } + else if (tokenizer.GetStringValue() == "\"") + { + version = tokenizer.ReadDoubleQuotedWord(); + } + else + { + SkipUnknownElement(tokenizer); + } + + tokenizer.NextToken(); + } + + tokenizer.CheckCloser(bracket); + + var id = new Wkt2Id(authority, code) + { + Version = version, + Uri = uri + }; + return id; + } + + private static string ReadRemark(WktStreamTokenizer tokenizer) + { + // REMARK["..."] + var bracket = tokenizer.ReadOpener(); + string remark = tokenizer.ReadDoubleQuotedWord(); + tokenizer.ReadCloser(bracket); + return remark; + } + + private static void SkipUnknownElement(WktStreamTokenizer tokenizer) + { + if (tokenizer.GetStringValue() == ",") + return; + + if (tokenizer.GetStringValue() == "[" || tokenizer.GetStringValue() == "(" || tokenizer.GetStringValue() == "]" || tokenizer.GetStringValue() == ")") + return; + + var tokenType = tokenizer.GetTokenType(); + string current = tokenizer.GetStringValue(); + + if (tokenType == TokenType.Number || current == "\"" || tokenType == TokenType.Word) + { + tokenizer.NextToken(); + if (tokenizer.GetStringValue() == "[" || tokenizer.GetStringValue() == "(") + { + var bracket = tokenizer.GetStringValue() == "[" ? WktBracket.Square : WktBracket.Round; + int depth = 1; + while (depth > 0) + { + tokenizer.NextToken(false); + string sv = tokenizer.GetStringValue(); + if (sv == "[" || sv == "(") depth++; + else if (sv == "]" || sv == ")") depth--; + } + tokenizer.CheckCloser(bracket); + } + } + } + } +} diff --git a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs new file mode 100644 index 0000000..245f64c --- /dev/null +++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs @@ -0,0 +1,608 @@ +using System; +using System.Globalization; +using System.Text; +using ProjNet.CoordinateSystems.Wkt2; + +namespace ProjNet.IO.CoordinateSystems +{ + /// + /// Serializes WKT2 CRS model objects to WKT2 (OGC 18-010r7 / ISO 19162:2019) strings. + /// + public static class CoordinateSystemWkt2Writer + { + /// + /// Writes a WKT2 string from a WKT2 CRS model. + /// + /// The CRS model to serialize. + /// A WKT2 string. + public static string Write(Wkt2CrsBase crs) + { + if (crs == null) throw new ArgumentNullException(nameof(crs)); + + if (crs is Wkt2GeogCrs geog) + return WriteGeogCrs(geog); + if (crs is Wkt2ProjCrs proj) + return WriteProjCrs(proj); + if (crs is Wkt2VertCrs vert) + return WriteVertCrs(vert); + if (crs is Wkt2CompoundCrs compound) + return WriteCompoundCrs(compound); + if (crs is Wkt2BoundCrs bound) + return WriteBoundCrs(bound); + if (crs is Wkt2EngCrs eng) + return WriteEngCrs(eng); + if (crs is Wkt2ParametricCrs param) + return WriteParametricCrs(param); + + throw new NotSupportedException($"WKT2 writer does not support '{crs.GetType().Name}'."); + } + + private static string WriteEngCrs(Wkt2EngCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + sb.Append(WriteEngineeringDatum(crs.Datum)); + sb.Append(','); + sb.Append(WriteCs(crs.CoordinateSystem)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteEngineeringDatum(Wkt2EngineeringDatum datum) + { + var sb = new StringBuilder(); + sb.Append(datum.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(datum.Name)); + sb.Append('"'); + + if (datum.Id != null) + { + sb.Append(','); + sb.Append(WriteId(datum.Id)); + } + + if (!string.IsNullOrWhiteSpace(datum.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(datum.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteParametricCrs(Wkt2ParametricCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + sb.Append(WriteParametricDatum(crs.Datum)); + sb.Append(','); + sb.Append(WriteCs(crs.CoordinateSystem)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteParametricDatum(Wkt2ParametricDatum datum) + { + var sb = new StringBuilder(); + sb.Append(datum.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(datum.Name)); + sb.Append('"'); + + if (datum.Id != null) + { + sb.Append(','); + sb.Append(WriteId(datum.Id)); + } + + if (!string.IsNullOrWhiteSpace(datum.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(datum.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteBoundCrs(Wkt2BoundCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + sb.Append("SOURCECRS["); + sb.Append(Write(crs.SourceCrs)); + sb.Append("],"); + + sb.Append("TARGETCRS["); + sb.Append(Write(crs.TargetCrs)); + sb.Append("],"); + + sb.Append(WriteAbridgedTransformation(crs.Transformation)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteAbridgedTransformation(Wkt2AbridgedTransformation transform) + { + var sb = new StringBuilder(); + sb.Append("ABRIDGEDTRANSFORMATION[\""); + sb.Append(EscapeQuotedText(transform.Name)); + sb.Append("\","); + sb.Append("METHOD[\""); + sb.Append(EscapeQuotedText(transform.MethodName)); + sb.Append("\"]"); + + foreach (var p in transform.Parameters) + { + sb.Append(','); + sb.Append(WriteParameter(p)); + } + + if (transform.Id != null) + { + sb.Append(','); + sb.Append(WriteId(transform.Id)); + } + + if (!string.IsNullOrWhiteSpace(transform.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(transform.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteCompoundCrs(Wkt2CompoundCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append('"'); + + foreach (var component in crs.Components) + { + sb.Append(','); + sb.Append(Write(component)); + } + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteVertCrs(Wkt2VertCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + sb.Append(WriteVerticalDatum(crs.Datum)); + sb.Append(','); + sb.Append(WriteCs(crs.CoordinateSystem)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteVerticalDatum(Wkt2VerticalDatum datum) + { + var sb = new StringBuilder(); + sb.Append(datum.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(datum.Name)); + sb.Append("\""); + + if (datum.Id != null) + { + sb.Append(','); + sb.Append(WriteId(datum.Id)); + } + + if (!string.IsNullOrWhiteSpace(datum.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(datum.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteProjCrs(Wkt2ProjCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + // WKT2 has BASEGEOGCRS; we emit base CRS as GEOGCRS using the existing writer. + sb.Append(WriteGeogCrs(crs.BaseCrs)); + sb.Append(','); + sb.Append(WriteConversion(crs.Conversion)); + sb.Append(','); + sb.Append(WriteCs(crs.CoordinateSystem)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteConversion(Wkt2Conversion conversion) + { + var sb = new StringBuilder(); + sb.Append("CONVERSION[\""); + sb.Append(EscapeQuotedText(conversion.Name)); + sb.Append("\","); + sb.Append("METHOD[\""); + sb.Append(EscapeQuotedText(conversion.MethodName)); + sb.Append("\"]"); + + foreach (var p in conversion.Parameters) + { + sb.Append(','); + sb.Append(WriteParameter(p)); + } + + if (conversion.Id != null) + { + sb.Append(','); + sb.Append(WriteId(conversion.Id)); + } + + if (!string.IsNullOrWhiteSpace(conversion.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(conversion.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteParameter(Wkt2Parameter parameter) + { + var sb = new StringBuilder(); + sb.Append("PARAMETER[\""); + sb.Append(EscapeQuotedText(parameter.Name)); + sb.Append("\","); + sb.Append(parameter.Value.ToString("R", CultureInfo.InvariantCulture)); + + if (parameter.Id != null) + { + sb.Append(','); + sb.Append(WriteId(parameter.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteGeogCrs(Wkt2GeogCrs crs) + { + var sb = new StringBuilder(); + sb.Append(crs.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(crs.Name)); + sb.Append("\","); + + sb.Append(WriteDatum(crs.Datum)); + + if (crs.PrimeMeridian != null) + { + sb.Append(','); + sb.Append(WritePrimeMeridian(crs.PrimeMeridian)); + } + + sb.Append(','); + sb.Append(WriteCs(crs.CoordinateSystem)); + + if (crs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(crs.Id)); + } + + if (!string.IsNullOrWhiteSpace(crs.Remark)) + { + sb.Append(",REMARK[\""); + sb.Append(EscapeQuotedText(crs.Remark)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteDatum(Wkt2GeodeticDatum datum) + { + var sb = new StringBuilder(); + + sb.Append(datum.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(datum.Name)); + sb.Append("\","); + sb.Append(WriteEllipsoid(datum.Ellipsoid)); + + if (datum.Id != null) + { + sb.Append(','); + sb.Append(WriteId(datum.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteEllipsoid(Wkt2Ellipsoid ellipsoid) + { + var sb = new StringBuilder(); + + sb.Append("ELLIPSOID[\""); + sb.Append(EscapeQuotedText(ellipsoid.Name)); + sb.Append("\","); + sb.Append(ellipsoid.SemiMajorAxis.ToString("R", CultureInfo.InvariantCulture)); + sb.Append(','); + sb.Append(ellipsoid.InverseFlattening.ToString("R", CultureInfo.InvariantCulture)); + + if (ellipsoid.LengthUnit != null) + { + sb.Append(','); + sb.Append(WriteUnit(ellipsoid.LengthUnit)); + } + + if (ellipsoid.Id != null) + { + sb.Append(','); + sb.Append(WriteId(ellipsoid.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WritePrimeMeridian(Wkt2PrimeMeridian pm) + { + var sb = new StringBuilder(); + + sb.Append("PRIMEM[\""); + sb.Append(EscapeQuotedText(pm.Name)); + sb.Append("\","); + sb.Append(pm.Longitude.ToString("R", CultureInfo.InvariantCulture)); + + if (pm.AngleUnit != null) + { + sb.Append(','); + sb.Append(WriteUnit(pm.AngleUnit)); + } + + if (pm.Id != null) + { + sb.Append(','); + sb.Append(WriteId(pm.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteCs(Wkt2CoordinateSystem cs) + { + var sb = new StringBuilder(); + + sb.Append("CS["); + sb.Append(cs.Type); + sb.Append(','); + sb.Append(cs.Dimension.ToString(CultureInfo.InvariantCulture)); + if (cs.Id != null) + { + sb.Append(','); + sb.Append(WriteId(cs.Id)); + } + sb.Append(']'); + + foreach (var axis in cs.Axes) + { + sb.Append(','); + sb.Append(WriteAxis(axis)); + } + + if (cs.Unit != null) + { + sb.Append(','); + sb.Append(WriteUnit(cs.Unit)); + } + + return sb.ToString(); + } + + private static string WriteAxis(Wkt2Axis axis) + { + var sb = new StringBuilder(); + + sb.Append("AXIS[\""); + sb.Append(EscapeQuotedText(axis.Name)); + sb.Append("\","); + sb.Append(axis.Direction); + + if (axis.Order.HasValue) + { + sb.Append(",ORDER["); + sb.Append(axis.Order.Value.ToString(CultureInfo.InvariantCulture)); + sb.Append(']'); + } + + if (axis.Unit != null) + { + sb.Append(','); + sb.Append(WriteUnit(axis.Unit)); + } + + if (axis.Id != null) + { + sb.Append(','); + sb.Append(WriteId(axis.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteUnit(Wkt2Unit unit) + { + var sb = new StringBuilder(); + + sb.Append(unit.Keyword.ToUpperInvariant()); + sb.Append("[\""); + sb.Append(EscapeQuotedText(unit.Name)); + sb.Append("\","); + sb.Append(unit.ConversionFactor.ToString("R", CultureInfo.InvariantCulture)); + + if (unit.Id != null) + { + sb.Append(','); + sb.Append(WriteId(unit.Id)); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string WriteId(Wkt2Id id) + { + var sb = new StringBuilder(); + + sb.Append("ID[\""); + sb.Append(EscapeQuotedText(id.Authority)); + sb.Append("\",\""); + sb.Append(EscapeQuotedText(id.Code)); + sb.Append("\""); + + if (!string.IsNullOrWhiteSpace(id.Version)) + { + sb.Append(",\""); + sb.Append(EscapeQuotedText(id.Version)); + sb.Append("\""); + } + + if (!string.IsNullOrWhiteSpace(id.Uri)) + { + sb.Append(",URI[\""); + sb.Append(EscapeQuotedText(id.Uri)); + sb.Append("\"]"); + } + + sb.Append(']'); + return sb.ToString(); + } + + private static string EscapeQuotedText(string text) + { + // WKT2 escapes a quote inside a quoted string as double quote. + return text.Replace("\"", "\"\""); + } + } +} diff --git a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs index 01fc335..e33f83b 100644 --- a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs +++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs @@ -68,6 +68,25 @@ public static IInfo Parse(string wkt) string objectName = tokenizer.GetStringValue(); switch (objectName) { + // WKT2 (OGC 18-010r7 / ISO 19162:2019) + case "GEOGCRS": + case "GEOGRAPHICCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "PROJCRS": + case "PROJECTEDCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "VERTCRS": + case "VERTICALCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "COMPOUNDCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "BOUNDCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "ENGCRS": + case "ENGINEERINGCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); + case "PARAMETRICCRS": + return CoordinateSystemWkt2Reader.Parse(wkt); case "UNIT": return ReadUnit(tokenizer); case "SPHEROID": diff --git a/test/ProjNet.Tests/CoordinateTransformTests.cs b/test/ProjNet.Tests/CoordinateTransformTests.cs index 493b6b1..c149d5d 100644 --- a/test/ProjNet.Tests/CoordinateTransformTests.cs +++ b/test/ProjNet.Tests/CoordinateTransformTests.cs @@ -5,6 +5,7 @@ using ProjNet.CoordinateSystems; using ProjNet.CoordinateSystems.Projections; using ProjNet.CoordinateSystems.Transformations; +using ProjNet.CoordinateSystems.Wkt2; using ProjNet.Geometries; using ProjNet.IO.CoordinateSystems; @@ -1189,6 +1190,22 @@ public void TestLamberTangentialConformalConicProjectionRegistryAndTransformatio Assert.IsTrue(ToleranceLessThan(pUtm, expected, 0.05), TransformationError("LambertConicConformal2SP", expected, pUtm)); } + [Test] + public void TestTransformationFromWkt2ToWkt1() + { + string sourceWkt = "GEOGCRS[\"WGS 84 (3D)\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS[ellipsoidal,3],AXIS[\"geodetic latitude (Lat)\",north,ORDER[1],ANGLEUNIT[\"degree minute second hemisphere\",0.0174532925199433]],AXIS[\"geodetic longitude (Long)\",east,ORDER[2],ANGLEUNIT[\"degree minute second hemisphere\",0.0174532925199433]],AXIS[\"ellipsoidal height (h)\",up,ORDER[3],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"World (by country)\"],BBOX[-90,-180,90,180]],ID[\"EPSG\",4329]]"; + string targetWkt = "PROJCS[\"ED50-UTM32\",GEOGCS[\"LLERP50-W\",DATUM[\"ERP50-W\",SPHEROID[\"INTNL\",6378388.000,297.00000000]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"false_easting\",500000.000],PARAMETER[\"false_northing\",0.000],PARAMETER[\"central_meridian\",9.00000000000000],PARAMETER[\"scale_factor\",0.9996],PARAMETER[\"latitude_of_origin\",0.000],UNIT[\"Meter\",1.00000000000000]]"; + + var sourceCoordinateSystem = CoordinateSystemWkt2Reader.ParseCrs(sourceWkt); + Assert.NotNull(sourceCoordinateSystem); + + var targetCoordinateSystem = GetCoordinateSystem(targetWkt); + Assert.NotNull(targetCoordinateSystem); + + var transformation = GetTransformation(Wkt2Conversions.ToProjNetGeographicCoordinateSystem((Wkt2GeogCrs)sourceCoordinateSystem), targetCoordinateSystem); + Assert.NotNull(transformation); + } + internal static CoordinateSystem GetCoordinateSystem(string wkt) { var coordinateSystemFactory = new CoordinateSystemFactory(); diff --git a/test/ProjNet.Tests/WKT/WKT2BoundCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2BoundCrsTests.cs new file mode 100644 index 0000000..f2fe5f6 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2BoundCrsTests.cs @@ -0,0 +1,62 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2BoundCrsTests + { + private const string Wkt2BoundCrs_Etrs89_ToWgs84 = "BOUNDCRS[\"ETRS89 (bound)\"," + + "SOURCECRS[GEOGCRS[\"ETRS89\"," + + "DATUM[\"European Terrestrial Reference System 1989\",ELLIPSOID[\"GRS 1980\",6378137,298.257222101,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "CS[ellipsoidal,2]," + + "AXIS[\"longitude\",east,ORDER[1]]," + + "AXIS[\"latitude\",north,ORDER[2]]," + + "ANGLEUNIT[\"degree\",0.0174532925199433]]]," + + "TARGETCRS[GEOGCRS[\"WGS 84\"," + + "DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "CS[ellipsoidal,2]," + + "AXIS[\"longitude\",east,ORDER[1]]," + + "AXIS[\"latitude\",north,ORDER[2]]," + + "ANGLEUNIT[\"degree\",0.0174532925199433]]]," + + "ABRIDGEDTRANSFORMATION[\"ETRS89 to WGS 84\"," + + "METHOD[\"Geocentric translations\"]," + + "PARAMETER[\"X-axis translation\",0]," + + "PARAMETER[\"Y-axis translation\",0]," + + "PARAMETER[\"Z-axis translation\",0]]," + + "ID[\"EPSG\",4937]," + + "USAGE[SCOPE[\"unknown\"],AREA[\"Europe\"],BBOX[34,-10,72,40]]]"; + + [Test] + public void ParseWkt2BoundCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2BoundCrs_Etrs89_ToWgs84); + Assert.That(model, Is.InstanceOf()); + + var bound = (Wkt2BoundCrs)model; + Assert.That(bound.SourceCrs, Is.InstanceOf()); + Assert.That(bound.TargetCrs, Is.InstanceOf()); + Assert.That(bound.Transformation.MethodName, Is.EqualTo("Geocentric translations")); + Assert.That(bound.Transformation.Parameters, Has.Count.EqualTo(3)); + + string wkt2 = bound.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("BOUNDCRS[\"ETRS89 (bound)\"")); + Assert.That(wkt2, Does.Contain("SOURCECRS[GEOGCRS[\"ETRS89\"")); + Assert.That(wkt2, Does.Contain("TARGETCRS[GEOGCRS[\"WGS 84\"")); + Assert.That(wkt2, Does.Contain("ABRIDGEDTRANSFORMATION[\"ETRS89 to WGS 84\"")); + Assert.That(wkt2, Does.Contain("METHOD[\"Geocentric translations\"]")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"4937\"]")); + } + + [Test] + public void BoundCrsParserSkipsUnknownMetadata() + { + var bound = (Wkt2BoundCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2BoundCrs_Etrs89_ToWgs84); + Assert.That(bound.Id.Authority, Is.EqualTo("EPSG")); + Assert.That(bound.Id.Code, Is.EqualTo("4937")); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2CompoundCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2CompoundCrsTests.cs new file mode 100644 index 0000000..efad14e --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2CompoundCrsTests.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2CompoundCrsTests + { + private const string Wkt2CompoundCrs_Wgs84_Height = "COMPOUNDCRS[\"WGS 84 + height\"," + + "GEOGCRS[\"WGS 84\"," + + "DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "CS[ellipsoidal,2]," + + "AXIS[\"longitude\",east,ORDER[1]]," + + "AXIS[\"latitude\",north,ORDER[2]]," + + "ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "VERTCRS[\"EGM96 height\"," + + "VDATUM[\"EGM96 geoid\"]," + + "CS[vertical,1]," + + "AXIS[\"gravity-related height (H)\",up,ORDER[1]]," + + "LENGTHUNIT[\"metre\",1]]," + + "ID[\"EPSG\",4979]," + + "USAGE[SCOPE[\"unknown\"],AREA[\"World\"],BBOX[-90,-180,90,180]]]"; + + [Test] + public void ParseWkt2CompoundCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2CompoundCrs_Wgs84_Height); + Assert.That(model, Is.InstanceOf()); + + var compound = (Wkt2CompoundCrs)model; + Assert.That(compound.Components, Has.Count.EqualTo(2)); + Assert.That(compound.Components[0], Is.InstanceOf()); + Assert.That(compound.Components[1], Is.InstanceOf()); + + string wkt2 = compound.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("COMPOUNDCRS[\"WGS 84 + height\"")); + Assert.That(wkt2, Does.Contain("GEOGCRS[\"WGS 84\"")); + Assert.That(wkt2, Does.Contain("VERTCRS[\"EGM96 height\"")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"4979\"]")); + } + + [Test] + public void CompoundCrsParserSkipsUnknownMetadata() + { + var compound = (Wkt2CompoundCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2CompoundCrs_Wgs84_Height); + Assert.That(compound.Components.Count, Is.EqualTo(2)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2EngCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2EngCrsTests.cs new file mode 100644 index 0000000..b2227e2 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2EngCrsTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2EngCrsTests + { + private const string Wkt2EngCrs_LocalGrid = "ENGCRS[\"Local engineering grid\"," + + "EDATUM[\"Local datum\",ID[\"EPSG\",1234]]," + + "CS[cartesian,2]," + + "AXIS[\"x\",east,ORDER[1]]," + + "AXIS[\"y\",north,ORDER[2]]," + + "LENGTHUNIT[\"metre\",1]," + + "ID[\"LOCAL\",1]," + + "USAGE[SCOPE[\"unknown\"],AREA[\"Somewhere\"],BBOX[0,0,1,1]]]"; + + [Test] + public void ParseWkt2EngCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2EngCrs_LocalGrid); + Assert.That(model, Is.InstanceOf()); + + var eng = (Wkt2EngCrs)model; + Assert.That(eng.Datum.Name, Is.EqualTo("Local datum")); + Assert.That(eng.CoordinateSystem.Dimension, Is.EqualTo(2)); + + string wkt2 = eng.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("ENGCRS[\"Local engineering grid\"")); + Assert.That(wkt2, Does.Contain("EDATUM[\"Local datum\"")); + Assert.That(wkt2, Does.Contain("CS[cartesian,2]")); + Assert.That(wkt2, Does.Contain("ID[\"LOCAL\",\"1\"]")); + } + + [Test] + public void EngCrsParserSkipsUnknownMetadata() + { + var eng = (Wkt2EngCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2EngCrs_LocalGrid); + Assert.That(eng.CoordinateSystem.Axes, Has.Count.EqualTo(2)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2Nad83NewJerseyFtUsTests.cs b/test/ProjNet.Tests/WKT/WKT2Nad83NewJerseyFtUsTests.cs new file mode 100644 index 0000000..ffeaa83 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2Nad83NewJerseyFtUsTests.cs @@ -0,0 +1,67 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2Nad83NewJerseyFtUsTests + { + private const string Wkt2Nad83_NewJersey_FtUs = "PROJCRS[\"NAD83 / New Jersey (ftUS)\"," + + "BASEGEODCRS[\"NAD83\"," + + "DATUM[\"North American Datum 1983\"," + + "ELLIPSOID[\"GRS 1980\",6378137,298.257222101,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]]," + + "CONVERSION[\"SPCS83 New Jersey zone (US Survey feet)\"," + + "METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]]," + + "PARAMETER[\"Latitude of natural origin\",38.8333333333333,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]]," + + "PARAMETER[\"Longitude of natural origin\",-74.5,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]]," + + "PARAMETER[\"Scale factor at natural origin\",0.9999,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]]," + + "PARAMETER[\"False easting\",492125,LENGTHUNIT[\"US survey foot\",0.304800609601219],ID[\"EPSG\",8806]]," + + "PARAMETER[\"False northing\",0,LENGTHUNIT[\"US survey foot\",0.304800609601219],ID[\"EPSG\",8807]]]," + + "CS[Cartesian,2]," + + "AXIS[\"easting (X)\",east,ORDER[1],LENGTHUNIT[\"US survey foot\",0.304800609601219]]," + + "AXIS[\"northing (Y)\",north,ORDER[2],LENGTHUNIT[\"US survey foot\",0.304800609601219]]," + + "AREA[\"USA - New Jersey\"]," + + "BBOX[38.87,-75.6,41.36,-73.88]," + + "ID[\"EPSG\",3424]]"; + + [Test] + public void ParseWkt2ProjCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2Nad83_NewJersey_FtUs); + Assert.That(model, Is.InstanceOf()); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("PROJCRS[\"NAD83 / New Jersey (ftUS)\"")); + Assert.That(wkt2, Does.Contain("CONVERSION[\"SPCS83 New Jersey zone (US Survey feet)\"")); + Assert.That(wkt2, Does.Contain("METHOD[\"Transverse Mercator\"]")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"3424\"]")); + } + + [Test] + public void ParseWkt2ProjCrsToProjNetProducesProjectedCs() + { + var cs = new CoordinateSystemFactory().CreateFromWkt(Wkt2Nad83_NewJersey_FtUs); + Assert.That(cs, Is.InstanceOf()); + + var pcs = (ProjectedCoordinateSystem)cs; + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ParseWkt2ProjCrsModelToProjNetUsesWkt2Conversions() + { + var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2Nad83_NewJersey_FtUs); + var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(model); + + Assert.That(pcs, Is.Not.Null); + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2ParametricCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2ParametricCrsTests.cs new file mode 100644 index 0000000..87eeb04 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2ParametricCrsTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2ParametricCrsTests + { + private const string Wkt2ParametricCrs_Sigma = "PARAMETRICCRS[\"Sigma (dimensionless)\"," + + "PDATUM[\"Sigma datum\",ID[\"EPSG\",9999]]," + + "CS[parametric,1]," + + "AXIS[\"sigma\",up,ORDER[1]]," + + "PARAMETRICUNIT[\"unity\",1]," + + "ID[\"LOCAL\",2]," + + "USAGE[SCOPE[\"unknown\"],AREA[\"Somewhere\"],BBOX[0,0,1,1]]]"; + + [Test] + public void ParseWkt2ParametricCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2ParametricCrs_Sigma); + Assert.That(model, Is.InstanceOf()); + + var p = (Wkt2ParametricCrs)model; + Assert.That(p.Datum.Name, Is.EqualTo("Sigma datum")); + Assert.That(p.CoordinateSystem.Dimension, Is.EqualTo(1)); + + string wkt2 = p.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("PARAMETRICCRS[\"Sigma (dimensionless)\"")); + Assert.That(wkt2, Does.Contain("PDATUM[\"Sigma datum\"")); + Assert.That(wkt2, Does.Contain("CS[parametric,1]")); + Assert.That(wkt2, Does.Contain("PARAMETRICUNIT[\"unity\",1")); + Assert.That(wkt2, Does.Contain("ID[\"LOCAL\",\"2\"]")); + } + + [Test] + public void ParametricCrsParserSkipsUnknownMetadata() + { + var p = (Wkt2ParametricCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2ParametricCrs_Sigma); + Assert.That(p.CoordinateSystem.Axes, Has.Count.EqualTo(1)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs new file mode 100644 index 0000000..f4076c4 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs @@ -0,0 +1,80 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2ProjCrsTests + { + private const string Wkt2ProjCrs_Utm32N = "PROJCRS[\"WGS 84 / UTM zone 32N\"," + + "BASEGEOGCRS[\"WGS 84\"," + + "DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]]," + + "CONVERSION[\"UTM zone 32N\"," + + "METHOD[\"Transverse Mercator\"]," + + "PARAMETER[\"Latitude of natural origin\",0]," + + "PARAMETER[\"Longitude of natural origin\",9]," + + "PARAMETER[\"Scale factor at natural origin\",0.9996]," + + "PARAMETER[\"False easting\",500000]," + + "PARAMETER[\"False northing\",0]]," + + "CS[cartesian,2]," + + "AXIS[\"(E)\",east,ORDER[1]]," + + "AXIS[\"(N)\",north,ORDER[2]]," + + "LENGTHUNIT[\"metre\",1]," + + "ID[\"EPSG\",32632]]"; + + [Test] + public void ParseWkt2ProjCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2ProjCrs_Utm32N); + Assert.That(model, Is.InstanceOf()); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("PROJCRS[\"WGS 84 / UTM zone 32N\"")); + Assert.That(wkt2, Does.Contain("CONVERSION[\"UTM zone 32N\"")); + Assert.That(wkt2, Does.Contain("METHOD[\"Transverse Mercator\"]")); + Assert.That(wkt2, Does.Contain("CS[cartesian,2]")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"32632\"]")); + } + + [Test] + public void ParseWkt2ProjCrsToProjNetProducesProjectedCs() + { + var cs = new CoordinateSystemFactory().CreateFromWkt(Wkt2ProjCrs_Utm32N); + Assert.That(cs, Is.InstanceOf()); + + var pcs = (ProjectedCoordinateSystem)cs; + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ParseWkt2ProjCrsModelToProjNetUsesWkt2Conversions() + { + var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2ProjCrs_Utm32N); + var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(model); + + Assert.That(pcs, Is.Not.Null); + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ProjNetProjectedToWkt2ToProjNetRoundTripPreservesCoreParams() + { + var original = ProjectedCoordinateSystem.WGS84_UTM(32, true); + + var wkt2Model = Wkt2Conversions.FromProjNetProjectedCoordinateSystem(original); + var roundTripped = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(wkt2Model); + + Assert.That(roundTripped.GeographicCoordinateSystem.EqualParams(original.GeographicCoordinateSystem), Is.True); + Assert.That(roundTripped.LinearUnit.EqualParams(original.LinearUnit), Is.True); + Assert.That(roundTripped.Projection.ClassName, Is.EqualTo(original.Projection.ClassName)); + Assert.That(roundTripped.Projection.NumParameters, Is.EqualTo(original.Projection.NumParameters)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs b/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs new file mode 100644 index 0000000..7a6dcd7 --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs @@ -0,0 +1,70 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2Rgf93Lambert93Tests + { + private const string Wkt2Rgf93_Lambert93 = "PROJCRS[\"RGF93 v1 / Lambert-93\"," + + "BASEGEOGCRS[\"RGF93 v1\"," + + "DATUM[\"Reseau Geodesique Francais 1993 v1\"," + + "ELLIPSOID[\"GRS 1980\",6378137,298.257222101,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "ID[\"EPSG\",4171]]," + + "CONVERSION[\"Lambert-93\"," + + "METHOD[\"Lambert Conic Conformal (2SP)\",ID[\"EPSG\",9802]]," + + "PARAMETER[\"Latitude of false origin\",46.5,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8821]]," + + "PARAMETER[\"Longitude of false origin\",3,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8822]]," + + "PARAMETER[\"Latitude of 1st standard parallel\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8823]]," + + "PARAMETER[\"Latitude of 2nd standard parallel\",44,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8824]]," + + "PARAMETER[\"Easting at false origin\",700000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8826]]," + + "PARAMETER[\"Northing at false origin\",6600000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8827]]]," + + "CS[Cartesian,2]," + + "AXIS[\"easting (X)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]]," + + "AXIS[\"northing (Y)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]]," + + "USAGE[SCOPE[\"Engineering survey, topographic mapping.\"]," + + "AREA[\"France - onshore and offshore, mainland and Corsica (France métropolitaine including Corsica).\"]," + + "BBOX[41.15,-9.86,51.56,10.38]]," + + "ID[\"EPSG\",2154]]"; + + [Test] + public void ParseWkt2ProjCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2Rgf93_Lambert93); + Assert.That(model, Is.InstanceOf()); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("PROJCRS[\"RGF93 v1 / Lambert-93\"")); + Assert.That(wkt2, Does.Contain("CONVERSION[\"Lambert-93\"")); + Assert.That(wkt2, Does.Contain("METHOD[\"Lambert Conic Conformal (2SP)\"]")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"2154\"]")); + } + + [Test] + public void ParseWkt2ProjCrsToProjNetProducesProjectedCs() + { + var cs = new CoordinateSystemFactory().CreateFromWkt(Wkt2Rgf93_Lambert93); + Assert.That(cs, Is.InstanceOf()); + + var pcs = (ProjectedCoordinateSystem)cs; + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ParseWkt2ProjCrsModelToProjNetUsesWkt2Conversions() + { + var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2Rgf93_Lambert93); + var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(model); + + Assert.That(pcs, Is.Not.Null); + Assert.That(pcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(pcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(pcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs b/test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs new file mode 100644 index 0000000..41468bc --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2RoundTripTests + { + private const string Wkt2Wgs84_3D_Epsg4329 = "GEOGCRS[\"WGS 84 (3D)\"," + + "DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]]," + + "PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]," + + "CS[ellipsoidal,3]," + + "AXIS[\"geodetic latitude (Lat)\",north,ORDER[1]]," + + "AXIS[\"geodetic longitude (Long)\",east,ORDER[2]]," + + "AXIS[\"ellipsoidal height (h)\",up,ORDER[3],LENGTHUNIT[\"metre\",1]]," + + "ANGLEUNIT[\"degree\",0.0174532925199433]," + + "ID[\"EPSG\",4329]]"; + + [Test] + public void ParseWkt2ToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2Wgs84_3D_Epsg4329); + Assert.That(model, Is.InstanceOf()); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("GEOGCRS[\"WGS 84 (3D)\"")); + Assert.That(wkt2, Does.Contain("CS[ellipsoidal,3]")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"4329\"]")); + } + + [Test] + public void ParseWkt2ToProjNetNormalizesToLonLat2D() + { + var cs = new CoordinateSystemFactory().CreateFromWkt(Wkt2Wgs84_3D_Epsg4329); + Assert.That(cs, Is.InstanceOf()); + + var gcs = (GeographicCoordinateSystem)cs; + Assert.That(gcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(gcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(gcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ParseWkt2ModelToProjNetUsesWkt2ConversionsNormalization() + { + var model = (Wkt2GeogCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2Wgs84_3D_Epsg4329); + var gcs = Wkt2Conversions.ToProjNetGeographicCoordinateSystem(model); + + Assert.That(gcs, Is.Not.Null); + Assert.That(gcs.AxisInfo, Has.Count.EqualTo(2)); + Assert.That(gcs.AxisInfo[0].Orientation, Is.EqualTo(AxisOrientationEnum.East)); + Assert.That(gcs.AxisInfo[1].Orientation, Is.EqualTo(AxisOrientationEnum.North)); + } + + [Test] + public void ProjNetToWkt2ModelWritesWkt2() + { + var gcs = GeographicCoordinateSystem.WGS84; + var model = Wkt2Conversions.FromProjNetGeographicCoordinateSystem(gcs); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("GEOGCRS[\"WGS 84\"")); + Assert.That(wkt2, Does.Contain("CS[ellipsoidal,2]")); + } + + [Test] + public void ProjNetToWkt2ToProjNetRoundTripPreservesCoreParams() + { + var original = GeographicCoordinateSystem.WGS84; + + var wkt2Model = Wkt2Conversions.FromProjNetGeographicCoordinateSystem(original); + var roundTripped = Wkt2Conversions.ToProjNetGeographicCoordinateSystem(wkt2Model); + + Assert.That(roundTripped.EqualParams(original), Is.True); + } + } +} diff --git a/test/ProjNet.Tests/WKT/WKT2VertCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2VertCrsTests.cs new file mode 100644 index 0000000..37c2d3f --- /dev/null +++ b/test/ProjNet.Tests/WKT/WKT2VertCrsTests.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using ProjNet.CoordinateSystems.Wkt2; +using ProjNet.IO.CoordinateSystems; + +namespace ProjNET.Tests.WKT +{ + [TestFixture] + public class WKT2VertCrsTests + { + private const string Wkt2VertCrs_Egm96 = "VERTCRS[\"EGM96 height\"," + + "VDATUM[\"EGM96 geoid\",ID[\"EPSG\",5171]]," + + "CS[vertical,1]," + + "AXIS[\"gravity-related height (H)\",up,ORDER[1]]," + + "LENGTHUNIT[\"metre\",1]," + + "ID[\"EPSG\",5773]," + + "USAGE[SCOPE[\"unknown\"],AREA[\"World\"],BBOX[-90,-180,90,180]]]"; + + [Test] + public void ParseWkt2VertCrsToModelRoundTripsToWkt2() + { + var model = CoordinateSystemWkt2Reader.ParseCrs(Wkt2VertCrs_Egm96); + Assert.That(model, Is.InstanceOf()); + + string wkt2 = model.ToWkt2String(); + Assert.That(wkt2, Does.StartWith("VERTCRS[\"EGM96 height\"")); + Assert.That(wkt2, Does.Contain("CS[vertical,1]")); + Assert.That(wkt2, Does.Contain("AXIS[\"gravity-related height (H)\",up,ORDER[1]]")); + Assert.That(wkt2, Does.Contain("LENGTHUNIT[\"metre\",1")); + Assert.That(wkt2, Does.Contain("ID[\"EPSG\",\"5773\"]")); + } + + [Test] + public void VertCrsParserSkipsUnknownMetadata() + { + var model = (Wkt2VertCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2VertCrs_Egm96); + Assert.That(model.Datum.Name, Is.EqualTo("EGM96 geoid")); + Assert.That(model.CoordinateSystem.Dimension, Is.EqualTo(1)); + } + } +}