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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
- Gradient Line — Color arcs by distance
- Animate Point Along Route — Moving markers
- Multiple Geometries — Combine lines and markers
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.