Skip to content

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:

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


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.