flutter_map icon indicating copy to clipboard operation
flutter_map copied to clipboard

Re-calculate polyline stroke width in meters more frequently along path

Open ReinisSprogis opened this issue 8 months ago • 19 comments

What is the bug?

Hi. When setting useStrokeWidthInMeter = true on a Polyline, the stroke width does not reflect accurate real-world meter sizing. This appears to be due to a lack of correction for projection distortion, particularly with respect to latitude. Circle elements do correctly account for projection distortion and render real-world dimensions accurately. For example, using identical size settings (e.g., radius or stroke width in meters), the rendered sizes only match near 0° longitude.

In the attached image: Blue line: Polyline with useStrokeWidthInMeter = true Black circle: Circle with radius in meters Both were configured to the same dimension in meters, but only align visually near the equator or 0° longitude.

Expected Behavior: The Polyline should account for projection distortion (like Circle does) so that stroke width in meters reflects consistent real-world size across different latitudes.

Image

How can we reproduce it?

  List<LatLng> line = [];
List<CircleMarker> circles = [];
...
//flutter map
 onPointerHover: (event, point) {
      line.add(point);
      circles.add( 
          CircleMarker(point: point,color: Colors.black.withAlpha(50), useRadiusInMeter:true, radius: 10000, ),
       );
       setState(() {});
    }
...
///layers 
 PolylineLayer( polylines: [Polyline(points: line, color: Colors.blue,useStrokeWidthInMeter: true, strokeWidth: 20000.0,)], ),
 CircleLayer(circles: circles),

Do you have a potential solution?

This isn’t a simple fix because a single lineWidth can’t represent a consistent real-world size across different latitudes — metres-per-pixel changes with latitude (due to projection distortion). If two points are added from -90 to +90, it would be fixed size line, so need to split it into segments and scale each segment to real world size. That could cause steps if segments are too long. So maybe need to make polyline out of vertices.

ReinisSprogis avatar Jun 26 '25 15:06 ReinisSprogis

@ReinisSprogis A relatively easy solution would be to replace each existing displayed segment by the equivalent trapezoid, with a different size at the start and at the end, depending on the start/end latitudes. That's based on a linear approximation: would that be good enough?

monsieurtanuki avatar Jun 26 '25 16:06 monsieurtanuki

Hi. @monsieurtanuki Yes, that's actually what I meant when I said "make a polyline out of vertices." I haven’t looked closely at how Polyline is currently drawn, but I assumed it’s just a Path rendered via CustomPainter. If it's already constructed from vertices, then this might be easier to address. Need to figure out how many segments are needed. Since the line must maintain a constant real-world width. Need to insert enough intermediate points so that the line visually maintains the correct width everywhere, even near the poles where distortion is stronger. The line width itself also affects how many subdivisions are required. The wider the line, the more segments may be needed to prevent visible distortion.

ReinisSprogis avatar Jun 26 '25 17:06 ReinisSprogis

Oh, I meant it would be relatively easy if we used the already existing segments and if we considered computing the correct "meter width" for the already existing vertices, admitting it's a fair approximation. You seem to need an extra "split the segments" step so that the segments are "small enough".

monsieurtanuki avatar Jun 26 '25 17:06 monsieurtanuki

It would improve if there are enough segments. But what if there are only 2 points from -90 to +90 Thats only one segment.

This is from circles, but PolyLine with useStrokeWidthInMeter = true should look similar. Otherwise it is incorrect. Or that parameter needs renaming to something like scaleOnZoom. But otherwise it would need to be divided into more segments to maintain accuracy. I don't see other way, unless canvas can be skewed somehow.

Image

ReinisSprogis avatar Jun 26 '25 17:06 ReinisSprogis

@ReinisSprogis You're right. What I mean is that we probably shouldn't jeopardize the "normal" use-cases just for your extreme -90/90 polyline. That's why I suggest to keep the current behavior as the baseline, while adding parameters to polyline:

  • "split me into segments not bigger than a specified distance" - that would probably cost a lot, so we need to make it an option.
  • "compute the stroke width at both segment ends and display a trapezoid". That would probably cost a lot too.

monsieurtanuki avatar Jun 28 '25 07:06 monsieurtanuki

Just had a look at the code displaying polylines. The thing is that we work most of the time on the projected points, i.e. pixels (as Offsets). That would mean we would have to add the latitude to the projected points, and compute from pixels the interpolated latitude of segments intersecting the screen border.

Perhaps a solution would be to create a new polyline pattern style (as existing DottedPixelHiker), that would say:

  • keep the latitude with the projected points
  • every x latitude change, create a new segment and display a trapezoid

Doing so we wouldn't impact the typical use-cases of polylines.

monsieurtanuki avatar Jul 06 '25 10:07 monsieurtanuki

I'm not sure to be honest what the solution is. I looked at the code. It uses ui.Path. It doesn't have variable width. Currently setting width based on first point added latitude. How would you draw a trapezoid?

ReinisSprogis avatar Jul 06 '25 18:07 ReinisSprogis

Instead of saying that point A and B are linked by a line that has a given width (computed once):

  • computing the width widthA in point A
  • computing the width widthB in point B
  • for the record in both cases we need the latitude, and in both cases at this level we're already at the projected=pixel level, not a the LatLng level anymore
  • computing the trapezoid vertices
    • two vertices at point A, of the segment centered on A, perpendicular to segment AB, and with a width of widthA
    • two vertices at point B, of the segment centered on B, perpendicular to segment AB, and with a width of widthB
    • or something like that for side-effect, like a strokeWidth of min(widthA, widthB) and just a triangle

monsieurtanuki avatar Jul 06 '25 18:07 monsieurtanuki

Yes, drawing path with vertices would resolve this. But you will lose all the styling options.

ReinisSprogis avatar Jul 06 '25 18:07 ReinisSprogis

Yes, drawing path with vertices would resolve this. But you will lose all the styling options.

Indeed.

monsieurtanuki avatar Jul 07 '25 08:07 monsieurtanuki

@ReinisSprogis Or maybe tweaking a bit the dotted or dashed styles. The idea would be to recompute the width for each dot or segment. That would be heavy because we'd need to unproject each dot or segment middle, in order to get the latitude, then the width in meters. That would be particularly heavy in dashed style because we'd need to create a new path for each segment (or a distinct "draw single line"). Currently we compute one single width, and add all segments to the same path that will be displayed with the same width.

monsieurtanuki avatar Jul 09 '25 06:07 monsieurtanuki

@monsieurtanuki I don't think there's a way to get metre accurate stroke widths without switching to vertices. Draw a triangle/quad strip and style it with shaders. That’s the only viable path I see. The upside is that it would give option for richer styling, higher performance than Path stroking, and near-pixel accuracy at any latitude. Each segment can carry its own gradient (for example to visualise speed along the path), and dashes, dots, and borders come almost “for free” in the fragment shader.

  1. Triangulate the polyline, add optional round joins/caps, minimise extra vertices where performance matters.
  2. Break the path where latitude changes, recompute metres to pixels per vertex, and clamp near the poles. Or draw at constant width.
  3. Create styling with fragment shaders. Also need to do the many worlds thing.

All that would be quite a challange to implement.

ReinisSprogis avatar Jul 09 '25 07:07 ReinisSprogis

All that would be quite a challange to implement.

https://en.wikipedia.org/wiki/English_understatement

That would mean rewriting all the polyline code. Only for more accurate meters in very specific cases, that doesn't seem worth the candle. That said, we used to code something vaguely similar in https://github.com/osmdroid/osmdroid/, where we attached values to segments, and changed the display accordingly:

  • https://www.youtube.com/watch?v=nl3AB052_tE
  • https://github.com/osmdroid/osmdroid/wiki/Milestones-feature
  • https://github.com/osmdroid/osmdroid/wiki/Polyline-advanced-display-styles
  • https://github.com/osmdroid/osmdroid/issues/1378
Image

monsieurtanuki avatar Jul 09 '25 08:07 monsieurtanuki

Its not only for accurate meters. But also would bring better perfomance. Some time ago I did quick test. I could animate 275K circles with 60fps over flutter map from random LatLng to random LatLng by drawing points with vertices. https://www.youtube.com/watch?v=AXeG4Y1sR1E Im sure it would increase performanse for lines as well. As I have done similar in one of my projects where I needed to rotate path around center point as if in 3D by Quaternion rotating XYZ coordinates every frame. Nothing could give me performance I needed the only way was to draw path with Vertices. Thats tens of thousnds of points smoothly rotated and drawn in flutter canvas as a path with variable width. To be honest I am not bothered by current implementation not to be accurate. It's just something I noticed. It's fine with me if left as is. I totaly can see why you guys wouldn't want to redo it. So I don't know. If you are going to split path into segments that might reduce performance and overall be worse than have correct meters in width.

ReinisSprogis avatar Jul 09 '25 09:07 ReinisSprogis

But also would bring better perfomance [...] by drawing points with vertices.

Could you be more explicit about "drawing points with vertices"? Which flutter method do you eventually use? Interested in how we may improve performances in general.

To be honest I am not bothered by current implementation not to be accurate. It's just something I noticed. It's fine with me if left as is.

If you find a relevant use-case, we can work on it.

I totaly can see why you guys wouldn't want to redo it. So I don't know. If you are going to split path into segments that might reduce performance and overall be worse than have correct meters in width.

Might reduce performance AND maintainability.

monsieurtanuki avatar Jul 10 '25 11:07 monsieurtanuki

This will explain it well https://youtu.be/pD38Yyz7N2E What I did to make it into a circle was draw one triangle and clip it with a circle shader. This results in the most efficient circle painting you can do with Flutter. (As far as I know)

The demo in my video also handled calculating meters to pixels for the circle radius. I was able to calculate the radius every frame and change the color. I didn't do any AABB checks eather to check what circles are within screen. Currently, CircleLayer in Flutter Map isn't bottlenecked by calculating meters to pixels, but by canvas.drawCircle, which becomes very slow when drawing thousands of circles. Issue #2101 wouldn’t be necessary. I might create a plugin for Flutter Map that draws circles and paths using vertices. That way, the current implementation which is simpler and works fine for a smaller number of markers doesn't need to be modified. If we want to draw fast, things get a bit more complicated and might not be as user friendly. The user would probably need to provide TypedData like a Float64List of LatLng pairs, a list of sizes, and a list of colors (unless all circles are the same color), so there’s no wasteful object conversion. So no objects like VertexMarker for each point. That’s the trade off for better performance but it’s not too bad. Also need a texture coordinates and I find it bit annoying, because they are all the same for every triangle. For 275K circles where each circles has 3 vertices containing xy position, thats a list of 275k x 6 = 1650000 position values that get recalculated every frame. Thats fast! There is also bug in impeller that prevented me from drawing circle shader on IOS so I ended up creating shader from image as texture to clip out circle as workaround, but this could be usefule as user cold provide shaders and make any shape marker they want. https://github.com/flutter/flutter/issues/168969#issuecomment-2891814664 there is a full code example of how i draw circles, you can try it out, but note that impeller is bugged.

ReinisSprogis avatar Jul 10 '25 12:07 ReinisSprogis

@ReinisSprogis Thank you for your answer: I was really wondering how to draw circles with triangles ;)

We currently use numerous drawCircle calls - perhaps we would be better off adding circles to a path and then one single drawPath call. That would be a start.

I might create a plugin for Flutter Map that draws circles and paths using vertices. That way, the current implementation which is simpler and works fine for a smaller number of markers doesn't need to be modified. If we want to draw fast, things get a bit more complicated and might not be as user friendly.

If it's just changing the drawing methods (and making them less straightforward) without improving significantly the performances, that may not be worth it. Especially if they are potential bugs, with impeller for instance.

monsieurtanuki avatar Jul 10 '25 13:07 monsieurtanuki

I had some thought on this. If splitted in segments, then you need to draw it with canvas.drawLine as it would be more efficient than draw each segment with path. No pin't drawing path put of 2 points. But also how would you set a color for each segment? Can't construct a Polyline by providing list of LatLng. Then maybe need to create new PolyLine, that takes List<PathSegment>, then user provide AB points and color for each segment, and then if width in meters is set, then just set each segment width based on first segments latitude point. But maybe can split it up and resize each splitted segment based on latitude diference between A and B if easy enough.

ReinisSprogis avatar Jul 12 '25 16:07 ReinisSprogis

Hi @joaovvrodrigues , Thanks for the feedback. This appears to be unrelated to this issue. It would be great if you could open a new issue with an MRE of what seems to be slowing your app down the most.

JaffaKetchup avatar Aug 15 '25 13:08 JaffaKetchup