I have a Google map embedded on a Blazor component. This is generated by the BlazorGoogleMaps package. My users can draw a path (set of connected lines) and search for results within a specified distance from that line. That all works fine.
What I would like to do is draw a polygon that shows the user the area that is being searched. I have a method that will generate a polygon (rectangle to be precise) that encompasses the area for a line...
I know there should also be semi-circles at each end of the rectangle. I'm having separate woes with that, so will ignore it for this question.
As you can see, when they just draw one single line, it displays fine.
However, if I do this for a path consisting of multiple lines, then the opacity is increased in the places where the rectangles overlap...
This seems to be a very well-known problem, and all the solutions I could find seem to come down to one of the methods shown in this post. That question was about circles, but the principle is the same.
The problem is that if I mimic the method for combining the polygons (getting the points and creating a new polygon with them as vertices), then the resulting polygon is not correct. My code is as follows...
List<Polygon> polygons = []; // Code to generate polygons ommitted
List<LatLngLiteral> corridorPoints = [];
foreach (Polygon polygon in polygons) {
corridorPoints.AddRange(await polygon.GetPath());
}
...however, the results are wrong....
As you can see, there is an area (surrounded highlighted by the red triangle) that should not be included. Also, there should be an edge that goes from the point marked "1" on the map to the point marked "2". However, if you look carefully, the edge actually goes to the point marked "3", which is why it goes a little south of point 2.
Anyone able to advise how I handle this?
I have a Google map embedded on a Blazor component. This is generated by the BlazorGoogleMaps package. My users can draw a path (set of connected lines) and search for results within a specified distance from that line. That all works fine.
What I would like to do is draw a polygon that shows the user the area that is being searched. I have a method that will generate a polygon (rectangle to be precise) that encompasses the area for a line...
I know there should also be semi-circles at each end of the rectangle. I'm having separate woes with that, so will ignore it for this question.
As you can see, when they just draw one single line, it displays fine.
However, if I do this for a path consisting of multiple lines, then the opacity is increased in the places where the rectangles overlap...
This seems to be a very well-known problem, and all the solutions I could find seem to come down to one of the methods shown in this post. That question was about circles, but the principle is the same.
The problem is that if I mimic the method for combining the polygons (getting the points and creating a new polygon with them as vertices), then the resulting polygon is not correct. My code is as follows...
List<Polygon> polygons = []; // Code to generate polygons ommitted
List<LatLngLiteral> corridorPoints = [];
foreach (Polygon polygon in polygons) {
corridorPoints.AddRange(await polygon.GetPath());
}
...however, the results are wrong....
As you can see, there is an area (surrounded highlighted by the red triangle) that should not be included. Also, there should be an edge that goes from the point marked "1" on the map to the point marked "2". However, if you look carefully, the edge actually goes to the point marked "3", which is why it goes a little south of point 2.
Anyone able to advise how I handle this?
Share edited Mar 10 at 8:10 MrUpsidown 22.5k15 gold badges83 silver badges141 bronze badges asked Mar 9 at 18:34 Avrohom YisroelAvrohom Yisroel 9,52210 gold badges62 silver badges133 bronze badges2 Answers
Reset to default 2I had to do something like this a while back, and spent a long time getting it wrong!
I ended up with the code shown below. This does a pretty good job, but does show distorted curves instead of perfect circles. They get better the closer you are to the equator, but can be quite poor as you get nearer the poles.
c#
/// <summary>
/// Creates a Polygon that represents the shape of the area covered by a corridor. Note that while this code does a reasonable job, but the curves are distorted. Could do with an algorithm that accounts for the curvature of the earth's surface
/// </summary>
/// <param name="points">Sequence of connected points defining the corridor</param>
/// <param name="distanceInMiles">Distance in miles from the lines to include in the polygon</param>
/// <param name="map">A reference to the GoogleMap which will house the polygon</param>
/// <returns></returns>
public static async Task<Polygon> CreateCorridorShape(List<LatLngLiteral> points, double distanceInMiles, GoogleMap map) {
List<LatLngLiteral> combinedPoints = CreatePointsForCorridorShape(points, distanceInMiles);
Polygon corridorShape = await Polygon.CreateAsync(map.JsRuntime, new PolygonOptions {
Draggable = false,
Editable = false,
Map = map.InteropObject,
FillOpacity = 0.1F,
StrokeWeight = 0,
});
await corridorShape.SetPath(combinedPoints);
return corridorShape;
}
/// <summary>
/// Creates the points for a polygon that includes all points within the specified distance of the connected lines.
/// </summary>
/// <param name="points">Sequence of connected points defining the line path</param>
/// <param name="distanceInMiles">Distance in miles from the lines to include in the polygon</param>
/// <returns>A list of LatLngLiteral points that define the boundary of the buffer polygon</returns>
private static List<LatLngLiteral> CreatePointsForCorridorShape(List<LatLngLiteral> points, double distanceInMiles) {
if (points == null || points.Count < 2) {
throw new ArgumentException("At least two points are required to create a line.", nameof(points));
}
// Create a geometric factory with high precision
GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(PrecisionModels.Floating));
// Convert the points to a NTS LineString
Coordinate[] coordinates = points.Select(p => new Coordinate(p.Lng, p.Lat)).ToArray();
LineString lineString = geometryFactory.CreateLineString(coordinates);
// Calculate the buffer distance in degrees
// This is an approximation as degrees vary in distance depending on latitude
double avgLat = points.Average(p => p.Lat);
double distanceInDegrees = MilesToDegrees(distanceInMiles, avgLat);
// Create the buffer with round end caps to get the semicircles at the ends
BufferParameters bufferParams = new BufferParameters {
EndCapStyle = EndCapStyle.Round,
JoinStyle = JoinStyle.Round,
QuadrantSegments = 16 // Controls the smoothness of curves
};
// Generate the buffer polygon
Geometry bufferedGeometry = BufferOp.Buffer(lineString, distanceInDegrees, bufferParams);
// Extract the exterior ring coordinates from the polygon
Coordinate[] exteriorRingCoordinates;
if (bufferedGeometry is EfPolygon polygon) {
exteriorRingCoordinates = polygon.ExteriorRing.Coordinates;
} else if (bufferedGeometry is MultiPolygon multiPolygon && multiPolygon.NumGeometries > 0) {
// If a MultiPolygon is returned, take the largest polygon
EfPolygon largestPolygon = (EfPolygon)multiPolygon.GetGeometryN(0);
exteriorRingCoordinates = largestPolygon.ExteriorRing.Coordinates;
} else {
throw new InvalidOperationException("Failed to create a valid polygon.");
}
// Convert the coordinates back to LatLngLiteral
List<LatLngLiteral> polygonPoints = exteriorRingCoordinates
.Select(c => new LatLngLiteral { Lat = c.Y, Lng = c.X })
.ToList();
// The last point in a ring is the same as the first, so we can remove it if needed
if (polygonPoints.Count > 1 &&
Math.Abs(polygonPoints[0].Lat - polygonPoints[polygonPoints.Count - 1].Lat) < 1e-8 &&
Math.Abs(polygonPoints[0].Lng - polygonPoints[polygonPoints.Count - 1].Lng) < 1e-8) {
polygonPoints.RemoveAt(polygonPoints.Count - 1);
}
return polygonPoints;
}
/// <summary>
/// Converts a distance in miles to an approximate equivalent in degrees at the given latitude
/// </summary>
/// <param name="miles">Distance in miles</param>
/// <param name="latitude">Latitude at which to calculate the conversion</param>
/// <returns>Approximate distance in degrees</returns>
private static double MilesToDegrees(double miles, double latitude) {
// At the equator, 1 degree is approximately 69.2 miles
// At higher latitudes, the east-west distance of 1 degree decreases
double latitudeAdjustment = Math.Cos(latitude * Math.PI / 180.0);
// Calculate approximate degrees for the given distance and latitude
// For north-south movement: 1 degree ≈ 69.1 miles
// For east-west movement: 1 degree ≈ 69.1 * cos(latitude) miles
// Since we're creating a buffer in all directions, use a compromise between
// the north-south and east-west scaling factors
double milesPerDegree = 69.1 * (1 + latitudeAdjustment) / 2;
return miles / milesPerDegree;
}
Calculating the union of polygons are not as simple as just adding all the vertices to the same list. If you where drawing lines rather than just the area you would likely see a lines going from "1" to "3", and from "1" to the top rightmost vertex.
To create a correct union you will need to find where the polygons intersect and create new vertices a these points.
There are several resources on how to do this:
- SO: How do I combine complex polygons?
- Wikipedia: Boolean operations on polygons
- 2D Polygon Boolean Operations
There are also third party libraries available that can do Union and other boolean operations. I have used Clipper2, but there most likely several others.