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.