From f58912cfb7fa02e0505c2591c3a4c459fb989d1a Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sat, 14 Dec 2024 17:43:13 +0100 Subject: [PATCH 1/6] feat: 1999 - Markers replicated in all world copies New file: * `multi_worlds.dart`: Example dedicated to replicated worlds and related objects (e.g. Markers). Impacted files: * `camera.dart`: added the `getWorldWidthAtZoom` method * `main.dart`: added references to new page `MultiWorldsPage` * `marker_layer.dart`: now tries to add/subtract world width to marker abscissa for replication * `menu_drawer.dart`: added references to new page `MultiWorldsPage` --- example/lib/main.dart | 2 + example/lib/pages/multi_worlds.dart | 70 ++++++++++++++++++ example/lib/widgets/drawer/menu_drawer.dart | 6 ++ lib/src/layer/marker_layer/marker_layer.dart | 75 ++++++++++++++------ lib/src/map/camera/camera.dart | 10 +++ 5 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 example/lib/pages/multi_worlds.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index b0c9b03c9..5ca19ad39 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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'; @@ -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(), diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart new file mode 100644 index 000000000..c3ce5f0a9 --- /dev/null +++ b/example/lib/pages/multi_worlds.dart @@ -0,0 +1,70 @@ +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 createState() => _MultiWorldsPageState(); +} + +class _MultiWorldsPageState extends State { + @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), + ), + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/drawer/menu_drawer.dart b/example/lib/widgets/drawer/menu_drawer.dart index 7e85f08f3..5d130c072 100644 --- a/example/lib/widgets/drawer/menu_drawer.dart +++ b/example/lib/widgets/drawer/menu_drawer.dart @@ -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'; @@ -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', diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 2d6c7c369..ea6da251e 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -45,6 +45,7 @@ class MarkerLayer extends StatelessWidget { return MobileLayerTransformer( child: Stack( children: (List markers) sync* { + final double worldWidth = map.getWorldWidthAtZoom(); for (final m in markers) { // Resolve real alignment // TODO this can probably just be done with calls to Size, Offset, and Rect @@ -56,33 +57,61 @@ 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), - ), - )) { + Positioned? getPositioned(final num? deltaX) { + final otherX = pxPoint.dx + (deltaX ?? 0); + // Cull if out of bounds + if (!map.pixelBounds.overlaps( + Rect.fromPoints( + Offset(otherX + left, pxPoint.dy - bottom), + Offset(otherX - right, pxPoint.dy + top), + ), + )) { + return null; + } + + final otherPoint = + deltaX == null ? pxPoint : Offset(otherX, pxPoint.dy); + // Apply map camera to marker position + final pos = otherPoint - map.pixelOrigin; + + return 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, + ); + } + + final main = getPositioned(null); + if (main == null) { continue; } + yield main; - // Apply map camera to marker position - final pos = pxPoint - map.pixelOrigin; + if (worldWidth == 0) { + 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, - ); + const directions = [-1, 1]; + for (final int direction in directions) { + double shift = 0; + while (true) { + shift += direction * worldWidth; + final additional = getPositioned(shift); + if (additional == null) { + break; + } + yield additional; + } + } } }(markers) .toList(), diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index bef6bc65c..dcde25d59 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -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) => From 38bdce3141c4a696a76c15108013155b58dbc697 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sat, 14 Dec 2024 17:51:16 +0100 Subject: [PATCH 2/6] Optimization suggestion --- lib/src/layer/marker_layer/marker_layer.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index ea6da251e..c7da90bbd 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -100,6 +100,7 @@ class MarkerLayer extends StatelessWidget { continue; } + // TODO: optimization - we may not even try if the visible world is smaller than a world width. const directions = [-1, 1]; for (final int direction in directions) { double shift = 0; From a38066883afc4c96f2cc6ea4c24a5eb2d1d12d58 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 15 Dec 2024 16:49:34 +0100 Subject: [PATCH 3/6] fixed "Marker projected to the wrong world" bug --- example/lib/pages/multi_worlds.dart | 13 +++++++++++++ lib/src/layer/marker_layer/marker_layer.dart | 7 +++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/example/lib/pages/multi_worlds.dart b/example/lib/pages/multi_worlds.dart index c3ce5f0a9..a82171bb1 100644 --- a/example/lib/pages/multi_worlds.dart +++ b/example/lib/pages/multi_worlds.dart @@ -59,6 +59,19 @@ class _MultiWorldsPageState extends State { 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), + ), + ), ], ), ], diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index c7da90bbd..7885b1534 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -91,16 +91,15 @@ class MarkerLayer extends StatelessWidget { } final main = getPositioned(null); - if (main == null) { - continue; + if (main != null) { + yield main; } - yield main; if (worldWidth == 0) { continue; } - // TODO: optimization - we may not even try if the visible world is smaller than a world width. + // TODO: optimization - find a way to skip these tests in some obvious situations. const directions = [-1, 1]; for (final int direction in directions) { double shift = 0; From 9591ee61bc7b0a8911c26cecc83f6b90f98cf1c9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 19 Dec 2024 21:59:42 +0100 Subject: [PATCH 4/6] Minor refactoring of `MarkerLayer` `Marker` positioning --- lib/src/layer/marker_layer/marker_layer.dart | 60 ++++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 7885b1534..1cba59b49 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -41,14 +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 markers) sync* { - final double worldWidth = map.getWorldWidthAtZoom(); 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; @@ -57,29 +57,30 @@ class MarkerLayer extends StatelessWidget { // Perform projection final pxPoint = map.projectAtZoom(m.point); - Positioned? getPositioned(final num? deltaX) { - final otherX = pxPoint.dx + (deltaX ?? 0); + Positioned? getPositioned(double worldShift) { + final shiftedX = pxPoint.dx + worldShift; + // Cull if out of bounds if (!map.pixelBounds.overlaps( Rect.fromPoints( - Offset(otherX + left, pxPoint.dy - bottom), - Offset(otherX - right, pxPoint.dy + top), + Offset(shiftedX + left, pxPoint.dy - bottom), + Offset(shiftedX - right, pxPoint.dy + top), ), )) { return null; } - final otherPoint = - deltaX == null ? pxPoint : Offset(otherX, pxPoint.dy); - // Apply map camera to marker position - final pos = otherPoint - map.pixelOrigin; + // 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: pos.dx - right, - top: pos.dy - bottom, + left: shiftedLocalPoint.dx - right, + top: shiftedLocalPoint.dy - bottom, child: (m.rotate ?? rotate) ? Transform.rotate( angle: -map.rotationRad, @@ -90,27 +91,24 @@ class MarkerLayer extends StatelessWidget { ); } - final main = getPositioned(null); - if (main != null) { - yield main; - } + // Create marker in main world, unless culled + if (getPositioned(0) case final main?) 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 - if (worldWidth == 0) { - continue; + // 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; } - - // TODO: optimization - find a way to skip these tests in some obvious situations. - const directions = [-1, 1]; - for (final int direction in directions) { - double shift = 0; - while (true) { - shift += direction * 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) From d9c8e8d6459eb82db7e27997af82f51a102a5ea9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 20 Dec 2024 10:21:41 +0100 Subject: [PATCH 5/6] Removed if-case statement --- lib/src/layer/marker_layer/marker_layer.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 1cba59b49..7e1199815 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -92,7 +92,8 @@ class MarkerLayer extends StatelessWidget { } // Create marker in main world, unless culled - if (getPositioned(0) case final main?) yield main; + 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 From 6d3135e64762ca0b3eb58e5320d3eb844c333491 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 20 Dec 2024 12:07:51 +0100 Subject: [PATCH 6/6] Re-added TODO comment --- lib/src/layer/marker_layer/marker_layer.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/layer/marker_layer/marker_layer.dart b/lib/src/layer/marker_layer/marker_layer.dart index 7e1199815..fe4693d46 100644 --- a/lib/src/layer/marker_layer/marker_layer.dart +++ b/lib/src/layer/marker_layer/marker_layer.dart @@ -97,6 +97,11 @@ class MarkerLayer extends StatelessWidget { // 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 + // 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