Skip to content

Commit 2bbafcd

Browse files
feat: add optimizeRadiusInMeters option to CircleLayer (#2101)
Co-authored-by: JaffaKetchup <[email protected]>
1 parent 352348e commit 2bbafcd

File tree

4 files changed

+161
-22
lines changed

4 files changed

+161
-22
lines changed

example/lib/pages/many_circles.dart

Lines changed: 109 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import 'package:flutter_map_example/widgets/number_of_items_slider.dart';
99
import 'package:flutter_map_example/widgets/show_no_web_perf_overlay_snackbar.dart';
1010
import 'package:latlong2/latlong.dart';
1111

12-
const _maxCirclesCount = 20000;
12+
const _maxCirclesCount = 30000;
1313

1414
/// On this page, [_maxCirclesCount] circles are randomly generated
1515
/// across europe, and then you can limit them with a slider
@@ -21,15 +21,19 @@ class ManyCirclesPage extends StatefulWidget {
2121
const ManyCirclesPage({super.key});
2222

2323
@override
24-
ManyCirclesPageState createState() => ManyCirclesPageState();
24+
State<ManyCirclesPage> createState() => _ManyCirclesPageState();
2525
}
2626

27-
class ManyCirclesPageState extends State<ManyCirclesPage> {
28-
double doubleInRange(Random source, num start, num end) =>
27+
class _ManyCirclesPageState extends State<ManyCirclesPage> {
28+
static double doubleInRange(Random source, num start, num end) =>
2929
source.nextDouble() * (end - start) + start;
30-
List<CircleMarker> allCircles = [];
3130

3231
int numOfCircles = _maxCirclesCount ~/ 10;
32+
List<CircleMarker> allCircles = [];
33+
34+
bool useBorders = false;
35+
bool useRadiusInMeters = false;
36+
bool optimizeRadiusInMeters = true;
3337

3438
@override
3539
void initState() {
@@ -39,12 +43,16 @@ class ManyCirclesPageState extends State<ManyCirclesPage> {
3943

4044
Future.microtask(() {
4145
final r = Random();
42-
for (var x = 0; x < _maxCirclesCount; x++) {
46+
for (double x = 0; x < _maxCirclesCount; x++) {
4347
allCircles.add(
4448
CircleMarker(
4549
point: LatLng(doubleInRange(r, 37, 55), doubleInRange(r, -9, 30)),
46-
color: Colors.red,
50+
color: HSLColor.fromAHSL(1, x % 360, 1, doubleInRange(r, 0.3, 0.7))
51+
.toColor(),
4752
radius: 5,
53+
useRadiusInMeter: false,
54+
borderStrokeWidth: 0,
55+
borderColor: Colors.black,
4856
),
4957
);
5058
}
@@ -76,18 +84,106 @@ class ManyCirclesPageState extends State<ManyCirclesPage> {
7684
),
7785
children: [
7886
openStreetMapTileLayer,
79-
CircleLayer(circles: allCircles.take(numOfCircles).toList()),
87+
CircleLayer(
88+
circles: allCircles.take(numOfCircles).toList(growable: false),
89+
optimizeRadiusInMeters: optimizeRadiusInMeters,
90+
),
8091
],
8192
),
8293
Positioned(
8394
left: 16,
8495
top: 16,
8596
right: 16,
86-
child: NumberOfItemsSlider(
87-
number: numOfCircles,
88-
onChanged: (v) => setState(() => numOfCircles = v),
89-
maxNumber: _maxCirclesCount,
90-
itemDescription: 'Circle',
97+
child: RepaintBoundary(
98+
child: Column(
99+
children: [
100+
NumberOfItemsSlider(
101+
number: numOfCircles,
102+
onChanged: (v) => setState(() => numOfCircles = v),
103+
maxNumber: _maxCirclesCount,
104+
itemDescription: 'Circle',
105+
),
106+
const SizedBox(height: 12),
107+
UnconstrainedBox(
108+
child: Container(
109+
decoration: BoxDecoration(
110+
color: Theme.of(context).colorScheme.surface,
111+
borderRadius: BorderRadius.circular(32),
112+
),
113+
padding: const EdgeInsets.symmetric(
114+
vertical: 4,
115+
horizontal: 16,
116+
),
117+
child: Row(
118+
children: [
119+
const Tooltip(
120+
message: 'Use Borders',
121+
child: Icon(Icons.circle_outlined),
122+
),
123+
const SizedBox(width: 8),
124+
Switch.adaptive(
125+
value: useBorders,
126+
onChanged: (v) {
127+
allCircles = allCircles
128+
.map(
129+
(c) => CircleMarker(
130+
point: c.point,
131+
radius: c.radius,
132+
color: c.color,
133+
useRadiusInMeter: c.useRadiusInMeter,
134+
borderColor: c.borderColor,
135+
borderStrokeWidth: v ? 5 : 0,
136+
),
137+
)
138+
.toList(growable: false);
139+
useBorders = v;
140+
setState(() {});
141+
},
142+
),
143+
const SizedBox(width: 16),
144+
const Tooltip(
145+
message: 'Use Radius In Meters',
146+
child: Icon(Icons.straighten),
147+
),
148+
const SizedBox(width: 8),
149+
Switch.adaptive(
150+
value: useRadiusInMeters,
151+
onChanged: (v) {
152+
allCircles = allCircles
153+
.map(
154+
(c) => CircleMarker(
155+
point: c.point,
156+
radius: v ? 25000 : 5,
157+
color: c.color,
158+
useRadiusInMeter: v,
159+
borderColor: c.borderColor,
160+
borderStrokeWidth: c.borderStrokeWidth,
161+
),
162+
)
163+
.toList(growable: false);
164+
useRadiusInMeters = v;
165+
setState(() {});
166+
},
167+
),
168+
const SizedBox(width: 16),
169+
const Tooltip(
170+
message: 'Optimise Meters Radius',
171+
child: Icon(Icons.speed_rounded),
172+
),
173+
const SizedBox(width: 8),
174+
Switch.adaptive(
175+
value: optimizeRadiusInMeters,
176+
onChanged: useRadiusInMeters
177+
? (v) =>
178+
setState(() => optimizeRadiusInMeters = v)
179+
: null,
180+
),
181+
],
182+
),
183+
),
184+
),
185+
],
186+
),
91187
),
92188
),
93189
if (!kIsWeb)

lib/src/layer/circle_layer/circle_layer.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_de
88
import 'package:latlong2/latlong.dart' hide Path;
99

1010
part 'circle_marker.dart';
11+
1112
part 'painter.dart';
1213

1314
/// A layer that displays a list of [CircleMarker] on the map
@@ -19,11 +20,33 @@ class CircleLayer<R extends Object> extends StatelessWidget {
1920
/// {@macro fm.lhn.layerHitNotifier.usage}
2021
final LayerHitNotifier<R>? hitNotifier;
2122

23+
/// Whether to use a single meters to pixels conversion ratio for all circles
24+
/// with [CircleMarker.useRadiusInMeter] enabled
25+
///
26+
/// > [!IMPORTANT]
27+
/// > This reduces the accuracy of the radius of circles. Depending on the
28+
/// > location of the circles, this may or may not be significant.
29+
///
30+
/// Where all circles within this layer are geographically (particularly
31+
/// latitudinally) close, the difference in the ratio between pixels and
32+
/// meters between circles is likely to be small. Calculating this
33+
/// conversion ratio is expensive, and is usually done for every circle to
34+
/// ensure accuracy, as the ratio depends on the latitude. Setting this `true`
35+
/// means the ratio is calculated based off the first circle only, then reused
36+
/// for all other circles within this layer.
37+
///
38+
/// This should not be used where circles are geographically spread out - it
39+
/// is best suited, for example, for circles located within a single city.
40+
///
41+
/// Defaults to `false`.
42+
final bool optimizeRadiusInMeters;
43+
2244
/// Create a new [CircleLayer] as a child for [FlutterMap]
2345
const CircleLayer({
2446
super.key,
2547
required this.circles,
2648
this.hitNotifier,
49+
this.optimizeRadiusInMeters = false,
2750
});
2851

2952
@override
@@ -36,6 +59,7 @@ class CircleLayer<R extends Object> extends StatelessWidget {
3659
circles: circles,
3760
camera: camera,
3861
hitNotifier: hitNotifier,
62+
optimizeRadiusInMeters: optimizeRadiusInMeters,
3963
),
4064
size: camera.size,
4165
isComplex: true,

lib/src/layer/circle_layer/circle_marker.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class CircleMarker<R extends Object> with HitDetectableElement<R> {
1212
final LatLng point;
1313

1414
/// The radius of the circle
15+
///
16+
/// Measured in pixels, unless [useRadiusInMeter] is set.
1517
final double radius;
1618

1719
/// The color of the circle area.
@@ -24,7 +26,7 @@ class CircleMarker<R extends Object> with HitDetectableElement<R> {
2426
/// to be visible.
2527
final Color borderColor;
2628

27-
/// Set to true if the radius should use the unit meters.
29+
/// Whether to treat [radius] as a measurement in meters instead of pixels.
2830
final bool useRadiusInMeter;
2931

3032
@override

lib/src/layer/circle_layer/painter.dart

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ class CirclePainter<R extends Object> extends CustomPainter
1212
@override
1313
final LayerHitNotifier<R>? hitNotifier;
1414

15+
/// If true, we reuse the same "meter in pixels" computation for all circles.
16+
final bool optimizeRadiusInMeters;
17+
1518
/// Create a [CirclePainter] instance by providing the required
1619
/// reference objects.
1720
CirclePainter({
1821
required this.circles,
1922
required this.camera,
2023
required this.hitNotifier,
24+
required this.optimizeRadiusInMeters,
2125
});
2226

2327
@override
@@ -26,7 +30,7 @@ class CirclePainter<R extends Object> extends CustomPainter
2630
required Offset point,
2731
required LatLng coordinate,
2832
}) {
29-
final radius = _getRadiusInPixel(element, withBorder: true);
33+
final radius = _getRadiusInPixel(element) + element.borderStrokeWidth / 2;
3034
final initialCenter = _getOffset(element.point);
3135

3236
WorldWorkControl checkIfHit(double shift) {
@@ -56,9 +60,11 @@ class CirclePainter<R extends Object> extends CustomPainter
5660
final points = <Color, Map<double, List<Offset>>>{};
5761
final pointsFilledBorder = <Color, Map<double, List<Offset>>>{};
5862
final pointsBorder = <Color, Map<double, Map<double, List<Offset>>>>{};
63+
_pixelsPerMeter = null;
5964
for (final circle in circles) {
60-
final radiusWithoutBorder = _getRadiusInPixel(circle, withBorder: false);
61-
final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true);
65+
final radiusWithoutBorder = _getRadiusInPixel(circle);
66+
final radiusWithBorder =
67+
radiusWithoutBorder + circle.borderStrokeWidth / 2;
6268
final initialCenter = _getOffset(circle.point);
6369

6470
/// Draws on a "single-world"
@@ -163,11 +169,22 @@ class CirclePainter<R extends Object> extends CustomPainter
163169

164170
Offset _getOffset(LatLng pos) => camera.getOffsetFromOrigin(pos);
165171

166-
double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) =>
167-
(withBorder ? circle.borderStrokeWidth / 2 : 0) +
168-
(circle.useRadiusInMeter
169-
? metersToScreenPixels(circle.point, circle.radius)
170-
: circle.radius);
172+
// Cached number of pixels per meter.
173+
double? _pixelsPerMeter;
174+
175+
double _getRadiusInPixel(CircleMarker circle) {
176+
if (!circle.useRadiusInMeter) {
177+
return circle.radius;
178+
}
179+
if (!optimizeRadiusInMeters) {
180+
return metersToScreenPixels(circle.point, circle.radius);
181+
}
182+
if (_pixelsPerMeter == null) {
183+
final result = metersToScreenPixels(circle.point, circle.radius);
184+
_pixelsPerMeter = result / circle.radius;
185+
}
186+
return _pixelsPerMeter! * circle.radius;
187+
}
171188

172189
/// Returns true if a centered circle with this radius is on the screen.
173190
bool _isVisible({

0 commit comments

Comments
 (0)