Add map projections

Add TransverseMercatorProjection, PolarStereographicProjection and derived UTM/UPS projections to the base library.
This commit is contained in:
ClemensFischer 2022-12-13 18:22:18 +01:00
parent 568f55feb0
commit e45180b26a
18 changed files with 647 additions and 332 deletions

View file

@ -15,8 +15,8 @@ namespace MapControl.Projections
throw new ArgumentException($"Invalid UTM zone {zone}.", nameof(zone));
}
const string wktFormat
= "PROJCS[\"ED50 / UTM zone {0}N\","
CoordinateSystemWkt
= $"PROJCS[\"ED50 / UTM zone {zone}N\","
+ "GEOGCS[\"ED50\","
+ "DATUM[\"European_Datum_1950\","
+ "SPHEROID[\"International 1924\",6378388,297,"
@ -30,7 +30,7 @@ namespace MapControl.Projections
+ "AUTHORITY[\"EPSG\",\"4230\"]],"
+ "PROJECTION[\"Transverse_Mercator\"],"
+ "PARAMETER[\"latitude_of_origin\",0],"
+ "PARAMETER[\"central_meridian\",{1}],"
+ $"PARAMETER[\"central_meridian\",{6 * zone - 183}],"
+ "PARAMETER[\"scale_factor\",0.9996],"
+ "PARAMETER[\"false_easting\",500000],"
+ "PARAMETER[\"false_northing\",0],"
@ -38,9 +38,7 @@ namespace MapControl.Projections
+ "AUTHORITY[\"EPSG\",\"9001\"]],"
+ "AXIS[\"Easting\",EAST],"
+ "AXIS[\"Northing\",NORTH],"
+ "AUTHORITY[\"EPSG\",\"230{0}\"]]";
CoordinateSystemWkt = string.Format(wktFormat, zone, 6 * zone - 183);
+ $"AUTHORITY[\"EPSG\",\"230{zone}\"]]";
}
}
}

View file

@ -15,8 +15,8 @@ namespace MapControl.Projections
throw new ArgumentException($"Invalid UTM zone {zone}.", nameof(zone));
}
const string wktFormat
= "PROJCS[\"ETRS89 / UTM zone {0}N\","
CoordinateSystemWkt
= $"PROJCS[\"ETRS89 / UTM zone {zone}N\","
+ "GEOGCS[\"ETRS89\","
+ "DATUM[\"European_Terrestrial_Reference_System_1989\","
+ "SPHEROID[\"GRS 1980\",6378137,298.257222101,"
@ -30,7 +30,7 @@ namespace MapControl.Projections
+ "AUTHORITY[\"EPSG\",\"4258\"]],"
+ "PROJECTION[\"Transverse_Mercator\"],"
+ "PARAMETER[\"latitude_of_origin\",0],"
+ "PARAMETER[\"central_meridian\",{1}],"
+ $"PARAMETER[\"central_meridian\",{6 * zone - 183}],"
+ "PARAMETER[\"scale_factor\",0.9996],"
+ "PARAMETER[\"false_easting\",500000],"
+ "PARAMETER[\"false_northing\",0],"
@ -38,9 +38,7 @@ namespace MapControl.Projections
+ "AUTHORITY[\"EPSG\",\"9001\"]],"
+ "AXIS[\"Easting\",EAST],"
+ "AXIS[\"Northing\",NORTH],"
+ "AUTHORITY[\"EPSG\",\"258{0}\"]]";
CoordinateSystemWkt = string.Format(wktFormat, zone, 6 * zone - 183);
+ $"AUTHORITY[\"EPSG\",\"258{zone}\"]]";
}
}
}

View file

@ -9,11 +9,7 @@ using ProjNet.CoordinateSystems;
using ProjNet.CoordinateSystems.Transformations;
using System;
using System.Globalization;
#if WINUI
using Windows.Foundation;
#elif UWP
using Windows.Foundation;
#else
#if !WINUI && !UWP
using System.Windows;
#endif
@ -24,9 +20,7 @@ namespace MapControl.Projections
/// </summary>
public class GeoApiProjection : MapProjection
{
private ICoordinateSystem coordinateSystem;
private double scaleFactor;
private string bboxFormat;
private IProjectedCoordinateSystem coordinateSystem;
protected GeoApiProjection()
{
@ -45,13 +39,13 @@ namespace MapControl.Projections
public string CoordinateSystemWkt
{
get => CoordinateSystem?.WKT;
protected set => CoordinateSystem = new CoordinateSystemFactory().CreateFromWkt(value);
protected set => CoordinateSystem = new CoordinateSystemFactory().CreateFromWkt(value) as IProjectedCoordinateSystem;
}
/// <summary>
/// Gets or sets the ICoordinateSystem of the MapProjection.
/// </summary>
public ICoordinateSystem CoordinateSystem
public IProjectedCoordinateSystem CoordinateSystem
{
get => coordinateSystem;
protected set
@ -72,42 +66,31 @@ namespace MapControl.Projections
? string.Format("{0}:{1}", coordinateSystem.Authority, coordinateSystem.AuthorityCode)
: "";
var projection = (coordinateSystem as IProjectedCoordinateSystem)?.Projection;
if (projection != null)
if (CrsId == "EPSG:3857")
{
Type = MapProjectionType.WebMercator;
}
else
{
var projection = coordinateSystem.Projection
?? throw new ArgumentException("CoordinateSystem.Projection must not be null.", nameof(value));
var centralMeridian = projection.GetParameter("central_meridian") ?? projection.GetParameter("longitude_of_origin");
var centralParallel = projection.GetParameter("central_parallel") ?? projection.GetParameter("latitude_of_origin");
var falseEasting = projection.GetParameter("false_easting");
var falseNorthing = projection.GetParameter("false_northing");
if (CrsId == "EPSG:3857")
{
Type = MapProjectionType.WebMercator;
}
else if (
(centralMeridian == null || centralMeridian.Value == 0d) &&
if ((centralMeridian == null || centralMeridian.Value == 0d) &&
(centralParallel == null || centralParallel.Value == 0d) &&
(falseEasting == null || falseEasting.Value == 0d) &&
(falseNorthing == null || falseNorthing.Value == 0d))
{
Type = MapProjectionType.NormalCylindrical;
}
else if (
projection.Name.StartsWith("UTM") ||
projection.Name.StartsWith("Transverse"))
else if (projection.Name.StartsWith("UTM") || projection.Name.StartsWith("Transverse"))
{
Type = MapProjectionType.TransverseCylindrical;
}
scaleFactor = 1d;
bboxFormat = "{0},{1},{2},{3}";
}
else
{
Type = MapProjectionType.NormalCylindrical;
scaleFactor = Wgs84MeterPerDegree;
bboxFormat = "{1},{0},{3},{2}";
}
}
}
@ -123,15 +106,14 @@ namespace MapControl.Projections
throw new InvalidOperationException("The CoordinateSystem property is not set.");
}
var coordinate = LocationToMapTransform.Transform(
new Coordinate(location.Longitude, location.Latitude));
var coordinate = LocationToMapTransform.Transform(new Coordinate(location.Longitude, location.Latitude));
if (coordinate == null)
{
return null;
}
return new Point(coordinate.X * scaleFactor, coordinate.Y * scaleFactor);
return new Point(coordinate.X, coordinate.Y);
}
public override Location MapToLocation(Point point)
@ -141,19 +123,15 @@ namespace MapControl.Projections
throw new InvalidOperationException("The CoordinateSystem property is not set.");
}
var coordinate = MapToLocationTransform.Transform(
new Coordinate(point.X / scaleFactor, point.Y / scaleFactor));
var coordinate = MapToLocationTransform.Transform(new Coordinate(point.X, point.Y));
return new Location(coordinate.Y, coordinate.X);
}
public override string GetBboxValue(MapRect rect)
{
return string.Format(CultureInfo.InvariantCulture, bboxFormat,
rect.XMin / scaleFactor,
rect.YMin / scaleFactor,
rect.XMax / scaleFactor,
rect.YMax / scaleFactor);
return string.Format(CultureInfo.InvariantCulture,
"{0},{1},{2},{3}", rect.XMin, rect.YMin, rect.XMax, rect.YMax);
}
}
}

View file

@ -8,79 +8,75 @@ namespace MapControl.Projections
{
public class GeoApiProjectionFactory : MapProjectionFactory
{
public const int WorldMercator = 3395;
public const int WebMercator = 3857;
public const int AutoUtm = 42001;
public const int Ed50UtmFirst = 23028;
public const int Ed50UtmLast = 23038;
public const int Etrs89UtmFirst = 25828;
public const int Etrs89UtmLast = 25838;
public const int Wgs84UtmNorthFirst = 32601;
public const int Wgs84UtmNorthLast = 32660;
public const int Wgs84UpsNorth = 32661;
public const int Wgs84UtmSouthFirst = 32701;
public const int Wgs84UtmSouthLast = 32760;
public const int Wgs84UpsSouth = 32761;
private const int WorldMercator = 3395;
private const int WebMercator = 3857;
private const int Ed50UtmFirst = 23028;
private const int Ed50UtmLast = 23038;
private const int Etrs89UtmFirst = 25828;
private const int Etrs89UtmLast = 25838;
private const int Wgs84UtmNorthFirst = 32601;
private const int Wgs84UtmNorthLast = 32660;
private const int Wgs84UtmSouthFirst = 32701;
private const int Wgs84UtmSouthLast = 32760;
public Dictionary<int, string> CoordinateSystemWkts { get; } = new Dictionary<int, string>();
public override MapProjection GetProjection(int epsgCode)
{
MapProjection projection = null;
if (CoordinateSystemWkts.TryGetValue(epsgCode, out string wkt))
{
projection = new GeoApiProjection(wkt);
}
else
{
switch (epsgCode)
{
case WorldMercator:
projection = new WorldMercatorProjection();
break;
case WebMercator:
projection = new WebMercatorProjection();
break;
case int c when c >= Ed50UtmFirst && c <= Ed50UtmLast:
projection = new Ed50UtmProjection(epsgCode % 100);
break;
case int c when c >= Etrs89UtmFirst && c <= Etrs89UtmLast:
projection = new Etrs89UtmProjection(epsgCode % 100);
break;
case int c when c >= Wgs84UtmNorthFirst && c <= Wgs84UtmNorthLast:
projection = new Wgs84UtmProjection(epsgCode % 100, true);
break;
case int c when c >= Wgs84UtmSouthFirst && c <= Wgs84UtmSouthLast:
projection = new Wgs84UtmProjection(epsgCode % 100, false);
break;
default:
break;
}
}
return projection ?? base.GetProjection(epsgCode);
}
public override MapProjection GetProjection(string crsId)
{
MapProjection projection = null;
var str = crsId.StartsWith("EPSG:") ? crsId.Substring(5)
: crsId.StartsWith("AUTO2:") ? crsId.Substring(6)
: null;
if (int.TryParse(str, out int code))
switch (crsId)
{
if (CoordinateSystemWkts.TryGetValue(code, out string wkt))
{
projection = new GeoApiProjection(wkt);
}
else
{
switch (code)
{
case WorldMercator:
projection = new WorldMercatorProjection();
break;
case Wgs84AutoUtmProjection.DefaultCrsId:
projection = new Wgs84AutoUtmProjection();
break;
case WebMercator:
projection = new WebMercatorProjection();
break;
case AutoUtm:
projection = new AutoUtmProjection();
break;
case int c when c >= Ed50UtmFirst && c <= Ed50UtmLast:
projection = new Ed50UtmProjection(code % 100);
break;
case int c when c >= Etrs89UtmFirst && c <= Etrs89UtmLast:
projection = new Etrs89UtmProjection(code % 100);
break;
case int c when c >= Wgs84UtmNorthFirst && c <= Wgs84UtmNorthLast:
projection = new Wgs84UtmProjection(code % 100, true);
break;
case int c when c >= Wgs84UtmSouthFirst && c <= Wgs84UtmSouthLast:
projection = new Wgs84UtmProjection(code % 100, false);
break;
case Wgs84UpsNorth:
projection = new UpsNorthProjection();
break;
case Wgs84UpsSouth:
projection = new UpsSouthProjection();
break;
default:
break;
}
}
default:
break;
}
return projection ?? base.GetProjection(crsId);

View file

@ -1,142 +0,0 @@
// XAML Map Control - https://github.com/ClemensFischer/XAML-Map-Control
// © 2022 Clemens Fischer
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if !UWP
using System.Windows;
#endif
namespace MapControl.Projections
{
/// <summary>
/// Elliptical Polar Stereographic Projection with a given scale factor at the pole and
/// optional false easting and northing, as used by the UPS North and UPS South projections.
/// See "Map Projections - A Working Manual" (https://pubs.usgs.gov/pp/1395/report.pdf), p.160-162.
/// </summary>
public class PolarStereographicProjection : MapProjection
{
private const double ConvergenceTolerance = 1e-6;
private const int MaxIterations = 10;
public bool IsNorth { get; }
public double ScaleFactor { get; }
public double FalseEasting { get; }
public double FalseNorthing { get; }
public PolarStereographicProjection(bool isNorth, double scaleFactor, double falseEasting, double falseNorthing)
{
Type = MapProjectionType.Azimuthal;
IsNorth = isNorth;
ScaleFactor = scaleFactor;
FalseEasting = falseEasting;
FalseNorthing = falseNorthing;
}
public override Scale GetRelativeScale(Location location)
{
var lat = (IsNorth ? location.Latitude : -location.Latitude) * Math.PI / 180d;
var a = Wgs84EquatorialRadius;
var e = Wgs84Eccentricity;
var s = Math.Sqrt(Math.Pow(1 + e, 1 + e) * Math.Pow(1 - e, 1 - e));
var t = Math.Tan(Math.PI / 4d - lat / 2d) / ConformalFactor(lat);
var rho = 2d * a * ScaleFactor * t / s;
var eSinLat = e * Math.Sin(lat);
var m = Math.Cos(lat) / Math.Sqrt(1d - eSinLat * eSinLat);
var k = rho / (a * m);
return new Scale(k, k);
}
public override Point? LocationToMap(Location location)
{
var lat = location.Latitude * Math.PI / 180d;
var lon = location.Longitude * Math.PI / 180d;
if (IsNorth)
{
lon = Math.PI - lon;
}
else
{
lat = -lat;
}
var a = Wgs84EquatorialRadius;
var e = Wgs84Eccentricity;
var s = Math.Sqrt(Math.Pow(1 + e, 1 + e) * Math.Pow(1 - e, 1 - e));
var t = Math.Tan(Math.PI / 4d - lat / 2d) / ConformalFactor(lat);
var rho = 2d * a * ScaleFactor * t / s;
return new Point(rho * Math.Sin(lon) + FalseEasting, rho * Math.Cos(lon) + FalseNorthing);
}
public override Location MapToLocation(Point point)
{
point.X -= FalseEasting;
point.Y -= FalseNorthing;
var lon = Math.Atan2(point.X, point.Y);
var rho = Math.Sqrt(point.X * point.X + point.Y * point.Y);
var a = Wgs84EquatorialRadius;
var e = Wgs84Eccentricity;
var s = Math.Sqrt(Math.Pow(1 + e, 1 + e) * Math.Pow(1 - e, 1 - e));
var t = rho * s / (2d * a * ScaleFactor);
var lat = Math.PI / 2d - 2d * Math.Atan(t);
var relChange = 1d;
for (int i = 0; i < MaxIterations && relChange > ConvergenceTolerance; i++)
{
var newLat = Math.PI / 2d - 2d * Math.Atan(t * ConformalFactor(lat));
relChange = Math.Abs(1d - newLat / lat);
lat = newLat;
}
if (IsNorth)
{
lon = Math.PI - lon;
}
else
{
lat = -lat;
}
return new Location(lat * 180d / Math.PI, lon * 180d / Math.PI);
}
private static double ConformalFactor(double lat)
{
var eSinLat = Wgs84Eccentricity * Math.Sin(lat);
return Math.Pow((1d - eSinLat) / (1d + eSinLat), Wgs84Eccentricity / 2d);
}
}
/// <summary>
/// Universal Polar Stereographic North Projection - EPSG:32661.
/// </summary>
public class UpsNorthProjection : PolarStereographicProjection
{
public const string DefaultCrsId = "EPSG:32661";
public UpsNorthProjection()
: base(true, 0.994, 2e6, 2e6)
{
CrsId = DefaultCrsId;
}
}
/// <summary>
/// Universal Polar Stereographic South Projection - EPSG:32761.
/// </summary>
public class UpsSouthProjection : PolarStereographicProjection
{
public const string DefaultCrsId = "EPSG:32761";
public UpsSouthProjection()
: base(false, 0.994, 2e6, 2e6)
{
CrsId = DefaultCrsId;
}
}
}

View file

@ -4,9 +4,6 @@
using ProjNet.CoordinateSystems;
using System;
#if !UWP
using System.Windows;
#endif
namespace MapControl.Projections
{

View file

@ -4,50 +4,47 @@
using ProjNet.CoordinateSystems;
using System;
using System.Windows;
namespace MapControl.Projections
{
public class AutoUtmProjection : GeoApiProjection
public class Wgs84AutoUtmProjection : GeoApiProjection
{
public const string DefaultCrsId = "AUTO2:42001";
public int ZoneNumber { get; private set; }
public bool ZoneIsNorth { get; private set; }
public AutoUtmProjection()
public Wgs84AutoUtmProjection(bool useZoneCrsId = false)
{
UseZoneCrsId = useZoneCrsId;
UpdateZone();
}
public bool UseZoneCrsId { get; set; }
public int Zone { get; private set; }
public bool IsNorth { get; private set; }
public bool UseZoneCrsId { get; }
public override Point? LocationToMap(Location location)
public override Location Center
{
UpdateZone();
return base.LocationToMap(location);
}
public override Location MapToLocation(Point point)
{
UpdateZone();
return base.MapToLocation(point);
get => base.Center;
set
{
if (!Equals(base.Center, value))
{
base.Center = value;
UpdateZone();
}
}
}
private void UpdateZone()
{
var north = Center.Latitude >= 0d;
var lon = Location.NormalizeLongitude(Center.Longitude);
var zone = (int)Math.Floor(lon / 6d) + 31;
var north = Center.Latitude >= 0d;
if (ZoneNumber != zone || ZoneIsNorth != north)
if (Zone != zone || IsNorth != north)
{
ZoneNumber = zone;
ZoneIsNorth = north;
CoordinateSystem = ProjectedCoordinateSystem.WGS84_UTM(ZoneNumber, ZoneIsNorth);
Zone = zone;
IsNorth = north;
CoordinateSystem = ProjectedCoordinateSystem.WGS84_UTM(Zone, IsNorth);
if (!UseZoneCrsId)
{

View file

@ -3,9 +3,6 @@
// Licensed under the Microsoft Public License (Ms-PL)
using System;
#if !UWP
using System.Windows;
#endif
namespace MapControl.Projections
{