Skip to content

Commit

Permalink
feat: replicate Markers across all worlds (#2000)
Browse files Browse the repository at this point in the history
Co-authored-by: Luka S <github@jaffaketchup.dev>
  • Loading branch information
monsieurtanuki and JaffaKetchup authored Jan 1, 2025
1 parent 5ac7dcf commit 7b39b26
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 25 deletions.
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

0 comments on commit 7b39b26

Please sign in to comment.