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

Measure Distances in Flutter ​

This tutorial shows how to let users tap on the map to place points and measure the distance between them using the Haversine formula.

Prerequisites ​

Before you begin, ensure you have:

Complete Measurement Tool ​

Tap on the map to add points. A line connects them and the total distance is calculated:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
import 'dart:math';

class MeasureDistanceScreen extends StatefulWidget {
  @override
  _MeasureDistanceScreenState createState() => _MeasureDistanceScreenState();
}

class _MeasureDistanceScreenState extends State<MeasureDistanceScreen> {
  MapController? mapController;
  List<Position> points = [];

  List<Marker> get _markers => points.asMap().entries.map((entry) {
    final isStart = entry.key == 0;
    return Marker(
      point: entry.value,
      size: const Size(28, 28),
      alignment: Alignment.bottomCenter,
      child: Icon(
        Icons.location_on,
        size: 28,
        color: isStart ? Colors.green : Colors.red,
      ),
    );
  }).toList();

  List<PolylineLayer> get _polylineLayers => points.length >= 2
      ? [
          PolylineLayer(
            polylines: [LineString(coordinates: points)],
            color: Colors.blue,
            width: 3,
            dashArray: const [12, 8],
          ),
        ]
      : const [];

  double get totalDistance {
    double total = 0;
    for (int i = 1; i < points.length; i++) {
      total += _haversine(points[i - 1], points[i]);
    }
    return total;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Measure Distance')),
      body: Column(
        children: [
          // Distance display
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(12),
            color: Colors.grey[100],
            child: Row(
              children: [
                Icon(Icons.straighten, size: 20, color: Colors.blue),
                SizedBox(width: 8),
                Text(
                  points.length < 2
                      ? 'Tap on the map to start measuring'
                      : 'Total: ${_formatDistance(totalDistance)}  |  ${points.length} points',
                  style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
                ),
                Spacer(),
                if (points.isNotEmpty)
                  TextButton(
                    onPressed: _clearPoints,
                    child: Text('Clear'),
                  ),
              ],
            ),
          ),
          // Segment details
          if (points.length >= 2)
            Container(
              height: 50,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                padding: EdgeInsets.symmetric(horizontal: 8),
                itemCount: points.length - 1,
                itemBuilder: (context, index) {
                  final dist = _haversine(points[index], points[index + 1]);
                  return Container(
                    margin: EdgeInsets.symmetric(horizontal: 4, vertical: 8),
                    padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6),
                    decoration: BoxDecoration(
                      color: Colors.blue[50],
                      borderRadius: BorderRadius.circular(16),
                    ),
                    child: Center(
                      child: Text(
                        '${index + 1}→${index + 2}: ${_formatDistance(dist)}',
                        style: TextStyle(fontSize: 12),
                      ),
                    ),
                  );
                },
              ),
            ),
          // Map
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
                initCenter: Position(2.3522, 48.8566), // Paris (lng, lat)
                initZoom: 13.0,
              ),
              onMapCreated: (controller) => mapController = controller,
              onEvent: (event) {
                if (event is MapEventClick) {
                  setState(() {
                    points.add(event.point);
                  });
                }
              },
              layers: _polylineLayers,
              mapChildren: [
                WidgetLayer(markers: _markers),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: points.isNotEmpty
          ? FloatingActionButton.small(
              onPressed: _undoLastPoint,
              child: Icon(Icons.undo),
              tooltip: 'Undo last point',
            )
          : null,
    );
  }

  void _clearPoints() {
    setState(() {
      points.clear();
    });
  }

  void _undoLastPoint() {
    if (points.isNotEmpty) {
      setState(() {
        points.removeLast();
      });
    }
  }

  /// Haversine formula to calculate distance between two Position points in meters
  double _haversine(Position a, Position b) {
    const double R = 6371000; // Earth's radius in meters
    final double lat1 = a.lat * pi / 180;
    final double lat2 = b.lat * pi / 180;
    final double dLat = (b.lat - a.lat) * pi / 180;
    final double dLng = (b.lng - a.lng) * pi / 180;

    final double h = sin(dLat / 2) * sin(dLat / 2) +
        cos(lat1) * cos(lat2) * sin(dLng / 2) * sin(dLng / 2);
    final double c = 2 * atan2(sqrt(h), sqrt(1 - h));

    return R * c;
  }

  /// Format distance for display
  String _formatDistance(double meters) {
    if (meters >= 1000) {
      return '${(meters / 1000).toStringAsFixed(2)} km';
    } else {
      return '${meters.toStringAsFixed(0)} m';
    }
  }
}

How Tap Detection Works ​

MapMetricsView doesn't expose a per-widget onMapClick callback. Instead, pass an onEvent callback and pattern-match on the sealed MapEvent type. Every tap arrives as a MapEventClick carrying a Position (longitude first) at event.point:

dart
onEvent: (event) {
  if (event is MapEventClick) {
    setState(() {
      points.add(event.point);
    });
  }
},

The point markers themselves are rendered with WidgetLayer/Marker (real Flutter widgets positioned over the map, from mapmetrics/src/widget_layer.dart) rather than a Marker/MarkerId Google-Maps-style API, which does not exist in this SDK.

How the Haversine Formula Works ​

The Haversine formula calculates the great-circle distance between two points on a sphere:

dart
double _haversine(Position a, Position b) {
  const double R = 6371000; // Earth's radius in meters
  final double lat1 = a.lat * pi / 180;
  final double lat2 = b.lat * pi / 180;
  final double dLat = (b.lat - a.lat) * pi / 180;
  final double dLng = (b.lng - a.lng) * pi / 180;

  final double h = sin(dLat / 2) * sin(dLat / 2) +
      cos(lat1) * cos(lat2) * sin(dLng / 2) * sin(dLng / 2);
  final double c = 2 * atan2(sqrt(h), sqrt(1 - h));

  return R * c; // Distance in meters
}

This requires import 'dart:math'; for the sin, cos, atan2, sqrt, and pi functions. Note that Position (from the geotypes package) exposes .lng and .lat — not .longitude/.latitude — and is always longitude-first.

Next Steps ​


Tip: The Haversine formula gives great-circle distance (straight line over the Earth's surface). For road/walking distance, you would need to use the MapMetrics Directions API.