Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replicate Markers across all worlds #2000

Merged
merged 10 commits into from
Jan 1, 2025
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'package:flutter_map_example/pages/many_markers.dart';
import 'package:flutter_map_example/pages/map_controller.dart';
import 'package:flutter_map_example/pages/map_inside_listview.dart';
import 'package:flutter_map_example/pages/markers.dart';
import 'package:flutter_map_example/pages/multi_worlds.dart';
import 'package:flutter_map_example/pages/overlay_image.dart';
import 'package:flutter_map_example/pages/plugin_zoombuttons.dart';
import 'package:flutter_map_example/pages/polygon.dart';
Expand Down Expand Up @@ -66,6 +67,7 @@ class MyApp extends StatelessWidget {
CirclePage.route: (context) => const CirclePage(),
OverlayImagePage.route: (context) => const OverlayImagePage(),
PolygonPage.route: (context) => const PolygonPage(),
MultiWorldsPage.route: (context) => const MultiWorldsPage(),
PolygonPerfStressPage.route: (context) => const PolygonPerfStressPage(),
SlidingMapPage.route: (_) => const SlidingMapPage(),
WMSLayerPage.route: (context) => const WMSLayerPage(),
Expand Down
83 changes: 83 additions & 0 deletions example/lib/pages/multi_worlds.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/misc/tile_providers.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';

/// Example dedicated to replicated worlds and related objects (e.g. Markers).
class MultiWorldsPage extends StatefulWidget {
static const String route = '/multi_worlds';

const MultiWorldsPage({super.key});

@override
State<MultiWorldsPage> createState() => _MultiWorldsPageState();
}

class _MultiWorldsPageState extends State<MultiWorldsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Multi-worlds')),
drawer: const MenuDrawer(MultiWorldsPage.route),
body: Stack(
children: [
FlutterMap(
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
initialZoom: 0,
initialRotation: 0,
),
children: [
openStreetMapTileLayer,
MarkerLayer(
markers: [
Marker(
point: const LatLng(48.856666, 2.351944),
alignment: Alignment.topCenter,
child: GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Paris'),
duration: Duration(seconds: 1),
showCloseIcon: true,
),
),
child: const Icon(Icons.location_on_rounded),
),
),
Marker(
point: const LatLng(34.05, -118.25),
child: GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Los Angeles'),
duration: Duration(seconds: 1),
showCloseIcon: true,
),
),
child: const Icon(Icons.location_city),
),
),
Marker(
point: const LatLng(35.689444, 139.691666),
child: GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Tokyo'),
duration: Duration(seconds: 1),
showCloseIcon: true,
),
),
child: const Icon(Icons.backpack_outlined),
),
),
],
),
],
),
],
),
);
}
}
6 changes: 6 additions & 0 deletions example/lib/widgets/drawer/menu_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:flutter_map_example/pages/many_markers.dart';
import 'package:flutter_map_example/pages/map_controller.dart';
import 'package:flutter_map_example/pages/map_inside_listview.dart';
import 'package:flutter_map_example/pages/markers.dart';
import 'package:flutter_map_example/pages/multi_worlds.dart';
import 'package:flutter_map_example/pages/overlay_image.dart';
import 'package:flutter_map_example/pages/plugin_zoombuttons.dart';
import 'package:flutter_map_example/pages/polygon.dart';
Expand Down Expand Up @@ -109,6 +110,11 @@ class MenuDrawer extends StatelessWidget {
routeName: ScaleBarPage.route,
currentRoute: currentRoute,
),
MenuItemWidget(
caption: 'Multi-world and layers',
routeName: MultiWorldsPage.route,
currentRoute: currentRoute,
),
const Divider(),
MenuItemWidget(
caption: 'Map Controller',
Expand Down
83 changes: 58 additions & 25 deletions lib/src/layer/marker_layer/marker_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,14 @@ class MarkerLayer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final map = MapCamera.of(context);
final worldWidth = map.getWorldWidthAtZoom();

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

// Cull if out of bounds
if (!map.pixelBounds.overlaps(
Rect.fromPoints(
Offset(pxPoint.dx + left, pxPoint.dy - bottom),
Offset(pxPoint.dx - right, pxPoint.dy + top),
),
)) {
continue;
Positioned? getPositioned(double worldShift) {
final shiftedX = pxPoint.dx + worldShift;

// Cull if out of bounds
if (!map.pixelBounds.overlaps(
Rect.fromPoints(
Offset(shiftedX + left, pxPoint.dy - bottom),
Offset(shiftedX - right, pxPoint.dy + top),
),
)) {
return null;
}

// Shift original coordinate along worlds, then move into relative
// to origin space
final shiftedLocalPoint =
Offset(shiftedX, pxPoint.dy) - map.pixelOrigin;

return Positioned(
key: m.key,
width: m.width,
height: m.height,
left: shiftedLocalPoint.dx - right,
top: shiftedLocalPoint.dy - bottom,
child: (m.rotate ?? rotate)
? Transform.rotate(
angle: -map.rotationRad,
alignment: (m.alignment ?? alignment) * -1,
child: m.child,
)
: m.child,
);
}

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

yield Positioned(
key: m.key,
width: m.width,
height: m.height,
left: pos.dx - right,
top: pos.dy - bottom,
child: (m.rotate ?? rotate)
? Transform.rotate(
angle: -map.rotationRad,
alignment: (m.alignment ?? alignment) * -1,
child: m.child,
)
: m.child,
);
// TODO: optimization - find a way to skip these tests in some
// obvious situations. Imagine we're in a map smaller than the
// world, and west lower than east - in that case we probably don't
// need to check eastern and western.

// Repeat over all worlds (<--||-->) until culling determines that
// that marker is out of view, and therefore all further markers in
// that direction will also be
if (worldWidth == 0) continue;
for (double shift = -worldWidth;; shift -= worldWidth) {
final additional = getPositioned(shift);
if (additional == null) break;
yield additional;
}
for (double shift = worldWidth;; shift += worldWidth) {
final additional = getPositioned(shift);
if (additional == null) break;
yield additional;
}
}
}(markers)
.toList(),
Expand Down
10 changes: 10 additions & 0 deletions lib/src/map/camera/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ class MapCamera {
LatLng unprojectAtZoom(Offset point, [double? zoom]) =>
crs.offsetToLatLng(point, zoom ?? this.zoom);

/// Returns the width of the world at the current zoom, or 0 if irrelevant.
double getWorldWidthAtZoom() {
if (!crs.replicatesWorldLongitude) {
return 0;
}
final offset0 = projectAtZoom(const LatLng(0, 0));
final offset180 = projectAtZoom(const LatLng(0, 180));
return 2 * (offset180.dx - offset0.dx).abs();
}

/// Calculates the scale for a zoom from [fromZoom] to [toZoom] using this
/// camera\s [crs].
double getZoomScale(double toZoom, double fromZoom) =>
Expand Down
Loading