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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Complete Measurement Tool
Tap on the map to add points. A line connects them and the total distance is calculated:
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:
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:
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
- Multiple Geometries — Draw styled lines and shapes on the map
- Popup on Click — Tap-based marker popups
- Navigation Controls — Camera and gesture handling
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.