add Arctic/Antarctic map projection and map sources for areas above and below 80 degrees#3677
add Arctic/Antarctic map projection and map sources for areas above and below 80 degrees#3677EosBandi wants to merge 5 commits intoArduPilot:masterfrom
Conversation
There was a problem hiding this comment.
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.
robertlong13
left a comment
There was a problem hiding this comment.
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:
robertlong13
left a comment
There was a problem hiding this comment.
LGTM, just a couple nits, then a rebase to fixup/squash to 2 commits, and then I'd say it's good to go.
| // 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); | ||
|
|
There was a problem hiding this comment.
Unused variable and no-op lines (Math.Sign(0) is 0)
| // 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); | |
| double absLat = Math.Abs(MainMap.Position.Lat); | ||
|
|
||
|
|
||
| if (dx == 0 ) dx = Math.Sign(dx); | ||
| if (dy == 0 ) dy = Math.Sign(dy); | ||
|
|
There was a problem hiding this comment.
Same as above
| double absLat = Math.Abs(MainMap.Position.Lat); | |
| if (dx == 0 ) dx = Math.Sign(dx); | |
| if (dy == 0 ) dy = Math.Sign(dy); | |
There was a problem hiding this comment.
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.
| double absLat = Math.Abs(MainMap.Position.Lat); | ||
|
|
||
|
|
||
| if (dx == 0 ) dx = Math.Sign(dx); | ||
| if (dy == 0 ) dy = Math.Sign(dy); | ||
|
|
There was a problem hiding this comment.
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.
| double absLat = Math.Abs(MainMap.Position.Lat); | |
| if (dx == 0 ) dx = Math.Sign(dx); | |
| if (dy == 0 ) dy = Math.Sign(dy); | |
| // 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); | ||
|
|
There was a problem hiding this comment.
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.
| // 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); | |
| 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))); |
There was a problem hiding this comment.
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.
| 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))); |
| 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); |
There was a problem hiding this comment.
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.
| 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>'."); | |
| } |
|
|
||
| // 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 |
There was a problem hiding this comment.
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.
| const double lambda_0 = 0.0; // central meridian in radians | |
| const double lambda_0 = 0.0 * Math.PI / 180.0; // central meridian in radians |
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.