Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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:

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):

dart
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:

dart
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

ScenarioWhat you actually control today
Global flight mapDraw routes with correctly-ordered Positions; they render fine across the antimeridian even without a wrap toggle
Country-level appUse MapOptions.maxBounds to keep the camera inside the region (see Restrict Map Panning)
City-level appNot relevant — the base map doesn't repeat visibly at high zoom regardless
Dashboard/analyticsShow markers at their true (single) coordinate; no per-marker "which world copy" concept exists in this SDK

Next Steps


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.