Skip to content

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:

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> {
  MapMetricsController? mapController;

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

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

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

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

  @override
  Widget build(BuildContext context) {
    final parisToNY = _generateArc(
      LatLng(48.8566, 2.3522),   // Paris
      LatLng(40.7128, -74.0060), // New York
    );

    return Scaffold(
      appBar: AppBar(title: Text('Flight Route')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(50.0, -35.0),
          zoom: 2.5,
        ),
        polylines: {
          Polyline(
            polylineId: PolylineId('flight_paris_ny'),
            points: parisToNY,
            color: Colors.blue,
            width: 3,
          ),
        },
        markers: {
          Marker(
            markerId: MarkerId('paris'),
            position: LatLng(48.8566, 2.3522),
            infoWindow: InfoWindow(title: 'Paris (CDG)'),
          ),
          Marker(
            markerId: MarkerId('new_york'),
            position: LatLng(40.7128, -74.0060),
            infoWindow: InfoWindow(title: 'New York (JFK)'),
          ),
        },
      ),
    );
  }
}

Multi-Route Flight Network

Display a hub-and-spoke flight network from a single airport:

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> {
  MapMetricsController? mapController;

  final LatLng hub = LatLng(48.8566, 2.3522); // Paris hub

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

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flight Network')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(30.0, 20.0),
              zoom: 1.8,
            ),
            polylines: destinations.map((dest) {
              final to = LatLng(dest['lat'], dest['lng']);
              return Polyline(
                polylineId: PolylineId('flight_${dest['name']}'),
                points: _generateArc(hub, to),
                color: (dest['color'] as Color).withOpacity(0.7),
                width: 2,
              );
            }).toSet(),
            markers: {
              // Hub marker
              Marker(
                markerId: MarkerId('hub'),
                position: hub,
                icon: BitmapDescriptor.defaultMarkerWithHue(
                    BitmapDescriptor.hueRed),
                infoWindow: InfoWindow(title: 'Paris (Hub)'),
              ),
              // Destination markers
              ...destinations.map((dest) => Marker(
                    markerId: MarkerId(dest['name']),
                    position: LatLng(dest['lat'], dest['lng']),
                    icon: BitmapDescriptor.defaultMarkerWithHue(
                        BitmapDescriptor.hueBlue),
                    infoWindow: InfoWindow(title: dest['name']),
                  )),
            },
          ),
          // 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:

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> {
  MapMetricsController? mapController;
  Timer? flightTimer;
  int currentIndex = 0;
  bool isFlying = false;

  late List<LatLng> flightPath;

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

  List<LatLng> _generateArc(LatLng from, LatLng to, {int segments = 100}) {
    final points = <LatLng>[];
    for (int i = 0; i <= segments; i++) {
      final t = i / segments;
      final lat = from.latitude + (to.latitude - from.latitude) * t;
      final lng = from.longitude + (to.longitude - from.longitude) * t;
      final dist = sqrt(pow(to.latitude - from.latitude, 2) +
          pow(to.longitude - from.longitude, 2));
      final alt = sin(t * pi) * dist * 0.15;
      points.add(LatLng(lat + alt, lng));
    }
    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: MapMetrics(
              styleUrl:
                  'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
              onMapCreated: (MapMetricsController controller) {
                mapController = controller;
              },
              initialCameraPosition: CameraPosition(
                target: LatLng(50.0, -35.0),
                zoom: 2.5,
              ),
              polylines: {
                // Full route (faded)
                Polyline(
                  polylineId: PolylineId('full_route'),
                  points: flightPath,
                  color: Colors.blue.withOpacity(0.3),
                  width: 2,
                ),
                // Traveled portion
                if (currentIndex > 0)
                  Polyline(
                    polylineId: PolylineId('traveled'),
                    points: flightPath.sublist(0, currentIndex + 1),
                    color: Colors.blue,
                    width: 3,
                  ),
              },
              markers: {
                Marker(
                  markerId: MarkerId('paris'),
                  position: flightPath.first,
                  infoWindow: InfoWindow(title: 'Paris (CDG)'),
                ),
                Marker(
                  markerId: MarkerId('new_york'),
                  position: flightPath.last,
                  infoWindow: InfoWindow(title: 'New York (JFK)'),
                ),
                Marker(
                  markerId: MarkerId('plane'),
                  position: flightPath[currentIndex],
                  icon: BitmapDescriptor.defaultMarkerWithHue(
                      BitmapDescriptor.hueAzure),
                ),
              },
            ),
          ),
        ],
      ),
      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.