Render World Copies in Flutter
Static pins render better with MarkerLayer
This page uses WidgetLayer, which repositions each marker in Dart every frame — it lags a frame while panning, ignores tilt and bearing, and does not scale past a few dozen markers. If your markers are plain pins with no Flutter content or gestures, use MarkerLayer instead; see Markers and Annotations.
This tutorial covers rendering routes and global data across the antimeridian (the 180th meridian / international date line) with mapmetrics.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
No renderWorldCopies toggle in this SDK
The current mapmetrics package (MapOptions and MapController) does not expose a renderWorldCopies property or a setRenderWorldCopies() method — there is no way to control whether the map repeats the world when zoomed out. MapOptions's real fields are initStyle, initZoom, initCenter, initPitch, initBearing, minZoom, maxZoom, minPitch, maxPitch, maxBounds, gestures, androidTextureMode, and androidMode — none of them control world wrapping.
If you need to prevent users from panning into a duplicate world, the closest real lever is MapOptions.maxBounds (a LngLatBounds), which constrains the camera to a bounding box — see Restrict Map Panning. It's not the same feature (it doesn't stop the base map tiles themselves from repeating), but it's what the SDK actually gives you today.
What is fully real and worth teaching here: drawing routes and markers that cross the antimeridian, using correct (longitude-first) coordinates.
Antimeridian-Crossing Routes
Draw a route that crosses the international date line (180th meridian):
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class AntimeridianRouteScreen extends StatefulWidget {
@override
_AntimeridianRouteScreenState createState() =>
_AntimeridianRouteScreenState();
}
class _AntimeridianRouteScreenState extends State<AntimeridianRouteScreen> {
MapController? mapController;
// Route: Tokyo → Honolulu (crosses the antimeridian)
final List<Position> route = [
Position(139.6503, 35.6762), // Tokyo (lng, lat)
Position(160.0, 35.0),
Position(180.0, 30.0), // Antimeridian
Position(-170.0, 25.0),
Position(-157.8583, 21.3069), // Honolulu
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Antimeridian Route')),
body: MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(180.0, 35.0), // Centered on the antimeridian
initZoom: 2.0,
),
onMapCreated: (MapController controller) {
mapController = controller;
},
layers: [
PolylineLayer(
polylines: [LineString(coordinates: route)],
color: Colors.blue,
width: 3,
),
],
mapChildren: [
WidgetLayer(
markers: [
Marker(
point: route.first,
size: const Size(28, 28),
alignment: Alignment.bottomCenter,
child: const Icon(Icons.flight_takeoff, color: Colors.red, size: 28),
),
Marker(
point: route.last,
size: const Size(28, 28),
alignment: Alignment.bottomCenter,
child: const Icon(Icons.flight_land, color: Colors.green, size: 28),
),
],
),
],
),
);
}
}Global Data Visualization
Display global data with correctly-ordered coordinates:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class GlobalDataScreen extends StatefulWidget {
@override
_GlobalDataScreenState createState() => _GlobalDataScreenState();
}
class _GlobalDataScreenState extends State<GlobalDataScreen> {
MapController? mapController;
final List<Map<String, dynamic>> globalOffices = [
{'city': 'New York', 'position': Position(-74.006, 40.713)},
{'city': 'London', 'position': Position(-0.128, 51.507)},
{'city': 'Dubai', 'position': Position(55.271, 25.205)},
{'city': 'Mumbai', 'position': Position(72.878, 19.076)},
{'city': 'Singapore', 'position': Position(103.820, 1.352)},
{'city': 'Tokyo', 'position': Position(139.650, 35.676)},
{'city': 'Sydney', 'position': Position(151.209, -33.869)},
{'city': 'Sao Paulo', 'position': Position(-46.633, -23.551)},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Global Offices')),
body: MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(0.0, 20.0), // lng, lat
initZoom: 1.5,
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [
WidgetLayer(
markers: globalOffices.map((office) {
return Marker(
point: office['position'] as Position,
size: const Size(28, 28),
alignment: Alignment.bottomCenter,
child: const Icon(Icons.business, color: Colors.indigo, size: 28),
);
}).toList(),
),
],
),
);
}
}When World Wrapping Matters
| Scenario | What you actually control today |
|---|---|
| Global flight map | Draw routes with correctly-ordered Positions; they render fine across the antimeridian even without a wrap toggle |
| Country-level app | Use MapOptions.maxBounds to keep the camera inside the region (see Restrict Map Panning) |
| City-level app | Not relevant — the base map doesn't repeat visibly at high zoom regardless |
| Dashboard/analytics | Show markers at their true (single) coordinate; no per-marker "which world copy" concept exists in this SDK |
Next Steps
- Restrict Map Panning — Lock the camera to a region with
maxBounds - Multiple Geometries — Markers, lines, polygons, circles together
- Navigation Controls — Camera and gesture handling
Tip: Whenever coordinates span the 180th meridian, just use the natural signed longitude values (e.g. 160.0 then 180.0 then -170.0) — you don't need any special wrap handling to draw a PolylineLayer route that crosses it.