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