Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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 ​

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:

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> {
  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:

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> {
  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 ​


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.