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

Arc Layer — Flight Routes in Flutter

This tutorial shows how to draw curved arc lines between locations — perfect for visualizing flight paths, connections between cities, or network maps.

Prerequisites

Before you begin, ensure you have:

There is no polylines: / markers: / Polyline / Marker widget API. Lines are rendered with PolylineLayer(polylines: List<LineString>, color:, width:) and points with MarkerLayer, both passed through the declarative layers: list on MapMetricsView. A LineString is built as LineString(coordinates: List<Position>).

Basic Flight Arc

Draw a curved arc between two cities by computing intermediate points on a great circle:

dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class FlightArcScreen extends StatefulWidget {
  @override
  _FlightArcScreenState createState() => _FlightArcScreenState();
}

class _FlightArcScreenState extends State<FlightArcScreen> {
  MapController? mapController;

  /// Generate arc points between two locations
  List<Position> _generateArc(Position from, Position to, {int segments = 50}) {
    final points = <Position>[];
    for (int i = 0; i <= segments; i++) {
      final t = i / segments;

      // Linear interpolation of lat/lng
      final lat = from.lat + (to.lat - from.lat) * t;
      final lng = from.lng + (to.lng - from.lng) * t;

      // Add altitude curve (parabolic)
      final altFactor = sin(t * pi) * 2.0;
      final curvedLat = lat + altFactor * (to.lng - from.lng) * 0.05;

      points.add(Position(lng, curvedLat));
    }
    return points;
  }

  @override
  Widget build(BuildContext context) {
    final paris = Position(2.3522, 48.8566);
    final newYork = Position(-74.0060, 40.7128);
    final parisToNY = _generateArc(paris, newYork);

    return Scaffold(
      appBar: AppBar(title: Text('Flight Route')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(-35.0, 50.0),
          initZoom: 2.5,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        layers: [
          PolylineLayer(
            polylines: [LineString(coordinates: parisToNY)],
            color: Colors.blue,
            width: 3,
          ),
          MarkerLayer(
            points: [Point(coordinates: paris)],
            textField: 'Paris (CDG)',
            textOffset: const [0, 1],
          ),
          MarkerLayer(
            points: [Point(coordinates: newYork)],
            textField: 'New York (JFK)',
            textOffset: const [0, 1],
          ),
        ],
      ),
    );
  }
}

Multi-Route Flight Network

Display a hub-and-spoke flight network from a single airport. Each route needs its own color, so it gets its own PolylineLayer instance — one PolylineLayer colors every line it holds uniformly:

dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class FlightNetworkScreen extends StatefulWidget {
  @override
  _FlightNetworkScreenState createState() => _FlightNetworkScreenState();
}

class _FlightNetworkScreenState extends State<FlightNetworkScreen> {
  MapController? mapController;

  final Position hub = Position(2.3522, 48.8566); // Paris hub (lng, lat)

  // lng/lat order.
  final List<Map<String, dynamic>> destinations = [
    {'name': 'New York', 'lng': -74.0060, 'lat': 40.7128, 'color': Colors.blue},
    {'name': 'Tokyo', 'lng': 139.6503, 'lat': 35.6762, 'color': Colors.red},
    {'name': 'Dubai', 'lng': 55.2708, 'lat': 25.2048, 'color': Colors.orange},
    {'name': 'Sao Paulo', 'lng': -46.6333, 'lat': -23.5505, 'color': Colors.green},
    {'name': 'London', 'lng': -0.1276, 'lat': 51.5074, 'color': Colors.purple},
    {'name': 'Singapore', 'lng': 103.8198, 'lat': 1.3521, 'color': Colors.teal},
    {'name': 'Cairo', 'lng': 31.2357, 'lat': 30.0444, 'color': Colors.amber},
  ];

  List<Position> _generateArc(Position from, Position to, {int segments = 60}) {
    final points = <Position>[];
    for (int i = 0; i <= segments; i++) {
      final t = i / segments;
      final lat = from.lat + (to.lat - from.lat) * t;
      final lng = from.lng + (to.lng - from.lng) * t;

      // Calculate distance for arc height
      final dist = sqrt(pow(to.lat - from.lat, 2) +
          pow(to.lng - from.lng, 2));
      final altFactor = sin(t * pi) * dist * 0.15;

      points.add(Position(lng, lat + altFactor));
    }
    return points;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flight Network')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(20.0, 30.0),
              initZoom: 1.8,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            layers: [
              // One PolylineLayer per route so each can have its own color.
              for (final dest in destinations)
                PolylineLayer(
                  polylines: [
                    LineString(
                      coordinates: _generateArc(
                        hub,
                        Position(dest['lng'] as double, dest['lat'] as double),
                      ),
                    ),
                  ],
                  color: (dest['color'] as Color).withValues(alpha: 0.7),
                  width: 2,
                ),
              // Hub marker
              MarkerLayer(
                points: [Point(coordinates: hub)],
                textField: 'Paris (Hub)',
                textOffset: const [0, 1],
              ),
              // Destination markers
              for (final dest in destinations)
                MarkerLayer(
                  points: [
                    Point(
                      coordinates:
                          Position(dest['lng'] as double, dest['lat'] as double),
                    ),
                  ],
                  textField: dest['name'] as String,
                  textOffset: const [0, 1],
                ),
            ],
          ),
          // Flight count badge
          Positioned(
            top: 16,
            right: 16,
            child: Container(
              padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              decoration: BoxDecoration(
                color: Colors.black87,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                '${destinations.length} routes from Paris',
                style: TextStyle(color: Colors.white, fontSize: 13),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Animated Flight Path

Animate a plane icon moving along a flight arc. As with other moving-point tutorials, the traveled portion of the line and the plane's position are rebuilt on every tick via setState, which redraws the layers: list:

dart
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class AnimatedFlightScreen extends StatefulWidget {
  @override
  _AnimatedFlightScreenState createState() => _AnimatedFlightScreenState();
}

class _AnimatedFlightScreenState extends State<AnimatedFlightScreen> {
  MapController? mapController;
  Timer? flightTimer;
  int currentIndex = 0;
  bool isFlying = false;

  late List<Position> flightPath;

  @override
  void initState() {
    super.initState();
    flightPath = _generateArc(
      Position(2.3522, 48.8566),   // Paris
      Position(-74.0060, 40.7128), // New York
      segments: 200,
    );
  }

  List<Position> _generateArc(Position from, Position to, {int segments = 100}) {
    final points = <Position>[];
    for (int i = 0; i <= segments; i++) {
      final t = i / segments;
      final lat = from.lat + (to.lat - from.lat) * t;
      final lng = from.lng + (to.lng - from.lng) * t;
      final dist = sqrt(pow(to.lat - from.lat, 2) +
          pow(to.lng - from.lng, 2));
      final alt = sin(t * pi) * dist * 0.15;
      points.add(Position(lng, lat + alt));
    }
    return points;
  }

  @override
  Widget build(BuildContext context) {
    final progress = flightPath.isEmpty
        ? 0.0
        : currentIndex / (flightPath.length - 1);

    return Scaffold(
      appBar: AppBar(title: Text('Animated Flight')),
      body: Column(
        children: [
          // Flight info bar
          Container(
            padding: EdgeInsets.all(12),
            color: Colors.blue[50],
            child: Row(
              children: [
                Text('CDG', style: TextStyle(fontWeight: FontWeight.bold)),
                Expanded(
                  child: Padding(
                    padding: EdgeInsets.symmetric(horizontal: 12),
                    child: LinearProgressIndicator(value: progress),
                  ),
                ),
                Text('JFK', style: TextStyle(fontWeight: FontWeight.bold)),
              ],
            ),
          ),
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initCenter: Position(-35.0, 50.0),
                initZoom: 2.5,
                initStyle:
                    'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              ),
              onMapCreated: (MapController controller) {
                mapController = controller;
              },
              layers: [
                // Full route (faded)
                PolylineLayer(
                  polylines: [LineString(coordinates: flightPath)],
                  color: Colors.blue.withValues(alpha: 0.3),
                  width: 2,
                ),
                // Traveled portion
                if (currentIndex > 0)
                  PolylineLayer(
                    polylines: [
                      LineString(
                        coordinates: flightPath.sublist(0, currentIndex + 1),
                      ),
                    ],
                    color: Colors.blue,
                    width: 3,
                  ),
                MarkerLayer(
                  points: [Point(coordinates: flightPath.first)],
                  textField: 'Paris (CDG)',
                  textOffset: const [0, 1],
                ),
                MarkerLayer(
                  points: [Point(coordinates: flightPath.last)],
                  textField: 'New York (JFK)',
                  textOffset: const [0, 1],
                ),
                CircleLayer(
                  points: [Point(coordinates: flightPath[currentIndex])],
                  radius: 6,
                  color: Colors.lightBlue,
                  strokeColor: Colors.white,
                  strokeWidth: 2,
                ),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: isFlying ? _stopFlight : _startFlight,
        child: Icon(isFlying ? Icons.pause : Icons.flight_takeoff),
      ),
    );
  }

  void _startFlight() {
    setState(() {
      isFlying = true;
      if (currentIndex >= flightPath.length - 1) currentIndex = 0;
    });
    flightTimer = Timer.periodic(Duration(milliseconds: 30), (_) {
      if (currentIndex >= flightPath.length - 1) {
        _stopFlight();
        return;
      }
      setState(() => currentIndex++);
    });
  }

  void _stopFlight() {
    flightTimer?.cancel();
    setState(() => isFlying = false);
  }

  @override
  void dispose() {
    flightTimer?.cancel();
    super.dispose();
  }
}

Next Steps


Tip: For realistic great-circle arcs on a flat map, increase the segments count for long-distance routes. The parabolic altitude offset (sin(t * pi)) creates the visual curve — adjust the multiplier to control arc height. Remember Position is (lng, lat) throughout — the interpolation math above operates on .lng/.lat fields, not a LatLng.longitude/.latitude pair.