Skip to content

Commit e9272e2

Browse files
feat: add multi-world support to Polygons and Polylines & refactoring (#2033)
Co-authored-by: JaffaKetchup <[email protected]>
1 parent f1d1675 commit e9272e2

File tree

14 files changed

+676
-467
lines changed

14 files changed

+676
-467
lines changed

example/lib/pages/multi_worlds.dart

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,87 @@ class _MultiWorldsPageState extends State<MultiWorldsPage> {
127127
..._customMarkers,
128128
],
129129
),
130+
GestureDetector(
131+
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
132+
SnackBar(
133+
content: Text(_hitNotifier.value!.hitValues.join(', ')),
134+
duration: const Duration(seconds: 1),
135+
showCloseIcon: true,
136+
),
137+
),
138+
child: PolygonLayer<String>(
139+
hitNotifier: _hitNotifier,
140+
simplificationTolerance: 0,
141+
useAltRendering: true,
142+
drawLabelsLast: false,
143+
polygons: [
144+
Polygon<String>(
145+
label: 'Aloha!',
146+
labelStyle:
147+
const TextStyle(color: Colors.green, fontSize: 40),
148+
labelPlacement:
149+
PolygonLabelPlacement.centroidWithMultiWorld,
150+
rotateLabel: false,
151+
points: const [
152+
LatLng(40, 149),
153+
LatLng(45, 159),
154+
LatLng(50, 169),
155+
LatLng(55, 179),
156+
LatLng(50, -170),
157+
LatLng(45, -160),
158+
LatLng(40, -150),
159+
LatLng(35, -160),
160+
LatLng(30, -170),
161+
LatLng(25, -180),
162+
LatLng(30, 169),
163+
LatLng(35, 159),
164+
],
165+
holePointsList: const [
166+
[
167+
LatLng(45, 175),
168+
LatLng(45, -175),
169+
LatLng(35, -175),
170+
LatLng(35, 175),
171+
],
172+
],
173+
color: const Color(0xFFFF0000),
174+
hitValue: 'Red Line, Across the universe...',
175+
),
176+
],
177+
),
178+
),
179+
GestureDetector(
180+
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
181+
SnackBar(
182+
content: Text(_hitNotifier.value!.hitValues.join(', ')),
183+
duration: const Duration(seconds: 1),
184+
showCloseIcon: true,
185+
),
186+
),
187+
child: PolylineLayer<String>(
188+
hitNotifier: _hitNotifier,
189+
simplificationTolerance: 0,
190+
polylines: [
191+
Polyline<String>(
192+
points: const [
193+
LatLng(-40, 150),
194+
LatLng(-45, 160),
195+
LatLng(-50, 170),
196+
LatLng(-55, 180),
197+
LatLng(-50, -170),
198+
LatLng(-45, -160),
199+
LatLng(-40, -150),
200+
LatLng(-45, -140),
201+
LatLng(-50, -130),
202+
],
203+
useStrokeWidthInMeter: true,
204+
strokeWidth: 500000,
205+
color: const Color(0xFF0000FF),
206+
hitValue: 'Blue Line',
207+
),
208+
],
209+
),
210+
),
130211
],
131212
),
132213
],

lib/src/layer/circle_layer/circle_layer.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:ui';
33

44
import 'package:flutter/widgets.dart';
55
import 'package:flutter_map/flutter_map.dart';
6+
import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart';
67
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
78
import 'package:latlong2/latlong.dart' hide Path;
89

lib/src/layer/circle_layer/painter.dart

Lines changed: 31 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,56 @@
11
part of 'circle_layer.dart';
22

3-
/// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer].
4-
base class CirclePainter<R extends Object>
5-
extends HitDetectablePainter<R, CircleMarker<R>> {
3+
/// The [CustomPainter] used to draw [CircleMarker]s for the [CircleLayer].
4+
class CirclePainter<R extends Object> extends CustomPainter
5+
with HitDetectablePainter<R, CircleMarker<R>>, FeatureLayerUtils {
66
/// Reference to the list of [CircleMarker]s of the [CircleLayer].
77
final List<CircleMarker<R>> circles;
88

9+
@override
10+
final MapCamera camera;
11+
12+
@override
13+
final LayerHitNotifier<R>? hitNotifier;
14+
915
/// Create a [CirclePainter] instance by providing the required
1016
/// reference objects.
1117
CirclePainter({
1218
required this.circles,
13-
required super.camera,
14-
required super.hitNotifier,
19+
required this.camera,
20+
required this.hitNotifier,
1521
});
1622

17-
static const _distance = Distance();
18-
1923
@override
2024
bool elementHitTest(
2125
CircleMarker<R> element, {
2226
required Offset point,
2327
required LatLng coordinate,
2428
}) {
25-
final worldWidth = _getWorldWidth();
2629
final radius = _getRadiusInPixel(element, withBorder: true);
2730
final initialCenter = _getOffset(element.point);
2831

29-
/// Returns null if invisible, true if hit, false if not hit.
30-
bool? checkIfHit(double shift) {
32+
WorldWorkControl checkIfHit(double shift) {
3133
final center = initialCenter + Offset(shift, 0);
32-
if (!_isVisible(
33-
screenRect: _screenRect,
34-
center: center,
35-
radiusInPixel: radius,
36-
)) {
37-
return null;
34+
if (!_isVisible(center: center, radiusInPixel: radius)) {
35+
return WorldWorkControl.invisible;
3836
}
3937

4038
return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <=
41-
radius * radius;
39+
radius * radius
40+
? WorldWorkControl.hit
41+
: WorldWorkControl.visible;
4242
}
4343

44-
if (checkIfHit(0) ?? false) {
45-
return true;
46-
}
47-
48-
// Repeat over all worlds (<--||-->) until culling determines that
49-
// that element is out of view, and therefore all further elements in
50-
// that direction will also be
51-
if (worldWidth == 0) return false;
52-
for (double shift = -worldWidth;; shift -= worldWidth) {
53-
final isHit = checkIfHit(shift);
54-
if (isHit == null) break;
55-
if (isHit) return true;
56-
}
57-
for (double shift = worldWidth;; shift += worldWidth) {
58-
final isHit = checkIfHit(shift);
59-
if (isHit == null) break;
60-
if (isHit) return true;
61-
}
62-
63-
return false;
44+
return workAcrossWorlds(checkIfHit);
6445
}
6546

6647
@override
6748
Iterable<CircleMarker<R>> get elements => circles;
6849

69-
late Rect _screenRect;
70-
7150
@override
7251
void paint(Canvas canvas, Size size) {
73-
_screenRect = Offset.zero & size;
74-
canvas.clipRect(_screenRect);
75-
76-
final worldWidth = _getWorldWidth();
52+
super.paint(canvas, size);
53+
canvas.clipRect(viewportRect);
7754

7855
// Let's calculate all the points grouped by color and radius
7956
final points = <Color, Map<double, List<Offset>>>{};
@@ -84,17 +61,15 @@ base class CirclePainter<R extends Object>
8461
final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true);
8562
final initialCenter = _getOffset(circle.point);
8663

87-
bool checkIfVisible(double shift) {
88-
bool result = false;
64+
/// Draws on a "single-world"
65+
WorldWorkControl drawIfVisible(double shift) {
66+
WorldWorkControl result = WorldWorkControl.invisible;
8967
final center = initialCenter + Offset(shift, 0);
9068

9169
bool isVisible(double radius) {
92-
if (_isVisible(
93-
screenRect: _screenRect,
94-
center: center,
95-
radiusInPixel: radius,
96-
)) {
97-
return result = true;
70+
if (_isVisible(center: center, radiusInPixel: radius)) {
71+
result = WorldWorkControl.visible;
72+
return true;
9873
}
9974
return false;
10075
}
@@ -123,23 +98,11 @@ base class CirclePainter<R extends Object>
12398
.add(center);
12499
}
125100
}
101+
126102
return result;
127103
}
128104

129-
checkIfVisible(0);
130-
131-
// Repeat over all worlds (<--||-->) until culling determines that
132-
// that element is out of view, and therefore all further elements in
133-
// that direction will also be
134-
if (worldWidth == 0) continue;
135-
for (double shift = -worldWidth;; shift -= worldWidth) {
136-
final isVisible = checkIfVisible(shift);
137-
if (!isVisible) break;
138-
}
139-
for (double shift = worldWidth;; shift += worldWidth) {
140-
final isVisible = checkIfVisible(shift);
141-
if (!isVisible) break;
142-
}
105+
workAcrossWorlds(drawIfVisible);
143106
}
144107

145108
// Now that all the points are grouped, let's draw them
@@ -203,21 +166,14 @@ base class CirclePainter<R extends Object>
203166
double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) =>
204167
(withBorder ? circle.borderStrokeWidth / 2 : 0) +
205168
(circle.useRadiusInMeter
206-
? (_getOffset(circle.point) -
207-
_getOffset(
208-
_distance.offset(circle.point, circle.radius, 180)))
209-
.distance
169+
? metersToScreenPixels(circle.point, circle.radius)
210170
: circle.radius);
211171

212172
/// Returns true if a centered circle with this radius is on the screen.
213173
bool _isVisible({
214-
required Rect screenRect,
215174
required Offset center,
216175
required double radiusInPixel,
217176
}) =>
218-
screenRect.overlaps(
219-
Rect.fromCircle(center: center, radius: radiusInPixel),
220-
);
221-
222-
double _getWorldWidth() => camera.getWorldWidthAtZoom();
177+
viewportRect
178+
.overlaps(Rect.fromCircle(center: center, radius: radiusInPixel));
223179
}

lib/src/layer/polygon_layer/label.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ LatLng _computeLabelPosition(
6060
) {
6161
return switch (labelPlacement) {
6262
PolygonLabelPlacement.centroid => _computeCentroid(points),
63+
PolygonLabelPlacement.centroidWithMultiWorld =>
64+
_computeCentroidWithMultiWorld(points),
6365
PolygonLabelPlacement.polylabel => _computePolylabel(points),
6466
};
6567
}
@@ -72,6 +74,29 @@ LatLng _computeCentroid(List<LatLng> points) {
7274
);
7375
}
7476

77+
/// Calculate the centroid of a given list of [LatLng] points with multiple worlds.
78+
LatLng _computeCentroidWithMultiWorld(List<LatLng> points) {
79+
if (points.isEmpty) return _computeCentroid(points);
80+
const halfWorld = 180;
81+
int count = 0;
82+
double sum = 0;
83+
late double lastLng;
84+
for (final LatLng point in points) {
85+
double lng = point.longitude;
86+
count++;
87+
if (count > 1) {
88+
if (lng - lastLng > halfWorld) {
89+
lng -= 2 * halfWorld;
90+
} else if (lng - lastLng < -halfWorld) {
91+
lng += 2 * halfWorld;
92+
}
93+
}
94+
lastLng = lng;
95+
sum += lastLng;
96+
}
97+
return LatLng(points.map((e) => e.latitude).average, sum / count);
98+
}
99+
75100
/// Use the Maxbox Polylabel algorithm to calculate the [LatLng] position for
76101
/// a given list of points.
77102
LatLng _computePolylabel(List<LatLng> points) {
@@ -93,3 +118,20 @@ LatLng _computePolylabel(List<LatLng> points) {
93118
labelPosition.point.x.toDouble(),
94119
);
95120
}
121+
122+
/// Defines the algorithm used to calculate the position of the [Polygon] label.
123+
///
124+
/// > [!IMPORTANT]
125+
/// > If your project allows users to browse across multiple worlds, and your
126+
/// > polygons may be over the anti-meridan boundary, [centroidWithMultiWorld]
127+
/// > must be used - other algorithms will produce unexpected results.
128+
enum PolygonLabelPlacement {
129+
/// Use the centroid of the [Polygon] outline as position for the label.
130+
centroid,
131+
132+
/// Use the centroid in a multi-world as position for the label.
133+
centroidWithMultiWorld,
134+
135+
/// Use the Mapbox Polylabel algorithm as position for the label.
136+
polylabel,
137+
}

0 commit comments

Comments
 (0)