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/Wkt2BBox.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2BBox.cs
new file mode 100644
index 0000000..dbc4268
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2BBox.cs
@@ -0,0 +1,46 @@
+using System;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Represents a WKT2 bounding box with south, west, north, east bounds.
+ ///
+ [Serializable]
+ public sealed class Wkt2BBox
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The southern latitude bound.
+ /// The western longitude bound.
+ /// The northern latitude bound.
+ /// The eastern longitude bound.
+ public Wkt2BBox(double south, double west, double north, double east)
+ {
+ South = south;
+ West = west;
+ North = north;
+ East = east;
+ }
+
+ ///
+ /// Gets the southern latitude bound.
+ ///
+ public double South { get; }
+
+ ///
+ /// Gets the western longitude bound.
+ ///
+ public double West { get; }
+
+ ///
+ /// Gets the northern latitude bound.
+ ///
+ public double North { get; }
+
+ ///
+ /// Gets the eastern longitude bound.
+ ///
+ public double East { get; }
+ }
+}
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/Wkt2ConcatenatedOperation.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ConcatenatedOperation.cs
new file mode 100644
index 0000000..2d2eb3e
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ConcatenatedOperation.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Represents a WKT2 CONCATENATEDOPERATION element.
+ ///
+ [Serializable]
+ public sealed class Wkt2ConcatenatedOperation : Wkt2CrsBase
+ {
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The operation name.
+ public Wkt2ConcatenatedOperation(string name)
+ : base(name)
+ {
+ }
+
+ ///
+ /// Gets or sets the version text.
+ ///
+ public string Version { get; set; }
+
+ ///
+ /// Gets or sets the source CRS.
+ ///
+ public Wkt2CrsBase SourceCrs { get; set; }
+
+ ///
+ /// Gets or sets the target CRS.
+ ///
+ public Wkt2CrsBase TargetCrs { get; set; }
+
+ ///
+ /// Gets the ordered list of operation steps.
+ ///
+ public List Steps { get; } = new List();
+
+ ///
+ /// Gets or sets the operation accuracy.
+ ///
+ public double? OperationAccuracy { get; set; }
+
+ ///
+ 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..344ef65
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Conversions.cs
@@ -0,0 +1,308 @@
+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));
+ if (pcs.GeographicCoordinateSystem == null)
+ throw new ArgumentException("ProjectedCoordinateSystem.GeographicCoordinateSystem cannot be null.", nameof(pcs));
+ if (pcs.Projection == null)
+ throw new ArgumentException("ProjectedCoordinateSystem.Projection cannot be null.", nameof(pcs));
+
+ var baseCrs = pcs.GeographicCoordinateSystem.FromProjNetGeographicCoordinateSystem();
+
+ var conversion = new Wkt2Conversion(pcs.Projection.Name, MapProjNetToWkt2MethodName(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));
+ if (gcs.HorizontalDatum?.Ellipsoid == null)
+ throw new ArgumentException("GeographicCoordinateSystem.HorizontalDatum.Ellipsoid cannot be null.", 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 readonly Dictionary Wkt2ToProjNetMethodMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // Transverse Mercator variants
+ { "Transverse Mercator", "Transverse_Mercator" },
+ { "Transverse Mercator (South Orientated)", "Transverse_Mercator" },
+ { "Gauss-Kruger", "Transverse_Mercator" },
+
+ // Mercator variants
+ { "Mercator", "Mercator_1SP" },
+ { "Mercator (variant A)", "Mercator_1SP" },
+ { "Mercator (1SP)", "Mercator_1SP" },
+ { "Mercator (variant B)", "Mercator_2SP" },
+ { "Mercator (2SP)", "Mercator_2SP" },
+ { "Mercator Auxiliary Sphere", "Mercator_Auxiliary_Sphere" },
+ { "Popular Visualisation Pseudo Mercator", "Popular_Visualisation_Pseudo_Mercator" },
+
+ // Lambert variants
+ { "Lambert Conic Conformal (2SP)", "lambert_conformal_conic_2sp" },
+ { "Lambert Conic Conformal (1SP)", "Lambert_Conformal_Conic" },
+ { "Lambert Azimuthal Equal Area", "Lambert_Azimuthal_Equal_Area" },
+
+ // Albers
+ { "Albers Equal Area", "Albers_Conic_Equal_Area" },
+ { "Albers", "Albers" },
+
+ // Stereographic variants
+ { "Oblique Stereographic", "Oblique_Stereographic" },
+ { "Polar Stereographic (variant A)", "Polar_Stereographic" },
+ { "Polar Stereographic (variant B)", "Polar_Stereographic" },
+ { "Polar Stereographic", "Polar_Stereographic" },
+
+ // Other projections
+ { "Hotine Oblique Mercator (variant A)", "Hotine_Oblique_Mercator" },
+ { "Hotine Oblique Mercator (variant B)", "Hotine_Oblique_Mercator" },
+ { "Hotine Oblique Mercator", "Hotine_Oblique_Mercator" },
+ { "Oblique Mercator", "Oblique_Mercator" },
+ { "Cassini-Soldner", "Cassini_Soldner" },
+ { "Krovak", "Krovak" },
+ { "Krovak (North Orientated)", "Krovak" },
+ { "American Polyconic", "Polyconic" },
+ { "Polyconic", "Polyconic" },
+ { "Orthographic", "Orthographic" },
+ };
+
+ internal static string MapProjectionMethodName(string wkt2Method)
+ {
+ if (string.IsNullOrWhiteSpace(wkt2Method))
+ return "";
+
+ string m = wkt2Method.Trim();
+ return Wkt2ToProjNetMethodMap.TryGetValue(m, out string projNetName)
+ ? projNetName
+ : m;
+ }
+
+ private static readonly Dictionary ProjNetToWkt2MethodMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Transverse_Mercator", "Transverse Mercator" },
+ { "Mercator_1SP", "Mercator (variant A)" },
+ { "Mercator_2SP", "Mercator (variant B)" },
+ { "Mercator", "Mercator (variant A)" },
+ { "Mercator_Auxiliary_Sphere", "Mercator Auxiliary Sphere" },
+ { "Popular_Visualisation_Pseudo_Mercator", "Popular Visualisation Pseudo Mercator" },
+ { "Pseudo_Mercator", "Popular Visualisation Pseudo Mercator" },
+ { "Google_Mercator", "Popular Visualisation Pseudo Mercator" },
+ { "lambert_conformal_conic_2sp", "Lambert Conic Conformal (2SP)" },
+ { "Lambert_Conformal_Conic", "Lambert Conic Conformal (1SP)" },
+ { "Lambert_Conic_Conformal_(2SP)", "Lambert Conic Conformal (2SP)" },
+ { "Lambert_Azimuthal_Equal_Area", "Lambert Azimuthal Equal Area" },
+ { "Albers_Conic_Equal_Area", "Albers Equal Area" },
+ { "Albers", "Albers Equal Area" },
+ { "Oblique_Stereographic", "Oblique Stereographic" },
+ { "Polar_Stereographic", "Polar Stereographic (variant A)" },
+ { "Hotine_Oblique_Mercator", "Hotine Oblique Mercator (variant A)" },
+ { "Hotine_Oblique_Mercator_Azimuth_Center", "Hotine Oblique Mercator (variant A)" },
+ { "Oblique_Mercator", "Oblique Mercator" },
+ { "Cassini_Soldner", "Cassini-Soldner" },
+ { "Krovak", "Krovak" },
+ { "Polyconic", "American Polyconic" },
+ { "Orthographic", "Orthographic" },
+ { "Gauss_Kruger", "Transverse Mercator" },
+ };
+
+ internal static string MapProjNetToWkt2MethodName(string projNetMethod)
+ {
+ if (string.IsNullOrWhiteSpace(projNetMethod))
+ return "";
+
+ string m = projNetMethod.Trim();
+ return ProjNetToWkt2MethodMap.TryGetValue(m, out string wkt2Name)
+ ? wkt2Name
+ : m;
+ }
+ }
+}
diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateOperation.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateOperation.cs
new file mode 100644
index 0000000..ad0967e
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CoordinateOperation.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Represents a single coordinate operation used as a STEP in a concatenated operation.
+ ///
+ [Serializable]
+ public sealed class Wkt2CoordinateOperation
+ {
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The WKT2 keyword (CONVERSION, COORDINATEOPERATION, etc.).
+ /// The operation name.
+ public Wkt2CoordinateOperation(string keyword, string name)
+ {
+ Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword));
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+
+ ///
+ /// Gets the WKT2 keyword (CONVERSION, COORDINATEOPERATION, etc.).
+ ///
+ public string Keyword { get; }
+
+ ///
+ /// Gets the operation name.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets or sets the method name.
+ ///
+ public string Method { get; set; }
+
+ ///
+ /// Gets the operation parameters.
+ ///
+ public List Parameters { get; } = new List();
+
+ ///
+ /// Gets or sets the source CRS (for COORDINATEOPERATION).
+ ///
+ public Wkt2CrsBase SourceCrs { get; set; }
+
+ ///
+ /// Gets or sets the target CRS (for COORDINATEOPERATION).
+ ///
+ public Wkt2CrsBase TargetCrs { get; set; }
+
+ ///
+ /// Gets or sets the operation accuracy.
+ ///
+ public double? OperationAccuracy { get; set; }
+
+ ///
+ /// Gets or sets the identifier.
+ ///
+ public Wkt2Id Id { get; set; }
+
+ ///
+ /// Gets or sets a remark.
+ ///
+ public string Remark { get; set; }
+ }
+}
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..42a4675
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2CrsBase.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+
+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; }
+
+ ///
+ /// Gets the list of usage metadata for this CRS.
+ ///
+ public List Usages { get; } = new List();
+
+ ///
+ /// Serializes the model back to a WKT2 string.
+ ///
+ public abstract string ToWkt2String();
+ }
+}
diff --git a/src/ProjNet/CoordinateSystems/Wkt2/Wkt2DerivedGeogCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2DerivedGeogCrs.cs
new file mode 100644
index 0000000..a743b6a
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2DerivedGeogCrs.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Derived geographic CRS element as used in WKT2 (DERIVEDGEOGCRS).
+ ///
+ [Serializable]
+ public sealed class Wkt2DerivedGeogCrs : Wkt2CrsBase
+ {
+ ///
+ /// Initializes a new instance.
+ ///
+ /// CRS keyword (e.g. DERIVEDGEOGCRS).
+ /// CRS name.
+ /// Base geographic CRS.
+ /// Deriving conversion.
+ /// Coordinate system.
+ public Wkt2DerivedGeogCrs(string keyword, string name, Wkt2GeogCrs baseCrs, Wkt2Conversion derivingConversion, Wkt2CoordinateSystem coordinateSystem)
+ : base(name)
+ {
+ Keyword = keyword ?? throw new ArgumentNullException(nameof(keyword));
+ BaseCrs = baseCrs ?? throw new ArgumentNullException(nameof(baseCrs));
+ DerivingConversion = derivingConversion ?? throw new ArgumentNullException(nameof(derivingConversion));
+ 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 deriving conversion.
+ ///
+ public Wkt2Conversion DerivingConversion { get; }
+
+ ///
+ /// Gets the derived geographic 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/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..add4667
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2EngineeringDatum.cs
@@ -0,0 +1,47 @@
+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 the optional anchor description for this datum.
+ ///
+ public string Anchor { get; set; }
+
+ ///
+ /// 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..26ab2bd
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2GeodeticDatum.cs
@@ -0,0 +1,54 @@
+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 the optional anchor description for this datum.
+ ///
+ public string Anchor { get; set; }
+
+ ///
+ /// Gets or sets the optional frame reference epoch for dynamic datums.
+ ///
+ public double? FrameEpoch { get; set; }
+
+ ///
+ /// 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..0703136
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2ParametricDatum.cs
@@ -0,0 +1,47 @@
+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 the optional anchor description for this datum.
+ ///
+ public string Anchor { get; set; }
+
+ ///
+ /// 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/Wkt2TemporalDatum.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2TemporalDatum.cs
new file mode 100644
index 0000000..bd105e4
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2TemporalDatum.cs
@@ -0,0 +1,52 @@
+using System;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Temporal datum element as used in WKT2 (TDATUM / TIMEDATUM).
+ ///
+ [Serializable]
+ public sealed class Wkt2TemporalDatum
+ {
+ ///
+ /// Initializes a new instance.
+ ///
+ /// Datum keyword (e.g. TDATUM or TIMEDATUM).
+ /// Datum name.
+ public Wkt2TemporalDatum(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 the calendar type.
+ ///
+ public string Calendar { get; set; }
+
+ ///
+ /// Gets or sets the time origin.
+ ///
+ public string TimeOrigin { get; set; }
+
+ ///
+ /// 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/Wkt2TimeCrs.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2TimeCrs.cs
new file mode 100644
index 0000000..f8d683b
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2TimeCrs.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Temporal CRS element as used in WKT2 (TIMECRS).
+ ///
+ [Serializable]
+ public sealed class Wkt2TimeCrs : Wkt2CrsBase
+ {
+ ///
+ /// Initializes a new instance.
+ ///
+ /// CRS keyword (e.g. TIMECRS).
+ /// CRS name.
+ /// Temporal datum.
+ /// Coordinate system.
+ public Wkt2TimeCrs(string keyword, string name, Wkt2TemporalDatum 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 temporal datum.
+ ///
+ public Wkt2TemporalDatum Datum { get; }
+
+ ///
+ /// Gets the temporal 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/Wkt2Usage.cs b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Usage.cs
new file mode 100644
index 0000000..dce06c6
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2Usage.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace ProjNet.CoordinateSystems.Wkt2
+{
+ ///
+ /// Represents a WKT2 USAGE element containing scope, area, and bounding box metadata.
+ ///
+ [Serializable]
+ public sealed class Wkt2Usage
+ {
+ ///
+ /// Gets or sets the scope description.
+ ///
+ public string Scope { get; set; }
+
+ ///
+ /// Gets or sets the area description.
+ ///
+ public string Area { get; set; }
+
+ ///
+ /// Gets or sets the bounding box.
+ ///
+ public Wkt2BBox BBox { 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..07aed6a
--- /dev/null
+++ b/src/ProjNet/CoordinateSystems/Wkt2/Wkt2VerticalDatum.cs
@@ -0,0 +1,47 @@
+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 the optional anchor description for this datum.
+ ///
+ public string Anchor { get; set; }
+
+ ///
+ /// 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..fd5c880
--- /dev/null
+++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Reader.cs
@@ -0,0 +1,2062 @@
+using System;
+using System.Collections.Generic;
+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);
+ case "TIMECRS":
+ return ReadTimeCrs(rootKeyword, tokenizer);
+ case "DERIVEDGEOGCRS":
+ return ReadDerivedGeogCrs(rootKeyword, tokenizer);
+ case "CONCATENATEDOPERATION":
+ return ReadConcatenatedOperation(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;
+ var usages = new List();
+
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ return crs;
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2EngineeringDatum ReadEngineeringDatum(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // EDATUM/DATUM["name", ANCHOR[...], ID[...], REMARK[...]]
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ Wkt2Id id = null;
+ string remark = null;
+ string anchor = null;
+
+ tokenizer.NextToken();
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "ANCHOR":
+ var anchorBracket = tokenizer.ReadOpener();
+ anchor = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(anchorBracket);
+ break;
+ 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, Anchor = anchor };
+ 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;
+ var usages = new List();
+
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ return crs;
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2ParametricDatum ReadParametricDatum(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // PDATUM/DATUM["name", ANCHOR[...], ID[...], REMARK[...]]
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ Wkt2Id id = null;
+ string remark = null;
+ string anchor = null;
+
+ tokenizer.NextToken();
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "ANCHOR":
+ var anchorBracket = tokenizer.ReadOpener();
+ anchor = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(anchorBracket);
+ break;
+ 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, Anchor = anchor };
+ 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;
+ var usages = new List();
+
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ 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 "ENGCRS":
+ case "ENGINEERINGCRS":
+ crs = ReadEngCrs(element, tokenizer);
+ break;
+ case "PARAMETRICCRS":
+ crs = ReadParametricCrs(element, tokenizer);
+ break;
+ case "TIMECRS":
+ crs = ReadTimeCrs(element, tokenizer);
+ break;
+ case "DERIVEDGEOGCRS":
+ crs = ReadDerivedGeogCrs(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 parameters = new System.Collections.Generic.List();
+ Wkt2Id id = null;
+ string remark = null;
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "METHOD":
+ methodName = ReadMethodName(tokenizer);
+ break;
+ case "PARAMETER":
+ parameters.Add(ReadParameter(tokenizer));
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+ if (string.IsNullOrWhiteSpace(methodName))
+ throw new ArgumentException("ABRIDGEDTRANSFORMATION is missing METHOD.");
+ var transform = new Wkt2AbridgedTransformation(name, methodName);
+ foreach (var p in parameters)
+ transform.Parameters.Add(p);
+ transform.Id = id;
+ transform.Remark = remark;
+ 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;
+ var usages = new List();
+
+ 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 "TIMECRS":
+ components.Add(ReadTimeCrs(element, tokenizer));
+ break;
+ case "DERIVEDGEOGCRS":
+ components.Add(ReadDerivedGeogCrs(element, tokenizer));
+ break;
+ case "ENGCRS":
+ case "ENGINEERINGCRS":
+ components.Add(ReadEngCrs(element, tokenizer));
+ break;
+ case "PARAMETRICCRS":
+ components.Add(ReadParametricCrs(element, tokenizer));
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ 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;
+ var usages = new List();
+
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ return crs;
+
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2VerticalDatum ReadVerticalDatum(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // VDATUM/DATUM["name", ANCHOR[...], ID[...], REMARK[...]]
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ Wkt2Id id = null;
+ string remark = null;
+ string anchor = null;
+
+ tokenizer.NextToken();
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "ANCHOR":
+ var anchorBracket = tokenizer.ReadOpener();
+ anchor = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(anchorBracket);
+ break;
+ 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, Anchor = anchor };
+ 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;
+ var usages = new List();
+
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ 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":
+ case "DYNAMICDATUM":
+ 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 parameters = new System.Collections.Generic.List();
+ Wkt2Id id = null;
+ string remark = null;
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "METHOD":
+ methodName = ReadMethodName(tokenizer);
+ break;
+ case "PARAMETER":
+ parameters.Add(ReadParameter(tokenizer));
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+ if (string.IsNullOrWhiteSpace(methodName))
+ throw new ArgumentException("CONVERSION is missing METHOD.");
+ var conversion = new Wkt2Conversion(name, methodName);
+ foreach (var p in parameters)
+ conversion.Parameters.Add(p);
+ conversion.Id = id;
+ conversion.Remark = remark;
+ return conversion;
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2TemporalDatum ReadTemporalDatum(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // TDATUM/TIMEDATUM["name", CALENDAR[...], TIMEORIGIN[...], ID[...], REMARK[...]]
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ Wkt2Id id = null;
+ string remark = null;
+ string calendar = null;
+ string timeOrigin = null;
+
+ tokenizer.NextToken();
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "CALENDAR":
+ var calBracket = tokenizer.ReadOpener();
+ calendar = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(calBracket);
+ break;
+ case "TIMEORIGIN":
+ var origBracket = tokenizer.ReadOpener();
+ timeOrigin = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(origBracket);
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+ return new Wkt2TemporalDatum(keyword, name)
+ {
+ Calendar = calendar,
+ TimeOrigin = timeOrigin,
+ Id = id,
+ Remark = remark
+ };
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2TimeCrs ReadTimeCrs(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // TIMECRS["name", TDATUM/TIMEDATUM[...], CS[...], AXIS..., TIMEUNIT..., ID...]
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+
+ Wkt2TemporalDatum datum = null;
+ Wkt2CoordinateSystem cs = null;
+ Wkt2Id id = null;
+ string remark = null;
+ var usages = new List();
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "TDATUM":
+ case "TIMEDATUM":
+ datum = ReadTemporalDatum(element, tokenizer);
+ break;
+ case "CS":
+ cs = ReadCoordinateSystem(tokenizer);
+ break;
+ case "AXIS":
+ if (cs == null)
+ cs = new Wkt2CoordinateSystem("temporal", 1);
+ cs.Axes.Add(ReadAxis(tokenizer));
+ break;
+ case "TIMEUNIT":
+ case "UNIT":
+ if (cs == null)
+ cs = new Wkt2CoordinateSystem("temporal", 1);
+ cs.Unit = ReadUnit(element, tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+
+ if (datum == null)
+ throw new ArgumentException("TIMECRS is missing TDATUM/TIMEDATUM.");
+ if (cs == null)
+ cs = new Wkt2CoordinateSystem("temporal", 1);
+
+ var crs = new Wkt2TimeCrs(keyword, name, datum, cs)
+ {
+ Id = id,
+ Remark = remark
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ return crs;
+
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2DerivedGeogCrs ReadDerivedGeogCrs(string keyword, WktStreamTokenizer tokenizer)
+ {
+ // DERIVEDGEOGCRS["name", BASEGEOGCRS[...], DERIVINGCONVERSION[...], 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;
+ var usages = new List();
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "BASEGEOGCRS":
+ case "BASEGEODCRS":
+ baseCrs = ReadBaseGeogCrs(tokenizer);
+ break;
+ case "DERIVINGCONVERSION":
+ conversion = ReadConversion(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 "SCALEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+
+ if (baseCrs == null)
+ throw new ArgumentException("DERIVEDGEOGCRS is missing BASEGEOGCRS.");
+ if (conversion == null)
+ throw new ArgumentException("DERIVEDGEOGCRS is missing DERIVINGCONVERSION.");
+ if (cs == null)
+ cs = new Wkt2CoordinateSystem("ellipsoidal", 2);
+
+ var crs = new Wkt2DerivedGeogCrs(keyword, name, baseCrs, conversion, cs)
+ {
+ Id = id,
+ Remark = remark
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ return crs;
+
+ 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;
+ var usages = new List();
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "DATUM":
+ case "TRF":
+ case "GEODETICDATUM":
+ case "DYNAMICDATUM":
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ 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
+ };
+ foreach (var u in usages)
+ crs.Usages.Add(u);
+ 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;
+ string anchor = null;
+ double? frameEpoch = null;
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "ELLIPSOID":
+ case "SPHEROID":
+ ellipsoid = ReadEllipsoid(tokenizer);
+ break;
+ case "ANCHOR":
+ var anchorBracket = tokenizer.ReadOpener();
+ anchor = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(anchorBracket);
+ break;
+ case "FRAMEEPOCH":
+ var epochBracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ frameEpoch = tokenizer.GetNumericValue();
+ tokenizer.ReadCloser(epochBracket);
+ 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, Anchor = anchor, FrameEpoch = frameEpoch };
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 "SCALEUNIT":
+ case "TIMEUNIT":
+ 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 Wkt2BBox ReadBBox(WktStreamTokenizer tokenizer)
+ {
+ var bracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ double south = tokenizer.GetNumericValue();
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+ double west = tokenizer.GetNumericValue();
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+ double north = tokenizer.GetNumericValue();
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+ double east = tokenizer.GetNumericValue();
+ tokenizer.ReadCloser(bracket);
+ return new Wkt2BBox(south, west, north, east);
+ }
+
+ private static Wkt2Usage ReadUsage(WktStreamTokenizer tokenizer)
+ {
+ var bracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ var usage = new Wkt2Usage();
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "SCOPE":
+ var scopeBracket = tokenizer.ReadOpener();
+ usage.Scope = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(scopeBracket);
+ break;
+ case "AREA":
+ var areaBracket = tokenizer.ReadOpener();
+ usage.Area = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(areaBracket);
+ break;
+ case "BBOX":
+ usage.BBox = ReadBBox(tokenizer);
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+ return usage;
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+ tokenizer.NextToken();
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ private static Wkt2ConcatenatedOperation ReadConcatenatedOperation(string keyword, WktStreamTokenizer tokenizer)
+ {
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+
+ string version = null;
+ Wkt2CrsBase sourceCrs = null;
+ Wkt2CrsBase targetCrs = null;
+ var steps = new List();
+ double? operationAccuracy = null;
+ Wkt2Id id = null;
+ string remark = null;
+ var usages = new List();
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "VERSION":
+ {
+ var vBracket = tokenizer.ReadOpener();
+ version = tokenizer.ReadDoubleQuotedWord();
+ tokenizer.ReadCloser(vBracket);
+ }
+ break;
+ case "SOURCECRS":
+ sourceCrs = ReadBoundCrsChildCrs(tokenizer);
+ break;
+ case "TARGETCRS":
+ targetCrs = ReadBoundCrsChildCrs(tokenizer);
+ break;
+ case "STEP":
+ steps.Add(ReadStepElement(tokenizer));
+ break;
+ case "OPERATIONACCURACY":
+ {
+ var aBracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ operationAccuracy = tokenizer.GetNumericValue();
+ tokenizer.ReadCloser(aBracket);
+ }
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "USAGE":
+ usages.Add(ReadUsage(tokenizer));
+ break;
+ case "SCOPE":
+ {
+ var scopeBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Scope = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(scopeBracket);
+ usages.Add(u);
+ }
+ break;
+ case "AREA":
+ {
+ var areaBracket = tokenizer.ReadOpener();
+ var u = new Wkt2Usage { Area = tokenizer.ReadDoubleQuotedWord() };
+ tokenizer.ReadCloser(areaBracket);
+ usages.Add(u);
+ }
+ break;
+ case "BBOX":
+ {
+ var u = new Wkt2Usage { BBox = ReadBBox(tokenizer) };
+ usages.Add(u);
+ }
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+
+ if (sourceCrs == null)
+ throw new ArgumentException("CONCATENATEDOPERATION is missing SOURCECRS.");
+ if (targetCrs == null)
+ throw new ArgumentException("CONCATENATEDOPERATION is missing TARGETCRS.");
+ if (steps.Count < 2)
+ throw new ArgumentException("CONCATENATEDOPERATION must have at least two STEP elements.");
+
+ var op = new Wkt2ConcatenatedOperation(name)
+ {
+ Version = version,
+ SourceCrs = sourceCrs,
+ TargetCrs = targetCrs,
+ OperationAccuracy = operationAccuracy,
+ Id = id,
+ Remark = remark
+ };
+ foreach (var s in steps)
+ op.Steps.Add(s);
+ foreach (var u in usages)
+ op.Usages.Add(u);
+ return op;
+
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+
+ tokenizer.NextToken();
+ }
+ }
+
+ private static Wkt2CoordinateOperation ReadStepElement(WktStreamTokenizer tokenizer)
+ {
+ var stepBracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ string childKeyword = tokenizer.GetStringValue();
+
+ var op = ReadCoordinateOperationStep(childKeyword, tokenizer);
+
+ tokenizer.ReadCloser(stepBracket);
+ return op;
+ }
+
+ private static Wkt2CoordinateOperation ReadCoordinateOperationStep(string keyword, WktStreamTokenizer tokenizer)
+ {
+ var bracket = tokenizer.ReadOpener();
+ string name = tokenizer.ReadDoubleQuotedWord();
+
+ tokenizer.ReadToken(",");
+ tokenizer.NextToken();
+
+ string method = null;
+ var parameters = new List();
+ Wkt2CrsBase sourceCrs = null;
+ Wkt2CrsBase targetCrs = null;
+ double? operationAccuracy = null;
+ Wkt2Id id = null;
+ string remark = null;
+
+ while (true)
+ {
+ string element = tokenizer.GetStringValue();
+ switch (element.ToUpperInvariant())
+ {
+ case "METHOD":
+ method = ReadMethodName(tokenizer);
+ break;
+ case "PARAMETER":
+ parameters.Add(ReadParameter(tokenizer));
+ break;
+ case "SOURCECRS":
+ sourceCrs = ReadBoundCrsChildCrs(tokenizer);
+ break;
+ case "TARGETCRS":
+ targetCrs = ReadBoundCrsChildCrs(tokenizer);
+ break;
+ case "OPERATIONACCURACY":
+ {
+ var aBracket = tokenizer.ReadOpener();
+ tokenizer.NextToken();
+ operationAccuracy = tokenizer.GetNumericValue();
+ tokenizer.ReadCloser(aBracket);
+ }
+ break;
+ case "ID":
+ id = ReadId(tokenizer);
+ break;
+ case "REMARK":
+ remark = ReadRemark(tokenizer);
+ break;
+ case ",":
+ break;
+ case "]":
+ case ")":
+ tokenizer.CheckCloser(bracket);
+
+ var op = new Wkt2CoordinateOperation(keyword, name)
+ {
+ Method = method,
+ SourceCrs = sourceCrs,
+ TargetCrs = targetCrs,
+ OperationAccuracy = operationAccuracy,
+ Id = id,
+ Remark = remark
+ };
+ foreach (var p in parameters)
+ op.Parameters.Add(p);
+ return op;
+
+ default:
+ SkipUnknownElement(tokenizer);
+ break;
+ }
+
+ tokenizer.NextToken();
+ }
+ }
+ }
+}
diff --git a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs
new file mode 100644
index 0000000..4031291
--- /dev/null
+++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWkt2Writer.cs
@@ -0,0 +1,914 @@
+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);
+ if (crs is Wkt2TimeCrs time)
+ return WriteTimeCrs(time);
+ if (crs is Wkt2DerivedGeogCrs derivedGeog)
+ return WriteDerivedGeogCrs(derivedGeog);
+ if (crs is Wkt2ConcatenatedOperation concat)
+ return WriteConcatenatedOperation(concat);
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 (!string.IsNullOrWhiteSpace(datum.Anchor))
+ sb.Append($",ANCHOR[\"{EscapeQuotedText(datum.Anchor)}\"]");
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 (!string.IsNullOrWhiteSpace(datum.Anchor))
+ sb.Append($",ANCHOR[\"{EscapeQuotedText(datum.Anchor)}\"]");
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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));
+ }
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 (!string.IsNullOrWhiteSpace(datum.Anchor))
+ sb.Append($",ANCHOR[\"{EscapeQuotedText(datum.Anchor)}\"]");
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 (!string.IsNullOrWhiteSpace(datum.Anchor))
+ sb.Append($",ANCHOR[\"{EscapeQuotedText(datum.Anchor)}\"]");
+
+ if (datum.FrameEpoch.HasValue)
+ sb.Append($",FRAMEEPOCH[{datum.FrameEpoch.Value.ToString("R", CultureInfo.InvariantCulture)}]");
+
+ 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 WriteBBox(Wkt2BBox bbox)
+ {
+ return $"BBOX[{bbox.South.ToString("R", CultureInfo.InvariantCulture)},{bbox.West.ToString("R", CultureInfo.InvariantCulture)},{bbox.North.ToString("R", CultureInfo.InvariantCulture)},{bbox.East.ToString("R", CultureInfo.InvariantCulture)}]";
+ }
+
+ private static string WriteUsage(Wkt2Usage usage)
+ {
+ var sb = new StringBuilder("USAGE[");
+ var parts = new System.Collections.Generic.List();
+ if (!string.IsNullOrWhiteSpace(usage.Scope))
+ parts.Add($"SCOPE[\"{EscapeQuotedText(usage.Scope)}\"]");
+ if (!string.IsNullOrWhiteSpace(usage.Area))
+ parts.Add($"AREA[\"{EscapeQuotedText(usage.Area)}\"]");
+ if (usage.BBox != null)
+ parts.Add(WriteBBox(usage.BBox));
+ sb.Append(string.Join(",", parts));
+ sb.Append(']');
+ return sb.ToString();
+ }
+
+ private static string WriteTemporalDatum(Wkt2TemporalDatum datum)
+ {
+ var sb = new StringBuilder();
+ sb.Append(datum.Keyword.ToUpperInvariant());
+ sb.Append("[\"");
+ sb.Append(EscapeQuotedText(datum.Name));
+ sb.Append("\"");
+
+ if (!string.IsNullOrWhiteSpace(datum.Calendar))
+ sb.Append($",CALENDAR[\"{EscapeQuotedText(datum.Calendar)}\"]");
+
+ if (!string.IsNullOrWhiteSpace(datum.TimeOrigin))
+ sb.Append($",TIMEORIGIN[\"{EscapeQuotedText(datum.TimeOrigin)}\"]");
+
+ 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 WriteTimeCrs(Wkt2TimeCrs crs)
+ {
+ var sb = new StringBuilder();
+ sb.Append(crs.Keyword.ToUpperInvariant());
+ sb.Append("[\"");
+ sb.Append(EscapeQuotedText(crs.Name));
+ sb.Append("\",");
+
+ sb.Append(WriteTemporalDatum(crs.Datum));
+ sb.Append(',');
+ sb.Append(WriteCs(crs.CoordinateSystem));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 WriteDerivedGeogCrs(Wkt2DerivedGeogCrs crs)
+ {
+ var sb = new StringBuilder();
+ sb.Append(crs.Keyword.ToUpperInvariant());
+ sb.Append("[\"");
+ sb.Append(EscapeQuotedText(crs.Name));
+ sb.Append("\",");
+
+ // Write base CRS wrapped in BASEGEOGCRS keyword.
+ sb.Append(WriteGeogCrs(crs.BaseCrs));
+ sb.Append(',');
+
+ // Write deriving conversion with DERIVINGCONVERSION keyword.
+ sb.Append("DERIVINGCONVERSION[\"");
+ sb.Append(EscapeQuotedText(crs.DerivingConversion.Name));
+ sb.Append("\",");
+ sb.Append("METHOD[\"");
+ sb.Append(EscapeQuotedText(crs.DerivingConversion.MethodName));
+ sb.Append("\"]");
+ foreach (var p in crs.DerivingConversion.Parameters)
+ {
+ sb.Append(',');
+ sb.Append(WriteParameter(p));
+ }
+ if (crs.DerivingConversion.Id != null)
+ {
+ sb.Append(',');
+ sb.Append(WriteId(crs.DerivingConversion.Id));
+ }
+ if (!string.IsNullOrWhiteSpace(crs.DerivingConversion.Remark))
+ {
+ sb.Append(",REMARK[\"");
+ sb.Append(EscapeQuotedText(crs.DerivingConversion.Remark));
+ sb.Append("\"]");
+ }
+ sb.Append(']');
+
+ sb.Append(',');
+ sb.Append(WriteCs(crs.CoordinateSystem));
+
+ foreach (var usage in crs.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ 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 WriteConcatenatedOperation(Wkt2ConcatenatedOperation op)
+ {
+ var sb = new StringBuilder();
+ sb.Append("CONCATENATEDOPERATION[\"");
+ sb.Append(EscapeQuotedText(op.Name));
+ sb.Append("\"");
+
+ if (!string.IsNullOrWhiteSpace(op.Version))
+ {
+ sb.Append(",VERSION[\"");
+ sb.Append(EscapeQuotedText(op.Version));
+ sb.Append("\"]");
+ }
+
+ if (op.SourceCrs != null)
+ {
+ sb.Append(",SOURCECRS[");
+ sb.Append(Write(op.SourceCrs));
+ sb.Append(']');
+ }
+
+ if (op.TargetCrs != null)
+ {
+ sb.Append(",TARGETCRS[");
+ sb.Append(Write(op.TargetCrs));
+ sb.Append(']');
+ }
+
+ foreach (var step in op.Steps)
+ {
+ sb.Append(",STEP[");
+ sb.Append(WriteCoordinateOperation(step));
+ sb.Append(']');
+ }
+
+ if (op.OperationAccuracy.HasValue)
+ {
+ sb.Append(",OPERATIONACCURACY[");
+ sb.Append(op.OperationAccuracy.Value.ToString(CultureInfo.InvariantCulture));
+ sb.Append(']');
+ }
+
+ foreach (var usage in op.Usages)
+ sb.Append($",{WriteUsage(usage)}");
+
+ if (op.Id != null)
+ {
+ sb.Append(',');
+ sb.Append(WriteId(op.Id));
+ }
+
+ if (!string.IsNullOrWhiteSpace(op.Remark))
+ {
+ sb.Append(",REMARK[\"");
+ sb.Append(EscapeQuotedText(op.Remark));
+ sb.Append("\"]");
+ }
+
+ sb.Append(']');
+ return sb.ToString();
+ }
+
+ private static string WriteCoordinateOperation(Wkt2CoordinateOperation op)
+ {
+ var sb = new StringBuilder();
+ sb.Append(op.Keyword.ToUpperInvariant());
+ sb.Append("[\"");
+ sb.Append(EscapeQuotedText(op.Name));
+ sb.Append("\"");
+
+ if (op.SourceCrs != null)
+ {
+ sb.Append(",SOURCECRS[");
+ sb.Append(Write(op.SourceCrs));
+ sb.Append(']');
+ }
+
+ if (op.TargetCrs != null)
+ {
+ sb.Append(",TARGETCRS[");
+ sb.Append(Write(op.TargetCrs));
+ sb.Append(']');
+ }
+
+ if (!string.IsNullOrWhiteSpace(op.Method))
+ {
+ sb.Append(",METHOD[\"");
+ sb.Append(EscapeQuotedText(op.Method));
+ sb.Append("\"]");
+ }
+
+ foreach (var p in op.Parameters)
+ {
+ sb.Append(',');
+ sb.Append(WriteParameter(p));
+ }
+
+ if (op.OperationAccuracy.HasValue)
+ {
+ sb.Append(",OPERATIONACCURACY[");
+ sb.Append(op.OperationAccuracy.Value.ToString(CultureInfo.InvariantCulture));
+ sb.Append(']');
+ }
+
+ if (op.Id != null)
+ {
+ sb.Append(',');
+ sb.Append(WriteId(op.Id));
+ }
+
+ if (!string.IsNullOrWhiteSpace(op.Remark))
+ {
+ sb.Append(",REMARK[\"");
+ sb.Append(EscapeQuotedText(op.Remark));
+ 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..396a06a 100644
--- a/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs
+++ b/src/ProjNet/IO/CoordinateSystems/CoordinateSystemWktReader.cs
@@ -68,6 +68,31 @@ 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 "TIMECRS":
+ return CoordinateSystemWkt2Reader.Parse(wkt);
+ case "DERIVEDGEOGCRS":
+ return CoordinateSystemWkt2Reader.Parse(wkt);
+ case "CONCATENATEDOPERATION":
+ 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/WKT2ConcatenatedOperationTests.cs b/test/ProjNet.Tests/WKT/WKT2ConcatenatedOperationTests.cs
new file mode 100644
index 0000000..13b1e91
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2ConcatenatedOperationTests.cs
@@ -0,0 +1,268 @@
+using System;
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2ConcatenatedOperationTests
+{
+ private const string Wgs84GeogCrs =
+ "GEOGCRS[\"WGS 84\"," +
+ "DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]]," +
+ "PRIMEM[\"Greenwich\",0]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ private const string Nad83GeogCrs =
+ "GEOGCRS[\"NAD83\"," +
+ "DATUM[\"North American Datum 1983\",ELLIPSOID[\"GRS 1980\",6378137,298.257222101]]," +
+ "PRIMEM[\"Greenwich\",0]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ [Test]
+ public void Parse_ConcatenatedOperation_Basic()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"UTM zone 28N to JHS height\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"UTM zone 28N\"," +
+ "METHOD[\"Transverse Mercator\"]," +
+ "PARAMETER[\"Latitude of natural origin\",0]," +
+ "PARAMETER[\"Longitude of natural origin\",-15]," +
+ "PARAMETER[\"Scale factor at natural origin\",0.9996]," +
+ "PARAMETER[\"False easting\",500000]," +
+ "PARAMETER[\"False northing\",0]]]," +
+ "STEP[CONVERSION[\"Northing change\",METHOD[\"Height Depth Reversal\"]]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.Name, Is.EqualTo("UTM zone 28N to JHS height"));
+ Assert.That(concat.SourceCrs, Is.Not.Null);
+ Assert.That(concat.TargetCrs, Is.Not.Null);
+
+ Assert.That(concat.Steps, Has.Count.EqualTo(2));
+
+ Assert.That(concat.Steps[0].Name, Is.EqualTo("UTM zone 28N"));
+ Assert.That(concat.Steps[0].Method, Is.EqualTo("Transverse Mercator"));
+ Assert.That(concat.Steps[0].Parameters, Has.Count.EqualTo(5));
+ Assert.That(concat.Steps[0].Parameters[0].Name, Is.EqualTo("Latitude of natural origin"));
+ Assert.That(concat.Steps[0].Parameters[0].Value, Is.EqualTo(0));
+ Assert.That(concat.Steps[0].Parameters[1].Name, Is.EqualTo("Longitude of natural origin"));
+ Assert.That(concat.Steps[0].Parameters[1].Value, Is.EqualTo(-15));
+ Assert.That(concat.Steps[0].Parameters[2].Name, Is.EqualTo("Scale factor at natural origin"));
+ Assert.That(concat.Steps[0].Parameters[2].Value, Is.EqualTo(0.9996));
+ Assert.That(concat.Steps[0].Parameters[3].Name, Is.EqualTo("False easting"));
+ Assert.That(concat.Steps[0].Parameters[3].Value, Is.EqualTo(500000));
+ Assert.That(concat.Steps[0].Parameters[4].Name, Is.EqualTo("False northing"));
+ Assert.That(concat.Steps[0].Parameters[4].Value, Is.EqualTo(0));
+
+ Assert.That(concat.Steps[1].Name, Is.EqualTo("Northing change"));
+ Assert.That(concat.Steps[1].Method, Is.EqualTo("Height Depth Reversal"));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_WithVersion()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Op with version\"," +
+ "VERSION[\"1.0\"]," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.Version, Is.EqualTo("1.0"));
+ Assert.That(concat.Steps, Has.Count.EqualTo(2));
+ Assert.That(concat.Steps[0].Name, Is.EqualTo("Step 1"));
+ Assert.That(concat.Steps[1].Name, Is.EqualTo("Step 2"));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_WithOperationAccuracy()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Accurate op\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]," +
+ "OPERATIONACCURACY[0.1]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.OperationAccuracy, Is.Not.Null);
+ Assert.That(concat.OperationAccuracy.Value, Is.EqualTo(0.1).Within(1e-10));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_WithIdAndRemark()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Op with id\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]," +
+ "ID[\"EPSG\",\"9999\"]," +
+ "REMARK[\"Test concatenated operation\"]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.Id, Is.Not.Null);
+ Assert.That(concat.Id.Authority, Is.EqualTo("EPSG"));
+ Assert.That(concat.Id.Code, Is.EqualTo("9999"));
+ Assert.That(concat.Remark, Is.EqualTo("Test concatenated operation"));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_WithUsage()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Op with usage\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]," +
+ "USAGE[SCOPE[\"Navigation\"],AREA[\"World\"],BBOX[-90,-180,90,180]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.Usages, Has.Count.GreaterThan(0));
+ Assert.That(concat.Usages[0].Scope, Is.EqualTo("Navigation"));
+ Assert.That(concat.Usages[0].Area, Is.EqualTo("World"));
+ Assert.That(concat.Usages[0].BBox, Is.Not.Null);
+ Assert.That(concat.Usages[0].BBox.South, Is.EqualTo(-90));
+ Assert.That(concat.Usages[0].BBox.West, Is.EqualTo(-180));
+ Assert.That(concat.Usages[0].BBox.North, Is.EqualTo(90));
+ Assert.That(concat.Usages[0].BBox.East, Is.EqualTo(180));
+ }
+
+ [Test]
+ public void RoundTrip_ConcatenatedOperation()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"UTM zone 28N to JHS height\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"UTM zone 28N\"," +
+ "METHOD[\"Transverse Mercator\"]," +
+ "PARAMETER[\"Latitude of natural origin\",0]," +
+ "PARAMETER[\"Longitude of natural origin\",-15]," +
+ "PARAMETER[\"Scale factor at natural origin\",0.9996]," +
+ "PARAMETER[\"False easting\",500000]," +
+ "PARAMETER[\"False northing\",0]]]," +
+ "STEP[CONVERSION[\"Northing change\",METHOD[\"Height Depth Reversal\"]]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ string output = crs.ToWkt2String();
+
+ var crs2 = CoordinateSystemWkt2Reader.ParseCrs(output);
+ Assert.That(crs2, Is.InstanceOf());
+
+ var concat2 = (Wkt2ConcatenatedOperation)crs2;
+ Assert.That(concat2.Name, Is.EqualTo("UTM zone 28N to JHS height"));
+ Assert.That(concat2.SourceCrs, Is.Not.Null);
+ Assert.That(concat2.TargetCrs, Is.Not.Null);
+ Assert.That(concat2.Steps, Has.Count.EqualTo(2));
+ Assert.That(concat2.Steps[0].Name, Is.EqualTo("UTM zone 28N"));
+ Assert.That(concat2.Steps[0].Method, Is.EqualTo("Transverse Mercator"));
+ Assert.That(concat2.Steps[0].Parameters, Has.Count.EqualTo(5));
+ Assert.That(concat2.Steps[0].Parameters[0].Value, Is.EqualTo(0));
+ Assert.That(concat2.Steps[0].Parameters[1].Value, Is.EqualTo(-15));
+ Assert.That(concat2.Steps[0].Parameters[2].Value, Is.EqualTo(0.9996));
+ Assert.That(concat2.Steps[0].Parameters[3].Value, Is.EqualTo(500000));
+ Assert.That(concat2.Steps[0].Parameters[4].Value, Is.EqualTo(0));
+ Assert.That(concat2.Steps[1].Name, Is.EqualTo("Northing change"));
+ Assert.That(concat2.Steps[1].Method, Is.EqualTo("Height Depth Reversal"));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_MissingSourceCrs_Throws()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Bad\"," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_MissingTargetCrs_Throws()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Bad\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Step 1\",METHOD[\"Identity\"]]]," +
+ "STEP[CONVERSION[\"Step 2\",METHOD[\"Identity\"]]]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_OnlyOneStep_Throws()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Bad\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Wgs84GeogCrs + "]," +
+ "STEP[CONVERSION[\"Only step\",METHOD[\"Identity\"]]]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_ConcatenatedOperation_WithCoordinateOperation()
+ {
+ const string wkt =
+ "CONCATENATEDOPERATION[\"Complex op\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Nad83GeogCrs + "]," +
+ "STEP[COORDINATEOPERATION[\"WGS 84 to NAD83 (1)\"," +
+ "SOURCECRS[" + Wgs84GeogCrs + "]," +
+ "TARGETCRS[" + Nad83GeogCrs + "]," +
+ "METHOD[\"Geocentric translations\"]," +
+ "PARAMETER[\"X-axis translation\",0]," +
+ "PARAMETER[\"Y-axis translation\",0]," +
+ "PARAMETER[\"Z-axis translation\",0]]]," +
+ "STEP[CONVERSION[\"Identity\",METHOD[\"Identity\"]]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var concat = (Wkt2ConcatenatedOperation)crs;
+ Assert.That(concat.Steps, Has.Count.EqualTo(2));
+ Assert.That(concat.Steps[0].Keyword, Is.EqualTo("COORDINATEOPERATION"));
+ Assert.That(concat.Steps[0].Name, Is.EqualTo("WGS 84 to NAD83 (1)"));
+ Assert.That(concat.Steps[0].SourceCrs, Is.Not.Null);
+ Assert.That(concat.Steps[0].TargetCrs, Is.Not.Null);
+ Assert.That(concat.Steps[0].Method, Is.EqualTo("Geocentric translations"));
+ Assert.That(concat.Steps[0].Parameters, Has.Count.EqualTo(3));
+ Assert.That(concat.Steps[0].Parameters[0].Name, Is.EqualTo("X-axis translation"));
+ Assert.That(concat.Steps[0].Parameters[0].Value, Is.EqualTo(0));
+
+ Assert.That(concat.Steps[1].Keyword, Is.EqualTo("CONVERSION"));
+ Assert.That(concat.Steps[1].Name, Is.EqualTo("Identity"));
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2DerivedGeogCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2DerivedGeogCrsTests.cs
new file mode 100644
index 0000000..740f0a3
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2DerivedGeogCrsTests.cs
@@ -0,0 +1,162 @@
+using System;
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2DerivedGeogCrsTests
+{
+ private const string Wkt2DerivedGeogCrs_AtlanticPole =
+ "DERIVEDGEOGCRS[\"WMO Atlantic Pole\"," +
+ "BASEGEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0]]," +
+ "DERIVINGCONVERSION[\"Atlantic pole rotation\"," +
+ "METHOD[\"Pole rotation\"]," +
+ "PARAMETER[\"Latitude of rotated pole\",52]," +
+ "PARAMETER[\"Longitude of rotated pole\",-30]," +
+ "PARAMETER[\"Axis rotation\",-25]]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north,ORDER[1]]," +
+ "AXIS[\"longitude\",east,ORDER[2]]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ [Test]
+ public void Parse_DerivedGeogCrs_Basic()
+ {
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(Wkt2DerivedGeogCrs_AtlanticPole);
+ Assert.That(crs, Is.InstanceOf());
+
+ var derived = (Wkt2DerivedGeogCrs)crs;
+ Assert.That(derived.Name, Is.EqualTo("WMO Atlantic Pole"));
+ Assert.That(derived.Keyword, Is.EqualTo("DERIVEDGEOGCRS"));
+
+ // BaseCrs
+ Assert.That(derived.BaseCrs, Is.Not.Null);
+ Assert.That(derived.BaseCrs, Is.InstanceOf());
+ Assert.That(derived.BaseCrs.Name, Is.EqualTo("WGS 84"));
+
+ // DerivingConversion
+ Assert.That(derived.DerivingConversion, Is.Not.Null);
+ Assert.That(derived.DerivingConversion.Name, Is.EqualTo("Atlantic pole rotation"));
+ Assert.That(derived.DerivingConversion.MethodName, Is.EqualTo("Pole rotation"));
+ Assert.That(derived.DerivingConversion.Parameters, Has.Count.EqualTo(3));
+ Assert.That(derived.DerivingConversion.Parameters[0].Name, Is.EqualTo("Latitude of rotated pole"));
+ Assert.That(derived.DerivingConversion.Parameters[0].Value, Is.EqualTo(52));
+ Assert.That(derived.DerivingConversion.Parameters[1].Name, Is.EqualTo("Longitude of rotated pole"));
+ Assert.That(derived.DerivingConversion.Parameters[1].Value, Is.EqualTo(-30));
+ Assert.That(derived.DerivingConversion.Parameters[2].Name, Is.EqualTo("Axis rotation"));
+ Assert.That(derived.DerivingConversion.Parameters[2].Value, Is.EqualTo(-25));
+
+ // CoordinateSystem
+ Assert.That(derived.CoordinateSystem, Is.Not.Null);
+ Assert.That(derived.CoordinateSystem.Type, Is.EqualTo("ellipsoidal"));
+ Assert.That(derived.CoordinateSystem.Dimension, Is.EqualTo(2));
+ Assert.That(derived.CoordinateSystem.Axes, Has.Count.EqualTo(2));
+ Assert.That(derived.CoordinateSystem.Axes[0].Name, Is.EqualTo("latitude"));
+ Assert.That(derived.CoordinateSystem.Axes[0].Direction, Is.EqualTo("north"));
+ Assert.That(derived.CoordinateSystem.Axes[0].Order, Is.EqualTo(1));
+ Assert.That(derived.CoordinateSystem.Axes[1].Name, Is.EqualTo("longitude"));
+ Assert.That(derived.CoordinateSystem.Axes[1].Direction, Is.EqualTo("east"));
+ Assert.That(derived.CoordinateSystem.Axes[1].Order, Is.EqualTo(2));
+
+ // Unit
+ Assert.That(derived.CoordinateSystem.Unit, Is.Not.Null);
+ Assert.That(derived.CoordinateSystem.Unit.Name, Is.EqualTo("degree"));
+ Assert.That(derived.CoordinateSystem.Unit.ConversionFactor, Is.EqualTo(0.0174532925199433).Within(1e-13));
+ }
+
+ [Test]
+ public void Parse_DerivedGeogCrs_WithIdAndRemark()
+ {
+ const string wkt =
+ "DERIVEDGEOGCRS[\"WMO Atlantic Pole\"," +
+ "BASEGEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0]]," +
+ "DERIVINGCONVERSION[\"Atlantic pole rotation\",METHOD[\"Pole rotation\"]]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]," +
+ "ID[\"EXAMPLE\",\"1234\"]," +
+ "REMARK[\"WMO Atlantic pole rotation example\"]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var derived = (Wkt2DerivedGeogCrs)crs;
+ Assert.That(derived.Id, Is.Not.Null);
+ Assert.That(derived.Id.Authority, Is.EqualTo("EXAMPLE"));
+ Assert.That(derived.Id.Code, Is.EqualTo("1234"));
+ Assert.That(derived.Remark, Is.EqualTo("WMO Atlantic pole rotation example"));
+ }
+
+ [Test]
+ public void RoundTrip_DerivedGeogCrs()
+ {
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(Wkt2DerivedGeogCrs_AtlanticPole);
+ string output = crs.ToWkt2String();
+
+ // Verify the serialized output contains expected fragments.
+ // Note: The writer outputs GEOGCRS for the BaseCrs (not BASEGEOGCRS),
+ // so a full re-parse round-trip is not possible without writer correction.
+ Assert.That(output, Does.StartWith("DERIVEDGEOGCRS[\"WMO Atlantic Pole\""));
+ Assert.That(output, Does.Contain("\"WGS 84\""));
+ Assert.That(output, Does.Contain("DERIVINGCONVERSION[\"Atlantic pole rotation\""));
+ Assert.That(output, Does.Contain("METHOD[\"Pole rotation\"]"));
+ Assert.That(output, Does.Contain("PARAMETER[\"Latitude of rotated pole\",52]"));
+ Assert.That(output, Does.Contain("PARAMETER[\"Longitude of rotated pole\",-30]"));
+ Assert.That(output, Does.Contain("PARAMETER[\"Axis rotation\",-25]"));
+ Assert.That(output, Does.Contain("CS[ellipsoidal,2]"));
+ Assert.That(output, Does.Contain("ANGLEUNIT[\"degree\""));
+ }
+
+ [Test]
+ public void Parse_DerivedGeogCrs_MissingBaseCrs_Throws()
+ {
+ const string wkt =
+ "DERIVEDGEOGCRS[\"Bad\"," +
+ "DERIVINGCONVERSION[\"Identity\",METHOD[\"Identity\"]]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_DerivedGeogCrs_MissingConversion_Throws()
+ {
+ const string wkt =
+ "DERIVEDGEOGCRS[\"Bad\"," +
+ "BASEGEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0]]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_DerivedGeogCrs_WithBaseGeodCrs()
+ {
+ const string wkt =
+ "DERIVEDGEOGCRS[\"Derived\"," +
+ "BASEGEODCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0]]," +
+ "DERIVINGCONVERSION[\"Identity\",METHOD[\"Identity\"]]," +
+ "CS[ellipsoidal,2]," +
+ "AXIS[\"latitude\",north]," +
+ "AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var derived = (Wkt2DerivedGeogCrs)crs;
+ Assert.That(derived.Name, Is.EqualTo("Derived"));
+ Assert.That(derived.BaseCrs, Is.Not.Null);
+ Assert.That(derived.BaseCrs.Name, Is.EqualTo("WGS 84"));
+ Assert.That(derived.DerivingConversion.MethodName, Is.EqualTo("Identity"));
+ }
+}
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/WKT2ErrorHandlingTests.cs b/test/ProjNet.Tests/WKT/WKT2ErrorHandlingTests.cs
new file mode 100644
index 0000000..b686ec1
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2ErrorHandlingTests.cs
@@ -0,0 +1,76 @@
+using System;
+using NUnit.Framework;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2ErrorHandlingTests
+{
+ [Test]
+ public void ParseCrs_NullInput_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(null));
+ }
+
+ [Test]
+ public void ParseCrs_EmptyString_ThrowsArgumentException()
+ {
+ // Empty or whitespace should throw ArgumentNullException (null-check catches whitespace too)
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(""));
+ }
+
+ [Test]
+ public void ParseCrs_WhitespaceOnly_ThrowsArgumentException()
+ {
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(" "));
+ }
+
+ [Test]
+ public void ParseCrs_UnrecognizedRootKeyword_ThrowsArgumentException()
+ {
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs("UNKNOWNCRS[\"test\"]"));
+ }
+
+ [Test]
+ public void ParseCrs_GeogCrsWithoutDatum_ThrowsArgumentException()
+ {
+ string wkt = "GEOGCRS[\"No Datum\",CS[ellipsoidal,2],AXIS[\"latitude\",north],AXIS[\"longitude\",east],ANGLEUNIT[\"degree\",0.0174532925199433]]";
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void ParseCrs_ConversionWithoutMethod_ThrowsArgumentException()
+ {
+ string wkt = "PROJCRS[\"No method\",BASEGEOGCRS[\"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[\"latitude\",north],AXIS[\"longitude\",east]],CONVERSION[\"Missing Method\",PARAMETER[\"false_easting\",0]],CS[cartesian,2],AXIS[\"easting\",east],AXIS[\"northing\",north],LENGTHUNIT[\"metre\",1]]";
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void ParseCrs_BoundCrsWithoutSourceCrs_ThrowsArgumentException()
+ {
+ string wkt = "BOUNDCRS[\"No source\",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[\"latitude\",north],AXIS[\"longitude\",east]]],ABRIDGEDTRANSFORMATION[\"test\",METHOD[\"Geocentric translations\"],PARAMETER[\"X-axis translation\",0],PARAMETER[\"Y-axis translation\",0],PARAMETER[\"Z-axis translation\",0]]]";
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void ParseCrs_CompoundCrsWithoutComponents_ThrowsArgumentException()
+ {
+ string wkt = "COMPOUNDCRS[\"Empty compound\"]";
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_UnsupportedCrsType_ThrowsNotSupportedException()
+ {
+ // VERTCRS is parseable to model but not convertible to ProjNet
+ string wkt = "VERTCRS[\"EGM96 height\",VDATUM[\"EGM96 geoid\"],CS[vertical,1],AXIS[\"gravity-related height (H)\",up,ORDER[1]],LENGTHUNIT[\"metre\",1],ID[\"EPSG\",5773]]";
+ Assert.Throws(() => CoordinateSystemWkt2Reader.Parse(wkt));
+ }
+
+ [Test]
+ public void Parse_NullInput_ThrowsArgumentNullException()
+ {
+ Assert.Throws(() => CoordinateSystemWkt2Reader.Parse(null));
+ }
+}
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/WKT2ParserEdgeCaseTests.cs b/test/ProjNet.Tests/WKT/WKT2ParserEdgeCaseTests.cs
new file mode 100644
index 0000000..ed2a771
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2ParserEdgeCaseTests.cs
@@ -0,0 +1,184 @@
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2ParserEdgeCaseTests
+{
+ // --- B4: Alternative Keywords ---
+
+ [Test]
+ public void ParseCrs_GeographicCrsKeyword_ParsesAsGeogCrs()
+ {
+ // Use GEOGRAPHICCRS instead of GEOGCRS
+ string wkt = "GEOGRAPHICCRS[\"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[\"latitude\",north,ORDER[1]],AXIS[\"longitude\",east,ORDER[2]],ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",4326]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model, Is.InstanceOf());
+ var geog = (Wkt2GeogCrs)model;
+ Assert.That(geog.Keyword, Is.EqualTo("GEOGRAPHICCRS"));
+ Assert.That(geog.Name, Is.EqualTo("WGS 84"));
+ }
+
+ [Test]
+ public void ParseCrs_ProjectedCrsKeyword_ParsesAsProjCrs()
+ {
+ // Use PROJECTEDCRS instead of PROJCRS
+ string wkt = "PROJECTEDCRS[\"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[\"easting (E)\",east,ORDER[1]],AXIS[\"northing (N)\",north,ORDER[2]],LENGTHUNIT[\"metre\",1],ID[\"EPSG\",32632]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model, Is.InstanceOf());
+ Assert.That(((Wkt2ProjCrs)model).Keyword, Is.EqualTo("PROJECTEDCRS"));
+ }
+
+ [Test]
+ public void ParseCrs_VerticalCrsKeyword_ParsesAsVertCrs()
+ {
+ string wkt = "VERTICALCRS[\"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]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model, Is.InstanceOf());
+ Assert.That(((Wkt2VertCrs)model).Keyword, Is.EqualTo("VERTICALCRS"));
+ }
+
+ [Test]
+ public void ParseCrs_EngineeringCrsKeyword_ParsesAsEngCrs()
+ {
+ string wkt = "ENGINEERINGCRS[\"Local grid\",EDATUM[\"Local datum\"],CS[cartesian,2],AXIS[\"x\",east,ORDER[1]],AXIS[\"y\",north,ORDER[2]],LENGTHUNIT[\"metre\",1]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model, Is.InstanceOf());
+ Assert.That(((Wkt2EngCrs)model).Keyword, Is.EqualTo("ENGINEERINGCRS"));
+ }
+
+ // --- B4: Element Ordering ---
+
+ [Test]
+ public void ParseConversion_MethodAfterParameter_PreservesAllData()
+ {
+ // This tests the fix: METHOD appearing after PARAMETER should not lose params
+ string wkt = "PROJCRS[\"Test\"," +
+ "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[\"Test Conv\",PARAMETER[\"False easting\",500000],METHOD[\"Transverse Mercator\"],PARAMETER[\"False northing\",0]]," +
+ "CS[cartesian,2],AXIS[\"easting\",east],AXIS[\"northing\",north],LENGTHUNIT[\"metre\",1]]";
+ var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Conversion.MethodName, Is.EqualTo("Transverse Mercator"));
+ Assert.That(model.Conversion.Parameters, Has.Count.EqualTo(2));
+ Assert.That(model.Conversion.Parameters[0].Name, Is.EqualTo("False easting"));
+ Assert.That(model.Conversion.Parameters[0].Value, Is.EqualTo(500000));
+ Assert.That(model.Conversion.Parameters[1].Name, Is.EqualTo("False northing"));
+ Assert.That(model.Conversion.Parameters[1].Value, Is.EqualTo(0));
+ }
+
+ [Test]
+ public void ParseConversion_IdBeforeMethod_PreservesId()
+ {
+ string wkt = "PROJCRS[\"Test\"," +
+ "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[\"Test Conv\",ID[\"EPSG\",16032],METHOD[\"Transverse Mercator\"],PARAMETER[\"False easting\",500000]]," +
+ "CS[cartesian,2],AXIS[\"easting\",east],AXIS[\"northing\",north],LENGTHUNIT[\"metre\",1]]";
+ var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Conversion.Id, Is.Not.Null);
+ Assert.That(model.Conversion.Id.Authority, Is.EqualTo("EPSG"));
+ Assert.That(model.Conversion.Id.Code, Is.EqualTo("16032"));
+ Assert.That(model.Conversion.MethodName, Is.EqualTo("Transverse Mercator"));
+ }
+
+ [Test]
+ public void ParseConversion_RemarkBeforeMethod_PreservesRemark()
+ {
+ string wkt = "PROJCRS[\"Test\"," +
+ "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[\"Test Conv\",REMARK[\"test remark\"],METHOD[\"Transverse Mercator\"],PARAMETER[\"False easting\",500000]]," +
+ "CS[cartesian,2],AXIS[\"easting\",east],AXIS[\"northing\",north],LENGTHUNIT[\"metre\",1]]";
+ var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Conversion.Remark, Is.EqualTo("test remark"));
+ Assert.That(model.Conversion.MethodName, Is.EqualTo("Transverse Mercator"));
+ }
+
+ // --- B4: Bracket Styles ---
+ // Note: WKT2 supports both [] and () brackets. The tokenizer handles this.
+
+ [Test]
+ public void ParseCrs_ParenthesisBrackets_ParsesCorrectly()
+ {
+ // Use () instead of []
+ string wkt = "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(\"latitude\",north,ORDER(1)),AXIS(\"longitude\",east,ORDER(2)),ANGLEUNIT(\"degree\",0.0174532925199433),ID(\"EPSG\",4326))";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model, Is.InstanceOf());
+ Assert.That(model.Name, Is.EqualTo("WGS 84"));
+ var geog = (Wkt2GeogCrs)model;
+ Assert.That(geog.Datum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ }
+
+ // --- B7: Edge Cases ---
+
+ [Test]
+ public void ParseCrs_3DGeogCrs_ParsesAllThreeAxes()
+ {
+ string wkt = "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[\"latitude\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS[\"longitude\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS[\"ellipsoidal height\",up,ORDER[3],LENGTHUNIT[\"metre\",1]],ID[\"EPSG\",4329]]";
+ var model = (Wkt2GeogCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.CoordinateSystem.Dimension, Is.EqualTo(3));
+ Assert.That(model.CoordinateSystem.Axes, Has.Count.EqualTo(3));
+ Assert.That(model.CoordinateSystem.Axes[2].Direction, Is.EqualTo("up"));
+ }
+
+ [Test]
+ public void ParseCrs_ZeroInverseFlattening_Sphere()
+ {
+ // A sphere has InverseFlattening = 0
+ string wkt = "GEOGCRS[\"Sphere\",DATUM[\"Sphere datum\",ELLIPSOID[\"Sphere\",6371000,0,LENGTHUNIT[\"metre\",1]]],CS[ellipsoidal,2],AXIS[\"latitude\",north],AXIS[\"longitude\",east],ANGLEUNIT[\"degree\",0.0174532925199433]]";
+ var model = (Wkt2GeogCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Datum.Ellipsoid.InverseFlattening, Is.EqualTo(0));
+ Assert.That(model.Datum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6371000));
+ }
+
+ [Test]
+ public void ParseCrs_VeryLongName_HandlesCorrectly()
+ {
+ string longName = new string('A', 500);
+ string wkt = $"GEOGCRS[\"{longName}\",DATUM[\"datum\",ELLIPSOID[\"ellips\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],CS[ellipsoidal,2],AXIS[\"lat\",north],AXIS[\"lon\",east],ANGLEUNIT[\"degree\",0.0174532925199433]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Name, Is.EqualTo(longName));
+ }
+
+ [Test]
+ public void ParseCrs_NameWithEscapedQuotes_ThrowsBecauseTokenizerDoesNotSupportIt()
+ {
+ // WKT2 spec escapes double quotes by doubling them: "" inside quoted string.
+ // The current tokenizer does not handle this and throws an ArgumentException.
+ string wkt = "GEOGCRS[\"WGS 84 \"\"test\"\"\",DATUM[\"datum\",ELLIPSOID[\"ellips\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],CS[ellipsoidal,2],AXIS[\"lat\",north],AXIS[\"lon\",east],ANGLEUNIT[\"degree\",0.0174532925199433]]";
+ Assert.That(() => CoordinateSystemWkt2Reader.ParseCrs(wkt), Throws.TypeOf());
+ }
+
+ [Test]
+ public void ParseCrs_RemarkPreservation()
+ {
+ string wkt = "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[\"latitude\",north],AXIS[\"longitude\",east],ANGLEUNIT[\"degree\",0.0174532925199433],REMARK[\"This is a test remark\"]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Remark, Is.EqualTo("This is a test remark"));
+ }
+
+ [Test]
+ public void ParseCrs_BaseGeodCrsKeyword_ParsesCorrectly()
+ {
+ // BASEGEODCRS is an alternative to BASEGEOGCRS
+ string wkt = "PROJCRS[\"NAD83\",BASEGEODCRS[\"NAD83\",DATUM[\"North American Datum 1983\",ELLIPSOID[\"GRS 1980\",6378137,298.257222101,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]],CONVERSION[\"test\",METHOD[\"Transverse Mercator\"],PARAMETER[\"False easting\",500000]],CS[cartesian,2],AXIS[\"easting\",east],AXIS[\"northing\",north],LENGTHUNIT[\"metre\",1]]";
+ var model = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.BaseCrs, Is.Not.Null);
+ Assert.That(model.BaseCrs.Datum.Name, Is.EqualTo("North American Datum 1983"));
+ }
+
+ [Test]
+ public void ParseCrs_IdWithVersionAndUri_PreservesAll()
+ {
+ string wkt = "GEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],CS[ellipsoidal,2],AXIS[\"lat\",north],AXIS[\"lon\",east],ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",4326,\"9.8.15\",URI[\"urn:ogc:def:crs:EPSG::4326\"]]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Id, Is.Not.Null);
+ Assert.That(model.Id.Authority, Is.EqualTo("EPSG"));
+ Assert.That(model.Id.Code, Is.EqualTo("4326"));
+ // Version and URI might or might not be preserved depending on implementation
+ // At minimum, parsing should succeed
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs
new file mode 100644
index 0000000..4b895d3
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2ProjCrsTests.cs
@@ -0,0 +1,96 @@
+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));
+
+ // Verify projection parameters
+ Assert.That(pcs.Projection.ClassName, Does.Contain("Transverse_Mercator").IgnoreCase);
+ Assert.That(pcs.LinearUnit.MetersPerUnit, Is.EqualTo(1.0));
+ // Verify base geographic CS
+ Assert.That(pcs.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ Assert.That(pcs.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid.InverseFlattening, Is.EqualTo(298.257223563));
+ }
+
+ [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));
+
+ // Verify projection parameters individually
+ for (int i = 0; i < original.Projection.NumParameters && i < roundTripped.Projection.NumParameters; i++)
+ {
+ var origParam = original.Projection.GetParameter(i);
+ var rtParam = roundTripped.Projection.GetParameter(origParam.Name);
+ if (rtParam != null)
+ Assert.That(rtParam.Value, Is.EqualTo(origParam.Value).Within(1e-10), $"Parameter '{origParam.Name}' differs");
+ }
+ }
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2ProjectionMappingTests.cs b/test/ProjNet.Tests/WKT/WKT2ProjectionMappingTests.cs
new file mode 100644
index 0000000..01778e1
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2ProjectionMappingTests.cs
@@ -0,0 +1,111 @@
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2ProjectionMappingTests
+{
+ [Test]
+ public void MapProjectionMethodName_TransverseMercator()
+ {
+ string result = Wkt2Conversions.MapProjectionMethodName("Transverse Mercator");
+ Assert.That(result, Is.EqualTo("Transverse_Mercator"));
+ }
+
+ [Test]
+ public void MapProjectionMethodName_LambertConicConformal2SP()
+ {
+ string result = Wkt2Conversions.MapProjectionMethodName("Lambert Conic Conformal (2SP)");
+ Assert.That(result, Is.EqualTo("lambert_conformal_conic_2sp"));
+ }
+
+ [Test]
+ public void MapProjectionMethodName_AlbersEqualArea()
+ {
+ string result = Wkt2Conversions.MapProjectionMethodName("Albers Equal Area");
+ Assert.That(result, Is.EqualTo("Albers_Conic_Equal_Area"));
+ }
+
+ [Test]
+ public void MapProjectionMethodName_Mercator_Variant_A()
+ {
+ string result = Wkt2Conversions.MapProjectionMethodName("Mercator (variant A)");
+ Assert.That(result, Is.EqualTo("Mercator_1SP"));
+ }
+
+ [Test]
+ public void MapProjectionMethodName_UnknownMethod_ReturnsInput()
+ {
+ string result = Wkt2Conversions.MapProjectionMethodName("Some Unknown Projection");
+ Assert.That(result, Is.EqualTo("Some Unknown Projection"));
+ }
+
+ [Test]
+ public void MapProjNetToWkt2MethodName_TransverseMercator()
+ {
+ string result = Wkt2Conversions.MapProjNetToWkt2MethodName("Transverse_Mercator");
+ Assert.That(result, Is.EqualTo("Transverse Mercator"));
+ }
+
+ [Test]
+ public void MapProjNetToWkt2MethodName_LambertConicConformal2SP()
+ {
+ string result = Wkt2Conversions.MapProjNetToWkt2MethodName("lambert_conformal_conic_2sp");
+ Assert.That(result, Is.EqualTo("Lambert Conic Conformal (2SP)"));
+ }
+
+ [Test]
+ public void MapProjNetToWkt2MethodName_UnknownMethod_ReturnsInput()
+ {
+ string result = Wkt2Conversions.MapProjNetToWkt2MethodName("Unknown_Projection");
+ Assert.That(result, Is.EqualTo("Unknown_Projection"));
+ }
+
+ [Test]
+ public void MapProjectionMethodName_CaseInsensitivity()
+ {
+ // The dictionaries use StringComparer.OrdinalIgnoreCase, so different casing should match.
+ string result = Wkt2Conversions.MapProjectionMethodName("transverse mercator");
+ Assert.That(result, Is.EqualTo("Transverse_Mercator"));
+
+ string result2 = Wkt2Conversions.MapProjectionMethodName("TRANSVERSE MERCATOR");
+ Assert.That(result2, Is.EqualTo("Transverse_Mercator"));
+ }
+
+ [Test]
+ public void RoundTrip_MethodNameMapping()
+ {
+ // For each known WKT2 method name that maps to a unique ProjNet name which maps back,
+ // verify the round-trip returns a valid WKT2 name.
+ var wkt2ToProjNet = new (string Wkt2, string ProjNet)[]
+ {
+ ("Transverse Mercator", "Transverse_Mercator"),
+ ("Mercator (variant A)", "Mercator_1SP"),
+ ("Mercator (variant B)", "Mercator_2SP"),
+ ("Lambert Conic Conformal (2SP)", "lambert_conformal_conic_2sp"),
+ ("Lambert Conic Conformal (1SP)", "Lambert_Conformal_Conic"),
+ ("Lambert Azimuthal Equal Area", "Lambert_Azimuthal_Equal_Area"),
+ ("Albers Equal Area", "Albers_Conic_Equal_Area"),
+ ("Oblique Stereographic", "Oblique_Stereographic"),
+ ("Cassini-Soldner", "Cassini_Soldner"),
+ ("Krovak", "Krovak"),
+ ("Orthographic", "Orthographic"),
+ };
+
+ foreach (var (wkt2Name, projNetName) in wkt2ToProjNet)
+ {
+ // WKT2 → ProjNet
+ string toProjNet = Wkt2Conversions.MapProjectionMethodName(wkt2Name);
+ Assert.That(toProjNet, Is.EqualTo(projNetName), $"WKT2→ProjNet failed for '{wkt2Name}'");
+
+ // ProjNet → WKT2 (should produce a valid WKT2 name)
+ string backToWkt2 = Wkt2Conversions.MapProjNetToWkt2MethodName(toProjNet);
+ Assert.That(backToWkt2, Is.Not.Null.And.Not.Empty, $"ProjNet→WKT2 failed for '{toProjNet}'");
+
+ // The round-trip WKT2 name should map back to the same ProjNet name
+ string roundTripped = Wkt2Conversions.MapProjectionMethodName(backToWkt2);
+ Assert.That(roundTripped, Is.EqualTo(projNetName), $"Round-trip failed for '{wkt2Name}' → '{projNetName}' → '{backToWkt2}' → '{roundTripped}'");
+ }
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs b/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs
new file mode 100644
index 0000000..899b254
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2Rgf93Lambert93Tests.cs
@@ -0,0 +1,75 @@
+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));
+
+ // Verify Lambert-93 parameters
+ Assert.That(pcs.Projection.ClassName, Does.Contain("lambert_conformal_conic").IgnoreCase);
+ Assert.That(pcs.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ Assert.That(pcs.GeographicCoordinateSystem.HorizontalDatum.Ellipsoid.InverseFlattening, Is.EqualTo(298.257222101));
+ }
+
+ [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..baa630f
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2RoundTripTests.cs
@@ -0,0 +1,94 @@
+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));
+
+ // Verify datum and ellipsoid values
+ Assert.That(gcs.HorizontalDatum.Name, Does.Contain("World Geodetic System 1984"));
+ Assert.That(gcs.HorizontalDatum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ Assert.That(gcs.HorizontalDatum.Ellipsoid.InverseFlattening, Is.EqualTo(298.257223563));
+ // Verify prime meridian
+ Assert.That(gcs.PrimeMeridian.Longitude, Is.EqualTo(0));
+ // Verify angular unit
+ Assert.That(gcs.AngularUnit.RadiansPerUnit, Is.EqualTo(0.0174532925199433).Within(1e-13));
+ }
+
+ [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);
+
+ // Verify detailed preservation
+ Assert.That(roundTripped.HorizontalDatum.Ellipsoid.SemiMajorAxis, Is.EqualTo(original.HorizontalDatum.Ellipsoid.SemiMajorAxis));
+ Assert.That(roundTripped.HorizontalDatum.Ellipsoid.InverseFlattening, Is.EqualTo(original.HorizontalDatum.Ellipsoid.InverseFlattening));
+ Assert.That(roundTripped.PrimeMeridian.Longitude, Is.EqualTo(original.PrimeMeridian.Longitude));
+ Assert.That(roundTripped.AngularUnit.RadiansPerUnit, Is.EqualTo(original.AngularUnit.RadiansPerUnit).Within(1e-13));
+ }
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2TimeCrsTests.cs b/test/ProjNet.Tests/WKT/WKT2TimeCrsTests.cs
new file mode 100644
index 0000000..b727e75
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2TimeCrsTests.cs
@@ -0,0 +1,181 @@
+using System;
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2TimeCrsTests
+{
+ [Test]
+ public void Parse_TimeCrs_WithTDatum()
+ {
+ const string wkt =
+ "TIMECRS[\"GPS Time\"," +
+ "TDATUM[\"Time origin\",TIMEORIGIN[\"1980-01-06T00:00:00.0Z\"],CALENDAR[\"proleptic Gregorian\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time (T)\",future,ORDER[1]]," +
+ "TIMEUNIT[\"day\",86400]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var timeCrs = (Wkt2TimeCrs)crs;
+ Assert.That(timeCrs.Name, Is.EqualTo("GPS Time"));
+ Assert.That(timeCrs.Keyword, Is.EqualTo("TIMECRS"));
+
+ Assert.That(timeCrs.Datum, Is.Not.Null);
+ Assert.That(timeCrs.Datum.Name, Is.EqualTo("Time origin"));
+ Assert.That(timeCrs.Datum.Keyword, Is.EqualTo("TDATUM"));
+ Assert.That(timeCrs.Datum.Calendar, Is.EqualTo("proleptic Gregorian"));
+ Assert.That(timeCrs.Datum.TimeOrigin, Is.EqualTo("1980-01-06T00:00:00.0Z"));
+
+ Assert.That(timeCrs.CoordinateSystem, Is.Not.Null);
+ Assert.That(timeCrs.CoordinateSystem.Type, Is.EqualTo("temporal"));
+ Assert.That(timeCrs.CoordinateSystem.Dimension, Is.EqualTo(1));
+ Assert.That(timeCrs.CoordinateSystem.Axes, Has.Count.EqualTo(1));
+ Assert.That(timeCrs.CoordinateSystem.Axes[0].Name, Is.EqualTo("time (T)"));
+ Assert.That(timeCrs.CoordinateSystem.Axes[0].Direction, Is.EqualTo("future"));
+ Assert.That(timeCrs.CoordinateSystem.Axes[0].Order, Is.EqualTo(1));
+
+ Assert.That(timeCrs.CoordinateSystem.Unit, Is.Not.Null);
+ Assert.That(timeCrs.CoordinateSystem.Unit.Name, Is.EqualTo("day"));
+ Assert.That(timeCrs.CoordinateSystem.Unit.ConversionFactor, Is.EqualTo(86400));
+ Assert.That(timeCrs.CoordinateSystem.Unit.Keyword, Is.EqualTo("TIMEUNIT"));
+ }
+
+ [Test]
+ public void Parse_TimeCrs_WithTimedatum()
+ {
+ const string wkt =
+ "TIMECRS[\"Modified Julian Date\"," +
+ "TIMEDATUM[\"Modified Julian\",TIMEORIGIN[\"1858-11-17\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time\",future]," +
+ "TIMEUNIT[\"day\",86400]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var timeCrs = (Wkt2TimeCrs)crs;
+ Assert.That(timeCrs.Name, Is.EqualTo("Modified Julian Date"));
+ Assert.That(timeCrs.Datum.Name, Is.EqualTo("Modified Julian"));
+ Assert.That(timeCrs.Datum.Keyword, Is.EqualTo("TIMEDATUM"));
+ Assert.That(timeCrs.Datum.TimeOrigin, Is.EqualTo("1858-11-17"));
+ }
+
+ [Test]
+ public void Parse_TimeCrs_WithIdAndRemark()
+ {
+ const string wkt =
+ "TIMECRS[\"DateTime\"," +
+ "TDATUM[\"DateTime\",TIMEORIGIN[\"0001-01-01T00:00:00\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time\",future]," +
+ "TIMEUNIT[\"second\",1]," +
+ "ID[\"PROJ\",\"TDATETIME\"]," +
+ "REMARK[\"For DateTime\"]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var timeCrs = (Wkt2TimeCrs)crs;
+ Assert.That(timeCrs.Name, Is.EqualTo("DateTime"));
+ Assert.That(timeCrs.Id, Is.Not.Null);
+ Assert.That(timeCrs.Id.Authority, Is.EqualTo("PROJ"));
+ Assert.That(timeCrs.Id.Code, Is.EqualTo("TDATETIME"));
+ Assert.That(timeCrs.Remark, Is.EqualTo("For DateTime"));
+ }
+
+ [Test]
+ public void Parse_TimeCrs_WithUsage()
+ {
+ const string wkt =
+ "TIMECRS[\"Unix Time\"," +
+ "TDATUM[\"Unix epoch\",TIMEORIGIN[\"1970-01-01T00:00:00Z\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time\",future]," +
+ "TIMEUNIT[\"second\",1]," +
+ "USAGE[SCOPE[\"Satellite\"],AREA[\"World\"],BBOX[-90,-180,90,180]]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var timeCrs = (Wkt2TimeCrs)crs;
+ Assert.That(timeCrs.Name, Is.EqualTo("Unix Time"));
+ Assert.That(timeCrs.Usages, Has.Count.EqualTo(1));
+ Assert.That(timeCrs.Usages[0].Scope, Is.EqualTo("Satellite"));
+ Assert.That(timeCrs.Usages[0].Area, Is.EqualTo("World"));
+ Assert.That(timeCrs.Usages[0].BBox, Is.Not.Null);
+ Assert.That(timeCrs.Usages[0].BBox.South, Is.EqualTo(-90));
+ Assert.That(timeCrs.Usages[0].BBox.West, Is.EqualTo(-180));
+ Assert.That(timeCrs.Usages[0].BBox.North, Is.EqualTo(90));
+ Assert.That(timeCrs.Usages[0].BBox.East, Is.EqualTo(180));
+ }
+
+ [Test]
+ public void RoundTrip_TimeCrs()
+ {
+ const string wkt =
+ "TIMECRS[\"GPS Time\"," +
+ "TDATUM[\"Time origin\",TIMEORIGIN[\"1980-01-06T00:00:00.0Z\"],CALENDAR[\"proleptic Gregorian\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time (T)\",future,ORDER[1]]," +
+ "TIMEUNIT[\"day\",86400]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ string output = crs.ToWkt2String();
+
+ var crs2 = CoordinateSystemWkt2Reader.ParseCrs(output);
+ Assert.That(crs2, Is.InstanceOf());
+
+ var timeCrs2 = (Wkt2TimeCrs)crs2;
+ Assert.That(timeCrs2.Name, Is.EqualTo("GPS Time"));
+ Assert.That(timeCrs2.Datum.Name, Is.EqualTo("Time origin"));
+ Assert.That(timeCrs2.Datum.Keyword, Is.EqualTo("TDATUM"));
+ Assert.That(timeCrs2.Datum.Calendar, Is.EqualTo("proleptic Gregorian"));
+ Assert.That(timeCrs2.Datum.TimeOrigin, Is.EqualTo("1980-01-06T00:00:00.0Z"));
+ Assert.That(timeCrs2.CoordinateSystem.Type, Is.EqualTo("temporal"));
+ Assert.That(timeCrs2.CoordinateSystem.Dimension, Is.EqualTo(1));
+ Assert.That(timeCrs2.CoordinateSystem.Axes, Has.Count.EqualTo(1));
+ Assert.That(timeCrs2.CoordinateSystem.Axes[0].Name, Is.EqualTo("time (T)"));
+ Assert.That(timeCrs2.CoordinateSystem.Axes[0].Direction, Is.EqualTo("future"));
+ Assert.That(timeCrs2.CoordinateSystem.Unit.Name, Is.EqualTo("day"));
+ Assert.That(timeCrs2.CoordinateSystem.Unit.ConversionFactor, Is.EqualTo(86400));
+ }
+
+ [Test]
+ public void Parse_TimeCrs_MissingDatum_Throws()
+ {
+ const string wkt =
+ "TIMECRS[\"Bad\"," +
+ "CS[temporal,1]," +
+ "AXIS[\"time\",future]," +
+ "TIMEUNIT[\"day\",86400]]";
+
+ Assert.Throws(() => CoordinateSystemWkt2Reader.ParseCrs(wkt));
+ }
+
+ [Test]
+ public void Parse_TimeCrs_WithUnit()
+ {
+ const string wkt =
+ "TIMECRS[\"Julian Date\"," +
+ "TDATUM[\"Julian\",TIMEORIGIN[\"-4713-11-24T12:00:00Z\"]]," +
+ "CS[temporal,1]," +
+ "AXIS[\"time\",future]," +
+ "UNIT[\"day\",86400]]";
+
+ var crs = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(crs, Is.InstanceOf());
+
+ var timeCrs = (Wkt2TimeCrs)crs;
+ Assert.That(timeCrs.Name, Is.EqualTo("Julian Date"));
+ Assert.That(timeCrs.CoordinateSystem.Unit, Is.Not.Null);
+ Assert.That(timeCrs.CoordinateSystem.Unit.Name, Is.EqualTo("day"));
+ Assert.That(timeCrs.CoordinateSystem.Unit.ConversionFactor, Is.EqualTo(86400));
+ Assert.That(timeCrs.CoordinateSystem.Unit.Keyword, Is.EqualTo("UNIT"));
+ Assert.That(timeCrs.Datum.TimeOrigin, Is.EqualTo("-4713-11-24T12:00:00Z"));
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2TransformAccuracyTests.cs b/test/ProjNet.Tests/WKT/WKT2TransformAccuracyTests.cs
new file mode 100644
index 0000000..baf86f3
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2TransformAccuracyTests.cs
@@ -0,0 +1,152 @@
+using NUnit.Framework;
+using ProjNet.CoordinateSystems;
+using ProjNet.CoordinateSystems.Transformations;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+///
+/// Verifies that coordinate transformations produce correct results after
+/// WKT2 model conversion (ProjNet → WKT2 → ProjNet round-trip) and
+/// direct WKT2 GEOGCRS parsing.
+///
+[TestFixture]
+public class WKT2TransformAccuracyTests
+{
+ private static readonly CoordinateTransformationFactory CtFactory = new CoordinateTransformationFactory();
+
+ // WGS 84 Geographic CRS (WKT2)
+ private const string Wkt2Wgs84 =
+ "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[\"latitude\",north,ORDER[1]]," +
+ "AXIS[\"longitude\",east,ORDER[2]]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]," +
+ "ID[\"EPSG\",4326]]";
+
+ // WKT1 Lambert-93 for constructing a ProjNet base PCS
+ private const string Wkt1Lambert93 =
+ "PROJCS[\"RGF93 / Lambert-93\"," +
+ "GEOGCS[\"RGF93\"," +
+ "DATUM[\"Reseau_Geodesique_Francais_1993\"," +
+ "SPHEROID[\"GRS 1980\",6378137,298.257222101]]," +
+ "PRIMEM[\"Greenwich\",0]," +
+ "UNIT[\"degree\",0.0174532925199433]]," +
+ "PROJECTION[\"Lambert_Conformal_Conic_2SP\"]," +
+ "PARAMETER[\"standard_parallel_1\",49]," +
+ "PARAMETER[\"standard_parallel_2\",44]," +
+ "PARAMETER[\"latitude_of_origin\",46.5]," +
+ "PARAMETER[\"central_meridian\",3]," +
+ "PARAMETER[\"false_easting\",700000]," +
+ "PARAMETER[\"false_northing\",6600000]," +
+ "UNIT[\"metre\",1]]";
+
+ [Test]
+ public void Wkt2ProjCrs_Utm32N_TransformsCoordinatesCorrectly()
+ {
+ // Round-trip ProjNet UTM32N through WKT2 model and verify transform
+ var original = ProjectedCoordinateSystem.WGS84_UTM(32, true);
+ var wkt2Model = Wkt2Conversions.FromProjNetProjectedCoordinateSystem(original);
+ var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(wkt2Model);
+
+ var transform = CtFactory.CreateFromCoordinateSystems(
+ GeographicCoordinateSystem.WGS84, pcs);
+
+ // Transform Stuttgart (lon=9.18, lat=48.78) - well within UTM zone 32
+ double[] pt = { 9.18, 48.78 };
+ var result = transform.MathTransform.Transform(pt);
+
+ Assert.That(result[0], Is.EqualTo(513224).Within(500), "Easting for Stuttgart");
+ Assert.That(result[1], Is.EqualTo(5403000).Within(500), "Northing for Stuttgart");
+ }
+
+ [Test]
+ public void Wkt2ProjCrs_Lambert93_TransformsCoordinatesCorrectly()
+ {
+ // Create Lambert-93 from WKT1, round-trip through WKT2
+ var factory = new CoordinateSystemFactory();
+ var lambert93 = (ProjectedCoordinateSystem)factory.CreateFromWkt(Wkt1Lambert93);
+ var wkt2Model = Wkt2Conversions.FromProjNetProjectedCoordinateSystem(lambert93);
+ var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(wkt2Model);
+
+ var transform = CtFactory.CreateFromCoordinateSystems(
+ GeographicCoordinateSystem.WGS84, pcs);
+
+ // Transform Paris (lon=2.35, lat=48.86)
+ double[] pt = { 2.35, 48.86 };
+ var result = transform.MathTransform.Transform(pt);
+
+ Assert.That(result[0], Is.EqualTo(652469).Within(500), "Easting for Paris in Lambert-93");
+ Assert.That(result[1], Is.EqualTo(6862035).Within(500), "Northing for Paris in Lambert-93");
+ }
+
+ [Test]
+ public void Wkt2GeogCrs_ToProjNetGcs_TransformToUtmWorks()
+ {
+ // Parse WKT2 GEOGCRS directly and use as source for transformation
+ var gcsModel = (Wkt2GeogCrs)CoordinateSystemWkt2Reader.ParseCrs(Wkt2Wgs84);
+ var gcs = Wkt2Conversions.ToProjNetGeographicCoordinateSystem(gcsModel);
+
+ // Use pre-defined UTM32N as target
+ var utm32 = ProjectedCoordinateSystem.WGS84_UTM(32, true);
+
+ var transform = CtFactory.CreateFromCoordinateSystems(gcs, utm32);
+
+ // Transform Frankfurt (lon=8.68, lat=50.11) - within UTM zone 32
+ double[] pt = { 8.68, 50.11 };
+ var result = transform.MathTransform.Transform(pt);
+
+ Assert.That(result[0], Is.GreaterThan(100000).And.LessThan(900000), "Easting in valid UTM range");
+ Assert.That(result[1], Is.GreaterThan(5000000).And.LessThan(6500000), "Northing in valid UTM range");
+ }
+
+ [Test]
+ public void Wkt2ProjCrs_RoundTrip_TransformResultMatchesOriginal()
+ {
+ // Create original ProjNet UTM32N
+ var original = ProjectedCoordinateSystem.WGS84_UTM(32, true);
+
+ // Convert to WKT2 and back
+ var wkt2Model = Wkt2Conversions.FromProjNetProjectedCoordinateSystem(original);
+ var roundTripped = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(wkt2Model);
+
+ // Transform same point with both
+ var transformOriginal = CtFactory.CreateFromCoordinateSystems(GeographicCoordinateSystem.WGS84, original);
+ var transformRoundTripped = CtFactory.CreateFromCoordinateSystems(GeographicCoordinateSystem.WGS84, roundTripped);
+
+ double[] pt1 = { 10.0, 50.0 };
+ double[] pt2 = { 10.0, 50.0 };
+
+ var result1 = transformOriginal.MathTransform.Transform(pt1);
+ var result2 = transformRoundTripped.MathTransform.Transform(pt2);
+
+ // Results should be very close (within 0.01 meters)
+ Assert.That(result2[0], Is.EqualTo(result1[0]).Within(0.01), "Easting matches after round-trip");
+ Assert.That(result2[1], Is.EqualTo(result1[1]).Within(0.01), "Northing matches after round-trip");
+ }
+
+ [Test]
+ public void Wkt2ProjCrs_InverseTransform_RecoversOriginalCoordinates()
+ {
+ // Round-trip ProjNet UTM32N through WKT2 model
+ var original = ProjectedCoordinateSystem.WGS84_UTM(32, true);
+ var wkt2Model = Wkt2Conversions.FromProjNetProjectedCoordinateSystem(original);
+ var pcs = Wkt2Conversions.ToProjNetProjectedCoordinateSystem(wkt2Model);
+
+ var transform = CtFactory.CreateFromCoordinateSystems(
+ GeographicCoordinateSystem.WGS84, pcs);
+
+ // Forward transform
+ double[] originalPt = { 9.0, 48.0 };
+ var projected = transform.MathTransform.Transform(originalPt);
+
+ // Inverse transform
+ var recovered = transform.MathTransform.Inverse().Transform(projected);
+
+ Assert.That(recovered[0], Is.EqualTo(9.0).Within(1e-6), "Longitude recovered");
+ Assert.That(recovered[1], Is.EqualTo(48.0).Within(1e-6), "Latitude recovered");
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2UsageMetadataTests.cs b/test/ProjNet.Tests/WKT/WKT2UsageMetadataTests.cs
new file mode 100644
index 0000000..d5213f7
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2UsageMetadataTests.cs
@@ -0,0 +1,118 @@
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2UsageMetadataTests
+{
+ private const string GeogCrsWithUsage =
+ "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[\"latitude\",north],AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]," +
+ "USAGE[SCOPE[\"Horizontal component of 3D system.\"],AREA[\"World.\"],BBOX[-90,-180,90,180]]," +
+ "ID[\"EPSG\",4326]]";
+
+ private const string VertCrsWithUsage =
+ "VERTCRS[\"EGM96 height\"," +
+ "VDATUM[\"EGM96 geoid\",ID[\"EPSG\",5171]]," +
+ "CS[vertical,1],AXIS[\"gravity-related height (H)\",up,ORDER[1]]," +
+ "LENGTHUNIT[\"metre\",1]," +
+ "USAGE[SCOPE[\"Geodesy.\"],AREA[\"World.\"],BBOX[-90,-180,90,180]]," +
+ "ID[\"EPSG\",5773]]";
+
+ private const string GeogCrsWithRemark =
+ "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[\"latitude\",north],AXIS[\"longitude\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]," +
+ "REMARK[\"This is the most common CRS for GPS data.\"]]";
+
+ [Test]
+ public void ParseCrs_WithUsage_StoresScope()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithUsage);
+ Assert.That(model.Usages, Has.Count.GreaterThanOrEqualTo(1));
+ Assert.That(model.Usages[0].Scope, Is.EqualTo("Horizontal component of 3D system."));
+ }
+
+ [Test]
+ public void ParseCrs_WithUsage_StoresArea()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithUsage);
+ Assert.That(model.Usages, Has.Count.GreaterThanOrEqualTo(1));
+ Assert.That(model.Usages[0].Area, Is.EqualTo("World."));
+ }
+
+ [Test]
+ public void ParseCrs_WithUsage_StoresBBox()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithUsage);
+ Assert.That(model.Usages, Has.Count.GreaterThanOrEqualTo(1));
+ var bbox = model.Usages[0].BBox;
+ Assert.That(bbox, Is.Not.Null);
+ Assert.That(bbox.South, Is.EqualTo(-90));
+ Assert.That(bbox.West, Is.EqualTo(-180));
+ Assert.That(bbox.North, Is.EqualTo(90));
+ Assert.That(bbox.East, Is.EqualTo(180));
+ }
+
+ [Test]
+ public void ParseCrs_WithUsage_RoundTripsViaWriter()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithUsage);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ Assert.That(written, Does.Contain("USAGE["));
+ Assert.That(written, Does.Contain("SCOPE["));
+ Assert.That(written, Does.Contain("AREA["));
+ Assert.That(written, Does.Contain("BBOX["));
+
+ // Re-parse and verify
+ var reparsed = CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Usages, Has.Count.GreaterThanOrEqualTo(1));
+ Assert.That(reparsed.Usages[0].Scope, Is.EqualTo("Horizontal component of 3D system."));
+ }
+
+ [Test]
+ public void ParseVertCrs_WithUsage_StoresMetadata()
+ {
+ var model = (Wkt2VertCrs)CoordinateSystemWkt2Reader.ParseCrs(VertCrsWithUsage);
+ Assert.That(model.Usages, Has.Count.GreaterThanOrEqualTo(1));
+ Assert.That(model.Usages[0].Scope, Is.EqualTo("Geodesy."));
+ Assert.That(model.Usages[0].Area, Is.EqualTo("World."));
+ }
+
+ [Test]
+ public void ParseCrs_WithRemark_StoresRemark()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithRemark);
+ Assert.That(model.Remark, Is.EqualTo("This is the most common CRS for GPS data."));
+ }
+
+ [Test]
+ public void ParseCrs_WithRemark_RoundTripsViaWriter()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWithRemark);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ Assert.That(written, Does.Contain("REMARK["));
+
+ var reparsed = CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Remark, Is.EqualTo("This is the most common CRS for GPS data."));
+ }
+
+ [Test]
+ public void ParseCrs_WithoutUsage_HasEmptyUsagesList()
+ {
+ string wkt = "GEOGCRS[\"Simple\"," +
+ "DATUM[\"D\",ELLIPSOID[\"E\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]]," +
+ "CS[ellipsoidal,2],AXIS[\"lat\",north],AXIS[\"lon\",east]," +
+ "ANGLEUNIT[\"degree\",0.0174532925199433]]";
+ var model = CoordinateSystemWkt2Reader.ParseCrs(wkt);
+ Assert.That(model.Usages, Is.Not.Null);
+ Assert.That(model.Usages, Has.Count.EqualTo(0));
+ }
+}
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));
+ }
+ }
+}
diff --git a/test/ProjNet.Tests/WKT/WKT2WriterTests.cs b/test/ProjNet.Tests/WKT/WKT2WriterTests.cs
new file mode 100644
index 0000000..cd53448
--- /dev/null
+++ b/test/ProjNet.Tests/WKT/WKT2WriterTests.cs
@@ -0,0 +1,144 @@
+using NUnit.Framework;
+using ProjNet.CoordinateSystems.Wkt2;
+using ProjNet.IO.CoordinateSystems;
+
+namespace ProjNET.Tests.WKT;
+
+[TestFixture]
+public class WKT2WriterTests
+{
+ // --- Write + Reparse for all CRS types ---
+
+ private const string GeogCrsWkt = "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[\"latitude\",north,ORDER[1]],AXIS[\"longitude\",east,ORDER[2]],ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",4326]]";
+
+ private const string ProjCrsWkt = "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[\"easting (E)\",east,ORDER[1]],AXIS[\"northing (N)\",north,ORDER[2]],LENGTHUNIT[\"metre\",1],ID[\"EPSG\",32632]]";
+
+ private const string VertCrsWkt = "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]]";
+
+ private const string EngCrsWkt = "ENGCRS[\"Local grid\",EDATUM[\"Local datum\"],CS[cartesian,2],AXIS[\"x\",east,ORDER[1]],AXIS[\"y\",north,ORDER[2]],LENGTHUNIT[\"metre\",1]]";
+
+ private const string ParametricCrsWkt = "PARAMETRICCRS[\"Sigma\",PDATUM[\"Sigma datum\"],CS[parametric,1],AXIS[\"sigma\",up,ORDER[1]],PARAMETRICUNIT[\"unity\",1]]";
+
+ private const string CompoundCrsWkt = "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[\"latitude\",north],AXIS[\"longitude\",east],ANGLEUNIT[\"degree\",0.0174532925199433]]," +
+ "VERTCRS[\"EGM96 height\",VDATUM[\"EGM96 geoid\"],CS[vertical,1],AXIS[\"height\",up],LENGTHUNIT[\"metre\",1]]]";
+
+ private const string BoundCrsWkt = "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[\"latitude\",north],AXIS[\"longitude\",east],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[\"latitude\",north],AXIS[\"longitude\",east],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]]]";
+
+ [Test]
+ public void WriteGeogCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2GeogCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("WGS 84"));
+ Assert.That(reparsed.Datum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ Assert.That(reparsed.Datum.Ellipsoid.InverseFlattening, Is.EqualTo(298.257223563));
+ Assert.That(reparsed.CoordinateSystem.Dimension, Is.EqualTo(2));
+ }
+
+ [Test]
+ public void WriteProjCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(ProjCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2ProjCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("WGS 84 / UTM zone 32N"));
+ Assert.That(reparsed.Conversion.MethodName, Is.EqualTo("Transverse Mercator"));
+ Assert.That(reparsed.Conversion.Parameters, Has.Count.EqualTo(5));
+ Assert.That(reparsed.BaseCrs.Datum.Ellipsoid.SemiMajorAxis, Is.EqualTo(6378137));
+ }
+
+ [Test]
+ public void WriteVertCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(VertCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2VertCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("EGM96 height"));
+ Assert.That(reparsed.CoordinateSystem.Dimension, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void WriteEngCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(EngCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2EngCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("Local grid"));
+ Assert.That(reparsed.CoordinateSystem.Axes, Has.Count.EqualTo(2));
+ }
+
+ [Test]
+ public void WriteParametricCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(ParametricCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2ParametricCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("Sigma"));
+ Assert.That(reparsed.CoordinateSystem.Dimension, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void WriteCompoundCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(CompoundCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2CompoundCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.Name, Is.EqualTo("WGS 84 + height"));
+ Assert.That(reparsed.Components, Has.Count.EqualTo(2));
+ Assert.That(reparsed.Components[0], Is.InstanceOf());
+ Assert.That(reparsed.Components[1], Is.InstanceOf());
+ }
+
+ [Test]
+ public void WriteBoundCrs_OutputIsReparseable()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(BoundCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ var reparsed = (Wkt2BoundCrs)CoordinateSystemWkt2Reader.ParseCrs(written);
+ Assert.That(reparsed.SourceCrs, Is.InstanceOf());
+ Assert.That(reparsed.TargetCrs, Is.InstanceOf());
+ Assert.That(reparsed.Transformation.MethodName, Is.EqualTo("Geocentric translations"));
+ Assert.That(reparsed.Transformation.Parameters, Has.Count.EqualTo(3));
+ }
+
+ // --- Output format tests ---
+
+ [Test]
+ public void Write_OutputUsesCorrectKeywordCasing()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ Assert.That(written, Does.StartWith("GEOGCRS["));
+ Assert.That(written, Does.Contain("DATUM["));
+ Assert.That(written, Does.Contain("ELLIPSOID["));
+ Assert.That(written, Does.Contain("CS["));
+ Assert.That(written, Does.Contain("AXIS["));
+ }
+
+ [Test]
+ public void Write_OutputUsesInvariantDecimalPoint()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ // Should use '.' not ',' for decimals (invariant culture)
+ Assert.That(written, Does.Contain("298.257223563"));
+ Assert.That(written, Does.Not.Contain("298,257223563"));
+ }
+
+ [Test]
+ public void Write_IdIsPreserved()
+ {
+ var model = CoordinateSystemWkt2Reader.ParseCrs(GeogCrsWkt);
+ string written = CoordinateSystemWkt2Writer.Write(model);
+ // Writer formats codes as quoted strings
+ Assert.That(written, Does.Contain("ID[\"EPSG\",\"4326\"]"));
+ }
+}