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)); + } + } +}