Skip to content

add Arctic/Antarctic map projection and map sources for areas above and below 80 degrees#3677

Open
EosBandi wants to merge 5 commits intoArduPilot:masterfrom
EosBandi:Arctic-projection
Open

add Arctic/Antarctic map projection and map sources for areas above and below 80 degrees#3677
EosBandi wants to merge 5 commits intoArduPilot:masterfrom
EosBandi:Arctic-projection

Conversation

@EosBandi
Copy link
Collaborator

@EosBandi EosBandi commented Mar 8, 2026

Add map sources for polar regions with polar projections
NASA GIBS Arctic (EPSG:3413) - Blue Marble Shaded Relief Bathymetry
NASA GIBS Antarctic (EPSG:3031) - Blue Marble Shaded Relief
Esri Polar Arctic Imagery (EPSG:5936) 15m TerraColor satellite imagery covering 50N-90N 15m/pixel
Esri Polar Arctic Ocean Base (EPSG:5936) Marine basemap with bathymetry, shaded relief, land cover, and roads
ArcGIS Arctic Bathymetry Basemap (EPSG:3995) IBCAO v4 + GEBCO_2020 bathymetry/topography with Natural Earth land cover ~66m/pixel at max zoom

Note: Grid/FaceMap will not work in these regions since UTM is not defined.

@meee1 meee1 requested a review from Copilot March 8, 2026 19:01
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds polar-region map support by introducing Arctic/Antarctic-projection tile providers and adjusting mouse-drag map panning to behave better near pole singularities.

Changes:

  • Registers new polar map providers (NASA GIBS Arctic/Antarctic, Esri Polar Arctic, ArcGIS Arctic Bathymetry).
  • Implements new polar stereographic projections (EPSG:3413, EPSG:3031, EPSG:5936, EPSG:3995) in the GMap projection layer.
  • Updates map drag/pan behavior to use incremental pixel deltas with optional damping near high latitudes.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Program.cs Registers newly added polar map providers into the provider list.
GCSViews/FlightPlanner.cs Updates panning logic to use incremental deltas (improves behavior near poles) and stores mouse-down local point.
GCSViews/FlightData.cs Mirrors incremental-delta panning logic for the flight data map control.
ExtLibs/Maps/GIBSArctic.cs Adds NASA GIBS Arctic WMTS provider using EPSG:3413 polar projection.
ExtLibs/Maps/GIBSAntarctic.cs Adds NASA GIBS Antarctic WMTS provider using EPSG:3031 polar projection.
ExtLibs/Maps/EsriArcticImagery.cs Adds Esri Polar Arctic imagery provider using EPSG:5936 projection.
ExtLibs/Maps/EsriArcticOceanBase.cs Adds Esri Arctic ocean base provider using EPSG:5936 projection.
ExtLibs/Maps/ArcGISArcticBathymetry.cs Adds ArcGIS Arctic bathymetry provider using EPSG:3995 projection.
ExtLibs/GMap.NET.Core/GMap.NET.Projections/PolarStereographicNorthProjection.cs Introduces EPSG:3413 polar stereographic north projection (512px tiles) for GIBS Arctic.
ExtLibs/GMap.NET.Core/GMap.NET.Projections/PolarStereographicSouthProjection.cs Introduces EPSG:3031 polar stereographic south projection (512px tiles) for GIBS Antarctic.
ExtLibs/GMap.NET.Core/GMap.NET.Projections/EPSG5936Projection.cs Introduces EPSG:5936 projection matching Esri Polar Arctic tiling scheme (256px tiles).
ExtLibs/GMap.NET.Core/GMap.NET.Projections/EPSG3995Projection.cs Introduces EPSG:3995 projection matching ArcGIS Arctic Bathymetry tiling scheme (256px tiles).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@robertlong13 robertlong13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Honestly really great work getting this done so quickly and cleanly. I only have two comments. One I left separately (the "damping" thing). The other is about the midline markers.

The markers are ending up on the rhumb line instead of on straight lines on the projection. But I'd say leave this part alone for now; it's a good little hint of the actual path that ArduPilot is gonna take.

The bigger problem is the midline marker artifact when crossing the anti-meridian. This is an existing Planner issue, but polar projection makes it a lot easier to encounter. Consider the following two points: (-89.5, 155), (-89.5, -165). The rhumb line center of these is (-89.5, 175), but Planner is drawing it as (-89.5, -5) because it's taking an average lat without being wrap-aware. I checked ArduPilot, and based on my quick search it at least handles anti-meridian wrap okay, so we probably should fix this quirk. The really bad one in the following screenshot is an example of the anti-meridian issue:

Image

Copy link
Collaborator

@robertlong13 robertlong13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a couple nits, then a rebase to fixup/squash to 2 commits, and then I'd say it's good to go.

Comment on lines +2983 to +2987
// optional damping near poles
double absLat = Math.Abs(gMapControl1.Position.Lat);
if (dx == 0) dx = Math.Sign(dx);
if (dy == 0) dy = Math.Sign(dy);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable and no-op lines (Math.Sign(0) is 0)

Suggested change
// optional damping near poles
double absLat = Math.Abs(gMapControl1.Position.Lat);
if (dx == 0) dx = Math.Sign(dx);
if (dy == 0) dy = Math.Sign(dy);

Comment on lines +7550 to +7555
double absLat = Math.Abs(MainMap.Position.Lat);


if (dx == 0 ) dx = Math.Sign(dx);
if (dy == 0 ) dy = Math.Sign(dy);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Suggested change
double absLat = Math.Abs(MainMap.Position.Lat);
if (dx == 0 ) dx = Math.Sign(dx);
if (dy == 0 ) dy = Math.Sign(dy);

@meee1 meee1 requested a review from Copilot March 12, 2026 05:35
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7550 to +7555
double absLat = Math.Abs(MainMap.Position.Lat);


if (dx == 0 ) dx = Math.Sign(dx);
if (dy == 0 ) dy = Math.Sign(dy);

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absLat is computed but not used, and the dx == 0 / dy == 0 assignments are no-ops (Math.Sign(0) returns 0). This looks like leftover logic for damping/avoiding zero deltas; either implement the intended damping using absLat (e.g., scale dx/dy as latitude approaches the pole) or remove these lines to avoid misleading future readers.

Suggested change
double absLat = Math.Abs(MainMap.Position.Lat);
if (dx == 0 ) dx = Math.Sign(dx);
if (dy == 0 ) dy = Math.Sign(dy);

Copilot uses AI. Check for mistakes.
Comment on lines +2983 to +2987
// optional damping near poles
double absLat = Math.Abs(gMapControl1.Position.Lat);
if (dx == 0) dx = Math.Sign(dx);
if (dy == 0) dy = Math.Sign(dy);

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in FlightPlanner: absLat is unused, and dx == 0 / dy == 0 => Math.Sign(0) is a no-op. Either apply (and document) the intended damping based on absLat or remove these lines to keep the panning logic minimal and unambiguous.

Suggested change
// optional damping near poles
double absLat = Math.Abs(gMapControl1.Position.Lat);
if (dx == 0) dx = Math.Sign(dx);
if (dy == 0) dy = Math.Sign(dy);

Copilot uses AI. Check for mistakes.
var mid = new PointLatLngAlt((now.Lat + next.Lat) / 2, (now.Lng + next.Lng) / 2, 0);
var p1 = MainMap.FromLatLngToLocal(now);
var p2 = MainMap.FromLatLngToLocal(next);
var mid = new PointLatLngAlt(MainMap.FromLocalToLatLng((int)((p1.X + p2.X) / 2), (int)((p1.Y + p2.Y) / 2)));
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The midpoint is computed in local pixel space (good for polar regions), but the cast to int truncates (floors) the averaged pixel coordinates, introducing a systematic bias—especially noticeable when repeatedly generating midpoints (e.g., for many segments). Prefer rounding to the nearest pixel before converting back to lat/lng (e.g., Math.Round) to keep the midpoint symmetric.

Suggested change
var mid = new PointLatLngAlt(MainMap.FromLocalToLatLng((int)((p1.X + p2.X) / 2), (int)((p1.Y + p2.Y) / 2)));
var mid = new PointLatLngAlt(
MainMap.FromLocalToLatLng(
(int)Math.Round((p1.X + p2.X) / 2.0, MidpointRounding.AwayFromZero),
(int)Math.Round((p1.Y + p2.Y) / 2.0, MidpointRounding.AwayFromZero)));

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +36
Type mytype = typeof(GMapProviders);
FieldInfo field = mytype.GetField("DbHash", BindingFlags.Static | BindingFlags.NonPublic);
Dictionary<int, GMapProvider> list = (Dictionary<int, GMapProvider>)field.GetValue(Instance);

list.Add(Instance.DbId, Instance);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering providers by reflecting into GMapProviders private field DbHash is very brittle: a rename/type change will crash at type-initialization time, and this pattern is duplicated across multiple new provider classes. If possible, prefer an explicit public registration API (or a single internal helper inside the GMap.NET fork) so providers can register without private reflection; otherwise centralize this reflection in one utility with a clear failure mode (e.g., throwing a descriptive exception) to reduce operational risk and duplication.

Suggested change
Type mytype = typeof(GMapProviders);
FieldInfo field = mytype.GetField("DbHash", BindingFlags.Static | BindingFlags.NonPublic);
Dictionary<int, GMapProvider> list = (Dictionary<int, GMapProvider>)field.GetValue(Instance);
list.Add(Instance.DbId, Instance);
RegisterProvider(Instance);
}
private static void RegisterProvider(GMapProvider provider)
{
if (provider == null)
{
throw new ArgumentNullException(nameof(provider));
}
Type providersType = typeof(GMapProviders);
const string fieldName = "DbHash";
FieldInfo field = providersType.GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic);
if (field == null)
{
throw new InvalidOperationException(
$"Cannot register provider '{provider.Name}': field '{providersType.FullName}.{fieldName}' was not found. The GMap.NET library may have changed.");
}
object value = field.GetValue(null);
if (value is Dictionary<int, GMapProvider> dictionary)
{
if (!dictionary.ContainsKey(provider.DbId))
{
dictionary.Add(provider.DbId, provider);
}
}
else
{
throw new InvalidOperationException(
$"Cannot register provider '{provider.Name}': field '{providersType.FullName}.{fieldName}' is not of the expected type 'Dictionary<int, GMapProvider>'.");
}

Copilot uses AI. Check for mistakes.

// EPSG:3995 parameters
static readonly double phi_c = 71.0 * Math.PI / 180.0; // standard parallel
const double lambda_0 = 0.0; // central meridian in radians
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says lambda_0 is in radians, but it's defined as a raw 0.0 constant (and unlike phi_c, it isn't converted). While 0 is the same in degrees/radians, this is confusing for anyone extending the class to a non-zero meridian. Consider defining it as an explicit radians value (e.g., 0.0 * Math.PI / 180.0) or updating the comment to avoid unit ambiguity.

Suggested change
const double lambda_0 = 0.0; // central meridian in radians
const double lambda_0 = 0.0 * Math.PI / 180.0; // central meridian in radians

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants