Removed MapProjection.MapToBoundingBox

This commit is contained in:
ClemensFischer 2026-01-30 22:26:51 +01:00
parent 4fa2470739
commit 5c8d000378
4 changed files with 134 additions and 129 deletions

View file

@ -44,9 +44,17 @@ namespace MapControl
var p2 = transform.Transform(new Point(bitmap.PixelWidth, bitmap.PixelHeight));
#endif
BitmapSource = bitmap;
BoundingBox = projection != null
? projection.MapToBoundingBox(new Rect(p1, p2))
: new BoundingBox(p1.Y, p1.X, p2.Y, p2.X);
if (projection != null)
{
var sw = projection.MapToLocation(p1);
var ne = projection.MapToLocation(p2);
BoundingBox = new BoundingBox(sw.Latitude, sw.Longitude, ne.Latitude, ne.Longitude);
}
else
{
BoundingBox = new BoundingBox(p1.Y, p1.X, p2.Y, p2.X);
}
}
public BitmapSource BitmapSource { get; }

View file

@ -195,11 +195,6 @@ namespace MapControl
/// </summary>
public Location ViewToLocation(Point point) => MapProjection.MapToLocation(ViewTransform.ViewToMap(point));
/// <summary>
/// Gets a BoundingBox in geographic coordinates that covers a rectangle in view coordinates.
/// </summary>
public BoundingBox ViewToBoundingBox(Rect rect) => MapProjection.MapToBoundingBox(ViewTransform.ViewToMapBounds(rect));
/// <summary>
/// Sets a temporary center point in view coordinates for scaling and rotation transformations.
/// This center point is automatically reset when the Center property is set by application code

View file

@ -34,8 +34,6 @@ namespace MapControl
public double Rotation => rotation;
}
private const double LineInterpolationResolution = 2d;
public static readonly DependencyProperty MinLineDistanceProperty =
DependencyPropertyHelper.Register<MapGraticule, double>(nameof(MinLineDistance), 150d);
@ -75,124 +73,61 @@ namespace MapControl
set => SetValue(FontSizeProperty, value);
}
private double PixelPerDegree => Math.Max(1d, ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree);
private double lineDistance;
private string labelFormat;
private void SetLineDistance()
{
var minDistance = MinLineDistance / PixelPerDegree;
var scale = minDistance < 1d / 60d ? 3600d : minDistance < 1d ? 60d : 1d;
minDistance *= scale;
var lineDistances = new double[] { 1d, 2d, 5d, 10d, 15d, 30d, 60d };
var i = 0;
while (i < lineDistances.Length - 1 && lineDistances[i] < minDistance)
{
i++;
}
lineDistance = Math.Min(lineDistances[i] / scale, 30d);
labelFormat = lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\""
: lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
}
private string GetLabelText(double value, string hemispheres)
{
var hemisphere = hemispheres[0];
if (value < -1e-8) // ~1 mm
{
value = -value;
hemisphere = hemispheres[1];
}
var seconds = (int)Math.Round(value * 3600d);
return string.Format(CultureInfo.InvariantCulture,
labelFormat, hemisphere, seconds / 3600, seconds / 60 % 60, seconds % 60);
}
private void AddLabel(List<Label> labels, double latitude, double longitude, Point position, double? rotation = null)
{
if (position.X >= 0d && position.X <= ParentMap.ActualWidth &&
position.Y >= 0d && position.Y <= ParentMap.ActualHeight)
{
if (!rotation.HasValue)
{
// Get rotation from second location with same latitude.
//
var pos = ParentMap.LocationToView(latitude, longitude + 10d / PixelPerDegree);
rotation = Math.Atan2(pos.Y - position.Y, pos.X - position.X) * 180d / Math.PI;
}
if (rotation.HasValue)
{
labels.Add(new Label(
GetLabelText(latitude, "NS"),
GetLabelText(Location.NormalizeLongitude(longitude), "EW"),
position.X, position.Y, rotation.Value));
}
}
}
private List<Label> DrawGraticule(PathFigureCollection figures)
{
var labels = new List<Label>();
figures.Clear();
SetLineDistance();
var labels = new List<Label>();
var lineDistance = GetLineDistance();
var labelFormat = lineDistance < 1d / 60d ? "{0} {1}°{2:00}'{3:00}\""
: lineDistance < 1d ? "{0} {1}°{2:00}'" : "{0} {1}°";
if (ParentMap.MapProjection.IsNormalCylindrical)
{
DrawCylindricalGraticule(figures, labels);
DrawCylindricalGraticule(figures, labels, lineDistance, labelFormat);
}
else
{
DrawGraticule(figures, labels);
DrawGraticule(figures, labels, lineDistance, labelFormat);
}
return labels;
}
private void DrawCylindricalGraticule(PathFigureCollection figures, List<Label> labels)
private void DrawCylindricalGraticule(PathFigureCollection figures, List<Label> labels, double lineDistance, string labelFormat)
{
var boundingBox = ParentMap.ViewToBoundingBox(new Rect(0d, 0d, ParentMap.ActualWidth, ParentMap.ActualHeight));
var latLabelStart = Math.Ceiling(boundingBox.South / lineDistance) * lineDistance;
var lonLabelStart = Math.Ceiling(boundingBox.West / lineDistance) * lineDistance;
var southWest = ParentMap.ViewToLocation(new Point(0d, ParentMap.ActualHeight));
var northEast = ParentMap.ViewToLocation(new Point(ParentMap.ActualWidth, 0d));
for (var lat = latLabelStart; lat <= boundingBox.North; lat += lineDistance)
var latLabelStart = Math.Ceiling(southWest.Latitude / lineDistance) * lineDistance;
var lonLabelStart = Math.Ceiling(southWest.Longitude / lineDistance) * lineDistance;
for (var lat = latLabelStart; lat <= northEast.Latitude; lat += lineDistance)
{
var p1 = ParentMap.LocationToView(lat, boundingBox.West);
var p2 = ParentMap.LocationToView(lat, boundingBox.East);
var p1 = ParentMap.LocationToView(lat, southWest.Longitude);
var p2 = ParentMap.LocationToView(lat, northEast.Longitude);
figures.Add(CreateLineFigure(p1, p2));
}
for (var lon = lonLabelStart; lon <= boundingBox.East; lon += lineDistance)
for (var lon = lonLabelStart; lon <= northEast.Longitude; lon += lineDistance)
{
var p1 = ParentMap.LocationToView(boundingBox.South, lon);
var p2 = ParentMap.LocationToView(boundingBox.North, lon);
var p1 = ParentMap.LocationToView(southWest.Latitude, lon);
var p2 = ParentMap.LocationToView(northEast.Latitude, lon);
figures.Add(CreateLineFigure(p1, p2));
for (var lat = latLabelStart; lat <= boundingBox.North; lat += lineDistance)
for (var lat = latLabelStart; lat <= northEast.Latitude; lat += lineDistance)
{
var position = ParentMap.LocationToView(lat, lon);
AddLabel(labels, lat, lon, position, ParentMap.ViewTransform.Rotation);
AddLabel(labels, labelFormat, lat, lon, ParentMap.LocationToView(lat, lon));
}
}
}
private void DrawGraticule(PathFigureCollection figures, List<Label> labels)
private void DrawGraticule(PathFigureCollection figures, List<Label> labels, double lineDistance, string labelFormat)
{
GetLatitudeRange(lineDistance, out double minLat, out double maxLat);
var latSegments = (int)Math.Round(Math.Abs(maxLat - minLat) / lineDistance);
var interpolationCount = Math.Max(1, (int)Math.Ceiling(lineDistance / LineInterpolationResolution));
var interpolationCount = Math.Max(1, (int)Math.Ceiling(lineDistance));
var interpolationDistance = lineDistance / interpolationCount;
var latPoints = latSegments * interpolationCount;
var centerLon = Math.Round(ParentMap.Center.Longitude / lineDistance) * lineDistance;
@ -221,21 +156,25 @@ namespace MapControl
var lat = minLat + i * lineDistance;
var lon = minLon;
var points = new List<Point>();
var position = ParentMap.LocationToView(lat, lon);
var mapPoint = ParentMap.MapProjection.LocationToMap(lat, lon);
var position = ParentMap.ViewTransform.MapToView(mapPoint);
var rotation = -ParentMap.MapProjection.GridConvergence(mapPoint.X, mapPoint.Y);
points.Add(position);
AddLabel(labels, lat, lon, position);
AddLabel(labels, labelFormat, lat, lon, position, rotation);
for (int j = 0; j < lonSegments; j++)
{
for (int k = 1; k <= interpolationCount; k++)
{
lon = minLon + j * lineDistance + k * interpolationDistance;
position = ParentMap.LocationToView(lat, lon);
mapPoint = ParentMap.MapProjection.LocationToMap(lat, lon);
position = ParentMap.ViewTransform.MapToView(mapPoint);
points.Add(position);
}
AddLabel(labels, lat, lon, position);
rotation = -ParentMap.MapProjection.GridConvergence(mapPoint.X, mapPoint.Y);
AddLabel(labels, labelFormat, lat, lon, position, rotation);
}
if (points.Count >= 2)
@ -262,7 +201,7 @@ namespace MapControl
points.Add(p);
}
if (points.Count >= 2)
if (visible && points.Count >= 2)
{
figures.Add(CreatePolylineFigure(points));
}
@ -274,19 +213,31 @@ namespace MapControl
{
var width = ParentMap.ActualWidth;
var height = ParentMap.ActualHeight;
var locations = new Location[]
var locations = new List<Location>
{
ParentMap.ViewToLocation(new Point(0d, 0d)),
ParentMap.ViewToLocation(new Point(width / 2d, 0d)),
ParentMap.ViewToLocation(new Point(width, 0d)),
ParentMap.ViewToLocation(new Point(width, height / 2d)),
ParentMap.ViewToLocation(new Point(width, height)),
ParentMap.ViewToLocation(new Point(width / 2d, height)),
ParentMap.ViewToLocation(new Point(0d, height)),
ParentMap.ViewToLocation(new Point(0d, height / 2)),
ParentMap.ViewToLocation(new Point(width, height)),
ParentMap.ViewToLocation(new Point(width / 2d, 0d)),
ParentMap.ViewToLocation(new Point(width / 2d, height)),
ParentMap.ViewToLocation(new Point(0d, height / 2d)),
ParentMap.ViewToLocation(new Point(width, height / 2d)),
};
var latitudes = locations.Where(loc => loc != null).Select(loc => loc.Latitude).Distinct();
var pole = ParentMap.LocationToView(90d, 0d);
if (pole.X >= 0d && pole.X <= width && pole.Y >= 0d && pole.Y <= height)
{
locations.Add(new Location(90d, 0d));
}
pole = ParentMap.LocationToView(-90d, 0d);
if (pole.X >= 0d && pole.X <= width && pole.Y >= 0d && pole.Y <= height)
{
locations.Add(new Location(-90d, 0d));
}
var latitudes = locations.Select(loc => loc.Latitude).Distinct();
var south = -90d;
var north = 90d;
@ -296,8 +247,71 @@ namespace MapControl
north = latitudes.Max();
}
minLatitude = Math.Max(Math.Floor(south / lineDistance) * lineDistance, -90d);
maxLatitude = Math.Min(Math.Ceiling(north / lineDistance) * lineDistance, 90d);
minLatitude = Math.Max(Math.Floor(south / lineDistance - 1d) * lineDistance, -90d);
maxLatitude = Math.Min(Math.Ceiling(north / lineDistance + 1d) * lineDistance, 90d);
}
private double GetLineDistance()
{
var pixelPerDegree = ParentMap.ViewTransform.Scale * MapProjection.Wgs84MeterPerDegree;
if (!ParentMap.MapProjection.IsNormalCylindrical)
{
pixelPerDegree *= Math.Cos(ParentMap.Center.Latitude * Math.PI / 180d);
}
var minDistance = MinLineDistance / Math.Max(1d, pixelPerDegree);
var scale = minDistance < 1d / 60d ? 3600d : minDistance < 1d ? 60d : 1d;
minDistance *= scale;
var lineDistances = new double[] { 1d, 2d, 5d, 10d, 15d, 30d, 60d };
var i = 0;
while (i < lineDistances.Length - 1 && lineDistances[i] < minDistance)
{
i++;
}
return Math.Min(lineDistances[i] / scale, 60d);
}
private void AddLabel(List<Label> labels, string labelFormat, double latitude, double longitude, Point position, double rotation = 0d)
{
if (position.X >= 0d && position.X <= ParentMap.ActualWidth &&
position.Y >= 0d && position.Y <= ParentMap.ActualHeight)
{
rotation = (rotation + ParentMap.ViewTransform.Rotation) % 360d;
if (rotation < -90d)
{
rotation += 180d;
}
else if (rotation > 90d)
{
rotation -= 180d;
}
labels.Add(new Label(
GetLabelText(latitude, labelFormat, "NS"),
GetLabelText(Location.NormalizeLongitude(longitude), labelFormat, "EW"),
position.X, position.Y, rotation));
}
}
private static string GetLabelText(double value, string labelFormat, string hemispheres)
{
var hemisphere = hemispheres[0];
if (value < -1e-8) // ~1 mm
{
value = -value;
hemisphere = hemispheres[1];
}
var seconds = (int)Math.Round(value * 3600d);
return string.Format(CultureInfo.InvariantCulture,
labelFormat, hemisphere, seconds / 3600, seconds / 60 % 60, seconds % 60);
}
private static PathFigure CreateLineFigure(Point p1, Point p2)

View file

@ -59,13 +59,6 @@ namespace MapControl
/// </summary>
public abstract Matrix RelativeTransform(double latitude, double longitude);
/// <summary>
/// Gets the grid convergence angle in degrees at the specified projected map coordinates.
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
/// projections, i.e. Transverse Mercator and Polar Stereographic.
/// </summary>
public virtual double GridConvergence(double x, double y) => 0d;
/// <summary>
/// Transforms geographic coordinates to a Point in projected map coordinates.
/// </summary>
@ -76,6 +69,13 @@ namespace MapControl
/// </summary>
public abstract Location MapToLocation(double x, double y);
/// <summary>
/// Gets the grid convergence angle in degrees at the specified projected map coordinates.
/// Used for rotating the Rect resulting from BoundingBoxToMap in non-normal-cylindrical
/// projections, i.e. Transverse Mercator and Polar Stereographic.
/// </summary>
public virtual double GridConvergence(double x, double y) => 0d;
/// <summary>
/// Gets the relative transform at the specified geographic Location.
/// </summary>
@ -93,7 +93,7 @@ namespace MapControl
/// <summary>
/// Transforms a BoundingBox in geographic coordinates to a Rect in projected map coordinates
/// with an optional rotation angle in degrees.
/// with an optional rotation angle in degrees for non-normal-cylindrical projections.
/// </summary>
public (Rect, double) BoundingBoxToMap(BoundingBox boundingBox)
{
@ -124,24 +124,12 @@ namespace MapControl
var height = Math.Sqrt(dx * dx + dy * dy);
rect = new Rect(centerX - width / 2d, centerY - height / 2d, width, height);
rotation = -GridConvergence(centerX, centerY); // invert direction for RotateTransform
}
return (rect, rotation);
}
/// <summary>
/// Transforms a Rect in projected map coordinates to a BoundingBox in geographic coordinates.
/// </summary>
public BoundingBox MapToBoundingBox(Rect rect)
{
var southWest = MapToLocation(rect.X, rect.Y);
var northEast = MapToLocation(rect.X + rect.Width, rect.Y + rect.Height);
return new BoundingBox(southWest.Latitude, southWest.Longitude, northEast.Latitude, northEast.Longitude);
}
public override string ToString()
{
return CrsId;