Fit Map to a LineString in Flutter
This tutorial shows how to automatically zoom and pan the map so that an entire route or LineString fits within the visible area.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Fit to a Route
Calculate the bounding box of a polyline and fit the camera to it:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
import 'dart:math';
class FitToLineStringScreen extends StatefulWidget {
@override
_FitToLineStringScreenState createState() => _FitToLineStringScreenState();
}
class _FitToLineStringScreenState extends State<FitToLineStringScreen> {
MapMetricsController? mapController;
final List<LatLng> routePoints = [
LatLng(40.4168, -3.7038), // Madrid
LatLng(41.3851, 2.1734), // Barcelona
LatLng(43.2965, 5.3698), // Marseille
LatLng(45.764, 4.8357), // Lyon
LatLng(48.8566, 2.3522), // Paris
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Fit to LineString')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(44.0, 2.0),
zoom: 4.0,
),
onStyleLoaded: () {
_fitToRoute();
},
polylines: {
Polyline(
polylineId: PolylineId('route'),
points: routePoints,
color: Colors.blue,
width: 4,
),
},
markers: {
Marker(
markerId: MarkerId('start'),
position: routePoints.first,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueGreen),
infoWindow: InfoWindow(title: 'Start: Madrid'),
),
Marker(
markerId: MarkerId('end'),
position: routePoints.last,
icon: BitmapDescriptor.defaultMarkerWithHue(
BitmapDescriptor.hueRed),
infoWindow: InfoWindow(title: 'End: Paris'),
),
},
),
floatingActionButton: FloatingActionButton(
onPressed: _fitToRoute,
child: Icon(Icons.fit_screen),
tooltip: 'Fit to Route',
),
);
}
void _fitToRoute() {
final bounds = _calculateBounds(routePoints);
mapController?.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 60.0), // 60px padding
);
}
/// Calculate the bounding box for a list of points
LatLngBounds _calculateBounds(List<LatLng> points) {
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
for (final point in points) {
minLat = min(minLat, point.latitude);
maxLat = max(maxLat, point.latitude);
minLng = min(minLng, point.longitude);
maxLng = max(maxLng, point.longitude);
}
return LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
}
}Multiple Routes with Fit
Show several routes and fit the camera to the selected one:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
import 'dart:math';
class MultiRouteFitScreen extends StatefulWidget {
@override
_MultiRouteFitScreenState createState() => _MultiRouteFitScreenState();
}
class _MultiRouteFitScreenState extends State<MultiRouteFitScreen> {
MapMetricsController? mapController;
int selectedRoute = 0;
final List<Map<String, dynamic>> routes = [
{
'name': 'Spain to France',
'color': Colors.blue,
'points': [
LatLng(40.4168, -3.7038), // Madrid
LatLng(41.3851, 2.1734), // Barcelona
LatLng(43.2965, 5.3698), // Marseille
LatLng(48.8566, 2.3522), // Paris
],
},
{
'name': 'Germany to Italy',
'color': Colors.red,
'points': [
LatLng(52.52, 13.405), // Berlin
LatLng(48.1351, 11.582), // Munich
LatLng(47.2692, 11.4041), // Innsbruck
LatLng(45.4642, 9.19), // Milan
LatLng(41.9028, 12.4964), // Rome
],
},
{
'name': 'UK to Scandinavia',
'color': Colors.green,
'points': [
LatLng(51.5074, -0.1276), // London
LatLng(52.3676, 4.9041), // Amsterdam
LatLng(53.5511, 9.9937), // Hamburg
LatLng(55.6761, 12.5683), // Copenhagen
LatLng(59.3293, 18.0686), // Stockholm
],
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Multi-Route Fit')),
body: Column(
children: [
// Route selector chips
Container(
padding: EdgeInsets.all(12),
child: Wrap(
spacing: 8,
children: routes.asMap().entries.map((entry) {
final i = entry.key;
final route = entry.value;
return ChoiceChip(
label: Text(route['name']),
selected: selectedRoute == i,
selectedColor: (route['color'] as Color).withOpacity(0.3),
onSelected: (selected) {
if (selected) {
setState(() => selectedRoute = i);
_fitToSelectedRoute();
}
},
);
}).toList(),
),
),
// Map
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(48.0, 8.0),
zoom: 4.0,
),
onStyleLoaded: () {
_fitToSelectedRoute();
},
polylines: routes.asMap().entries.map((entry) {
final i = entry.key;
final route = entry.value;
return Polyline(
polylineId: PolylineId('route_$i'),
points: route['points'] as List<LatLng>,
color: i == selectedRoute
? route['color'] as Color
: (route['color'] as Color).withOpacity(0.3),
width: i == selectedRoute ? 5 : 2,
);
}).toSet(),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _fitToAll,
child: Icon(Icons.zoom_out_map),
tooltip: 'Fit All Routes',
),
);
}
void _fitToSelectedRoute() {
final points = routes[selectedRoute]['points'] as List<LatLng>;
final bounds = _calculateBounds(points);
mapController?.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 60.0),
);
}
void _fitToAll() {
final allPoints = <LatLng>[];
for (final route in routes) {
allPoints.addAll(route['points'] as List<LatLng>);
}
final bounds = _calculateBounds(allPoints);
mapController?.animateCamera(
CameraUpdate.newLatLngBounds(bounds, 60.0),
);
}
LatLngBounds _calculateBounds(List<LatLng> points) {
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
for (final point in points) {
minLat = min(minLat, point.latitude);
maxLat = max(maxLat, point.latitude);
minLng = min(minLng, point.longitude);
maxLng = max(maxLng, point.longitude);
}
return LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
);
}
}Next Steps
- Fit to Bounding Box — Fit to a defined area
- Fly to a Location — Smooth camera transitions
- Add a Polyline — Draw routes on the map
Tip: Add padding (the second parameter in newLatLngBounds) to keep route endpoints visible and not hidden behind UI elements like bottom sheets or floating buttons.