Skip to content

Commit 7b39b26

Browse files
feat: replicate Markers across all worlds (#2000)
Co-authored-by: Luka S <[email protected]>
1 parent 5ac7dcf commit 7b39b26

File tree

5 files changed

+159
-25
lines changed

5 files changed

+159
-25
lines changed

example/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:flutter_map_example/pages/many_markers.dart';
1515
import 'package:flutter_map_example/pages/map_controller.dart';
1616
import 'package:flutter_map_example/pages/map_inside_listview.dart';
1717
import 'package:flutter_map_example/pages/markers.dart';
18+
import 'package:flutter_map_example/pages/multi_worlds.dart';
1819
import 'package:flutter_map_example/pages/overlay_image.dart';
1920
import 'package:flutter_map_example/pages/plugin_zoombuttons.dart';
2021
import 'package:flutter_map_example/pages/polygon.dart';
@@ -66,6 +67,7 @@ class MyApp extends StatelessWidget {
6667
CirclePage.route: (context) => const CirclePage(),
6768
OverlayImagePage.route: (context) => const OverlayImagePage(),
6869
PolygonPage.route: (context) => const PolygonPage(),
70+
MultiWorldsPage.route: (context) => const MultiWorldsPage(),
6971
PolygonPerfStressPage.route: (context) => const PolygonPerfStressPage(),
7072
SlidingMapPage.route: (_) => const SlidingMapPage(),
7173
WMSLayerPage.route: (context) => const WMSLayerPage(),
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_map/flutter_map.dart';
3+
import 'package:flutter_map_example/misc/tile_providers.dart';
4+
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
5+
import 'package:latlong2/latlong.dart';
6+
7+
/// Example dedicated to replicated worlds and related objects (e.g. Markers).
8+
class MultiWorldsPage extends StatefulWidget {
9+
static const String route = '/multi_worlds';
10+
11+
const MultiWorldsPage({super.key});
12+
13+
@override
14+
State<MultiWorldsPage> createState() => _MultiWorldsPageState();
15+
}
16+
17+
class _MultiWorldsPageState extends State<MultiWorldsPage> {
18+
@override
19+
Widget build(BuildContext context) {
20+
return Scaffold(
21+
appBar: AppBar(title: const Text('Multi-worlds')),
22+
drawer: const MenuDrawer(MultiWorldsPage.route),
23+
body: Stack(
24+
children: [
25+
FlutterMap(
26+
options: const MapOptions(
27+
initialCenter: LatLng(51.5, -0.09),
28+
initialZoom: 0,
29+
initialRotation: 0,
30+
),
31+
children: [
32+
openStreetMapTileLayer,
33+
MarkerLayer(
34+
markers: [
35+
Marker(
36+
point: const LatLng(48.856666, 2.351944),
37+
alignment: Alignment.topCenter,
38+
child: GestureDetector(
39+
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
40+
const SnackBar(
41+
content: Text('Paris'),
42+
duration: Duration(seconds: 1),
43+
showCloseIcon: true,
44+
),
45+
),
46+
child: const Icon(Icons.location_on_rounded),
47+
),
48+
),
49+
Marker(
50+
point: const LatLng(34.05, -118.25),
51+
child: GestureDetector(
52+
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
53+
const SnackBar(
54+
content: Text('Los Angeles'),
55+
duration: Duration(seconds: 1),
56+
showCloseIcon: true,
57+
),
58+
),
59+
child: const Icon(Icons.location_city),
60+
),
61+
),
62+
Marker(
63+
point: const LatLng(35.689444, 139.691666),
64+
child: GestureDetector(
65+
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
66+
const SnackBar(
67+
content: Text('Tokyo'),
68+
duration: Duration(seconds: 1),
69+
showCloseIcon: true,
70+
),
71+
),
72+
child: const Icon(Icons.backpack_outlined),
73+
),
74+
),
75+
],
76+
),
77+
],
78+
),
79+
],
80+
),
81+
);
82+
}
83+
}

example/lib/widgets/drawer/menu_drawer.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:flutter_map_example/pages/many_markers.dart';
1616
import 'package:flutter_map_example/pages/map_controller.dart';
1717
import 'package:flutter_map_example/pages/map_inside_listview.dart';
1818
import 'package:flutter_map_example/pages/markers.dart';
19+
import 'package:flutter_map_example/pages/multi_worlds.dart';
1920
import 'package:flutter_map_example/pages/overlay_image.dart';
2021
import 'package:flutter_map_example/pages/plugin_zoombuttons.dart';
2122
import 'package:flutter_map_example/pages/polygon.dart';
@@ -109,6 +110,11 @@ class MenuDrawer extends StatelessWidget {
109110
routeName: ScaleBarPage.route,
110111
currentRoute: currentRoute,
111112
),
113+
MenuItemWidget(
114+
caption: 'Multi-world and layers',
115+
routeName: MultiWorldsPage.route,
116+
currentRoute: currentRoute,
117+
),
112118
const Divider(),
113119
MenuItemWidget(
114120
caption: 'Map Controller',

lib/src/layer/marker_layer/marker_layer.dart

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ class MarkerLayer extends StatelessWidget {
4141
@override
4242
Widget build(BuildContext context) {
4343
final map = MapCamera.of(context);
44+
final worldWidth = map.getWorldWidthAtZoom();
4445

4546
return MobileLayerTransformer(
4647
child: Stack(
4748
children: (List<Marker> markers) sync* {
4849
for (final m in markers) {
4950
// Resolve real alignment
50-
// TODO this can probably just be done with calls to Size, Offset, and Rect
51+
// TODO: maybe just using Size, Offset, and Rect?
5152
final left = 0.5 * m.width * ((m.alignment ?? alignment).x + 1);
5253
final top = 0.5 * m.height * ((m.alignment ?? alignment).y + 1);
5354
final right = m.width - left;
@@ -56,33 +57,65 @@ class MarkerLayer extends StatelessWidget {
5657
// Perform projection
5758
final pxPoint = map.projectAtZoom(m.point);
5859

59-
// Cull if out of bounds
60-
if (!map.pixelBounds.overlaps(
61-
Rect.fromPoints(
62-
Offset(pxPoint.dx + left, pxPoint.dy - bottom),
63-
Offset(pxPoint.dx - right, pxPoint.dy + top),
64-
),
65-
)) {
66-
continue;
60+
Positioned? getPositioned(double worldShift) {
61+
final shiftedX = pxPoint.dx + worldShift;
62+
63+
// Cull if out of bounds
64+
if (!map.pixelBounds.overlaps(
65+
Rect.fromPoints(
66+
Offset(shiftedX + left, pxPoint.dy - bottom),
67+
Offset(shiftedX - right, pxPoint.dy + top),
68+
),
69+
)) {
70+
return null;
71+
}
72+
73+
// Shift original coordinate along worlds, then move into relative
74+
// to origin space
75+
final shiftedLocalPoint =
76+
Offset(shiftedX, pxPoint.dy) - map.pixelOrigin;
77+
78+
return Positioned(
79+
key: m.key,
80+
width: m.width,
81+
height: m.height,
82+
left: shiftedLocalPoint.dx - right,
83+
top: shiftedLocalPoint.dy - bottom,
84+
child: (m.rotate ?? rotate)
85+
? Transform.rotate(
86+
angle: -map.rotationRad,
87+
alignment: (m.alignment ?? alignment) * -1,
88+
child: m.child,
89+
)
90+
: m.child,
91+
);
6792
}
6893

69-
// Apply map camera to marker position
70-
final pos = pxPoint - map.pixelOrigin;
94+
// Create marker in main world, unless culled
95+
final main = getPositioned(0);
96+
if (main != null) yield main;
97+
// It is unsafe to assume that if the main one is culled, it will
98+
// also be culled in all other worlds, so we must continue
7199

72-
yield Positioned(
73-
key: m.key,
74-
width: m.width,
75-
height: m.height,
76-
left: pos.dx - right,
77-
top: pos.dy - bottom,
78-
child: (m.rotate ?? rotate)
79-
? Transform.rotate(
80-
angle: -map.rotationRad,
81-
alignment: (m.alignment ?? alignment) * -1,
82-
child: m.child,
83-
)
84-
: m.child,
85-
);
100+
// TODO: optimization - find a way to skip these tests in some
101+
// obvious situations. Imagine we're in a map smaller than the
102+
// world, and west lower than east - in that case we probably don't
103+
// need to check eastern and western.
104+
105+
// Repeat over all worlds (<--||-->) until culling determines that
106+
// that marker is out of view, and therefore all further markers in
107+
// that direction will also be
108+
if (worldWidth == 0) continue;
109+
for (double shift = -worldWidth;; shift -= worldWidth) {
110+
final additional = getPositioned(shift);
111+
if (additional == null) break;
112+
yield additional;
113+
}
114+
for (double shift = worldWidth;; shift += worldWidth) {
115+
final additional = getPositioned(shift);
116+
if (additional == null) break;
117+
yield additional;
118+
}
86119
}
87120
}(markers)
88121
.toList(),

lib/src/map/camera/camera.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,16 @@ class MapCamera {
239239
LatLng unprojectAtZoom(Offset point, [double? zoom]) =>
240240
crs.offsetToLatLng(point, zoom ?? this.zoom);
241241

242+
/// Returns the width of the world at the current zoom, or 0 if irrelevant.
243+
double getWorldWidthAtZoom() {
244+
if (!crs.replicatesWorldLongitude) {
245+
return 0;
246+
}
247+
final offset0 = projectAtZoom(const LatLng(0, 0));
248+
final offset180 = projectAtZoom(const LatLng(0, 180));
249+
return 2 * (offset180.dx - offset0.dx).abs();
250+
}
251+
242252
/// Calculates the scale for a zoom from [fromZoom] to [toZoom] using this
243253
/// camera\s [crs].
244254
double getZoomScale(double toZoom, double fromZoom) =>

0 commit comments

Comments
 (0)