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.