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
Draw the route with a PolylineLayer, mark the endpoints with CircleLayers, and calculate the bounding box of the route to fit the camera to it with MapController.fitBounds:
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> {
MapController? mapController;
final List<Position> routePoints = [
Position(-3.7038, 40.4168), // Madrid
Position(2.1734, 41.3851), // Barcelona
Position(5.3698, 43.2965), // Marseille
Position(4.8357, 45.764), // Lyon
Position(2.3522, 48.8566), // Paris
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Fit to LineString')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.0, 44.0), // lng, lat
initZoom: 4,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onStyleLoaded: (StyleController style) {
_fitToRoute();
},
layers: [
PolylineLayer(
polylines: [LineString(coordinates: routePoints)],
color: Colors.blue,
width: 4,
),
CircleLayer(
points: [Point(coordinates: routePoints.first)],
color: Colors.green,
radius: 8,
strokeColor: Colors.white,
strokeWidth: 2,
),
CircleLayer(
points: [Point(coordinates: routePoints.last)],
color: Colors.red,
radius: 8,
strokeColor: Colors.white,
strokeWidth: 2,
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _fitToRoute,
child: Icon(Icons.fit_screen),
tooltip: 'Fit to Route',
),
);
}
void _fitToRoute() {
final bounds = _calculateBounds(routePoints);
mapController?.fitBounds(
bounds: bounds,
padding: const EdgeInsets.all(60), // 60px padding
);
}
/// Calculate the bounding box for a list of positions
LngLatBounds _calculateBounds(List<Position> points) {
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
for (final point in points) {
final lat = point.lat.toDouble();
final lng = point.lng.toDouble();
minLat = min(minLat, lat);
maxLat = max(maxLat, lat);
minLng = min(minLng, lng);
maxLng = max(maxLng, lng);
}
return LngLatBounds(
longitudeWest: minLng,
longitudeEast: maxLng,
latitudeSouth: minLat,
latitudeNorth: maxLat,
);
}
}Multiple Routes with Fit
Show several routes and fit the camera to the selected one. Each route is its own PolylineLayer so the selected route can be drawn thicker and brighter than the rest:
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> {
MapController? mapController;
int selectedRoute = 0;
final List<Map<String, dynamic>> routes = [
{
'name': 'Spain to France',
'color': Colors.blue,
'points': [
Position(-3.7038, 40.4168), // Madrid
Position(2.1734, 41.3851), // Barcelona
Position(5.3698, 43.2965), // Marseille
Position(2.3522, 48.8566), // Paris
],
},
{
'name': 'Germany to Italy',
'color': Colors.red,
'points': [
Position(13.405, 52.52), // Berlin
Position(11.582, 48.1351), // Munich
Position(11.4041, 47.2692), // Innsbruck
Position(9.19, 45.4642), // Milan
Position(12.4964, 41.9028), // Rome
],
},
{
'name': 'UK to Scandinavia',
'color': Colors.green,
'points': [
Position(-0.1276, 51.5074), // London
Position(4.9041, 52.3676), // Amsterdam
Position(9.9937, 53.5511), // Hamburg
Position(12.5683, 55.6761), // Copenhagen
Position(18.0686, 59.3293), // 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: MapMetricsView(
options: MapOptions(
initCenter: Position(8.0, 48.0), // lng, lat
initZoom: 4,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onStyleLoaded: (StyleController style) {
_fitToSelectedRoute();
},
layers: routes.asMap().entries.map((entry) {
final i = entry.key;
final route = entry.value;
final isSelected = i == selectedRoute;
return PolylineLayer(
polylines: [
LineString(coordinates: route['points'] as List<Position>),
],
color: isSelected
? route['color'] as Color
: (route['color'] as Color).withOpacity(0.3),
width: isSelected ? 5 : 2,
);
}).toList(),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _fitToAll,
child: Icon(Icons.zoom_out_map),
tooltip: 'Fit All Routes',
),
);
}
void _fitToSelectedRoute() {
final points = routes[selectedRoute]['points'] as List<Position>;
final bounds = _calculateBounds(points);
mapController?.fitBounds(bounds: bounds, padding: const EdgeInsets.all(60));
}
void _fitToAll() {
final allPoints = <Position>[];
for (final route in routes) {
allPoints.addAll(route['points'] as List<Position>);
}
final bounds = _calculateBounds(allPoints);
mapController?.fitBounds(bounds: bounds, padding: const EdgeInsets.all(60));
}
LngLatBounds _calculateBounds(List<Position> points) {
double minLat = double.infinity;
double maxLat = -double.infinity;
double minLng = double.infinity;
double maxLng = -double.infinity;
for (final point in points) {
final lat = point.lat.toDouble();
final lng = point.lng.toDouble();
minLat = min(minLat, lat);
maxLat = max(maxLat, lat);
minLng = min(minLng, lng);
maxLng = max(maxLng, lng);
}
return LngLatBounds(
longitudeWest: minLng,
longitudeEast: maxLng,
latitudeSouth: minLat,
latitudeNorth: maxLat,
);
}
}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 padding argument to fitBounds) to keep route endpoints visible and not hidden behind UI elements like bottom sheets or floating buttons.