Satellite Map with Terrain Elevation in Flutter
This tutorial shows how to display satellite imagery combined with 3D terrain elevation — ideal for outdoor, hiking, and geographic exploration apps.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Satellite View
Switch to a satellite style URL for aerial imagery:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class SatelliteMapScreen extends StatefulWidget {
@override
_SatelliteMapScreenState createState() => _SatelliteMapScreenState();
}
class _SatelliteMapScreenState extends State<SatelliteMapScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Satellite View')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_SATELLITE_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(46.8182, 8.2275), // Swiss Alps
zoom: 10.0,
tilt: 60.0,
bearing: 30.0,
),
),
);
}
}Toggle Between Map and Satellite
Let users switch between standard map and satellite views:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MapToggleScreen extends StatefulWidget {
@override
_MapToggleScreenState createState() => _MapToggleScreenState();
}
class _MapToggleScreenState extends State<MapToggleScreen> {
MapMetricsController? mapController;
bool isSatellite = false;
final String mapStyleUrl =
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY';
final String satelliteStyleUrl =
'https://gateway.mapmetrics.org/styles/YOUR_SATELLITE_STYLE_ID?token=YOUR_API_KEY';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Map / Satellite Toggle')),
body: Stack(
children: [
MapMetrics(
styleUrl: isSatellite ? satelliteStyleUrl : mapStyleUrl,
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 15.0,
tilt: isSatellite ? 45.0 : 0.0,
),
),
// Toggle button
Positioned(
top: 16,
right: 16,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
),
child: ToggleButtons(
isSelected: [!isSatellite, isSatellite],
onPressed: (index) {
setState(() {
isSatellite = index == 1;
});
},
borderRadius: BorderRadius.circular(8),
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Icon(Icons.map, size: 18),
SizedBox(width: 4),
Text('Map'),
],
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Icon(Icons.satellite, size: 18),
SizedBox(width: 4),
Text('Satellite'),
],
),
),
],
),
),
),
],
),
);
}
}Satellite with 3D Terrain
Combine satellite imagery with terrain elevation for a dramatic effect:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class SatelliteTerrainScreen extends StatefulWidget {
@override
_SatelliteTerrainScreenState createState() =>
_SatelliteTerrainScreenState();
}
class _SatelliteTerrainScreenState extends State<SatelliteTerrainScreen> {
MapMetricsController? mapController;
double exaggeration = 1.5;
bool terrainEnabled = true;
final List<Map<String, dynamic>> locations = [
{'name': 'Swiss Alps', 'lat': 46.818, 'lng': 8.228, 'zoom': 10.0, 'bearing': 30.0},
{'name': 'Grand Canyon', 'lat': 36.107, 'lng': -112.113, 'zoom': 11.0, 'bearing': 90.0},
{'name': 'Mount Fuji', 'lat': 35.361, 'lng': 138.727, 'zoom': 11.0, 'bearing': 200.0},
{'name': 'Himalayas', 'lat': 27.988, 'lng': 86.925, 'zoom': 10.0, 'bearing': 45.0},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Satellite + Terrain')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_SATELLITE_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(46.818, 8.228),
zoom: 10.0,
tilt: 60.0,
bearing: 30.0,
),
onStyleLoaded: () {
if (terrainEnabled) _enableTerrain();
},
),
// Location buttons
Positioned(
bottom: 80,
left: 8,
right: 8,
child: SizedBox(
height: 40,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: locations.length,
separatorBuilder: (_, __) => SizedBox(width: 6),
itemBuilder: (context, i) {
final loc = locations[i];
return ElevatedButton(
onPressed: () {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(loc['lat'], loc['lng']),
zoom: loc['zoom'],
tilt: 60.0,
bearing: loc['bearing'],
),
),
);
},
child: Text(loc['name'], style: TextStyle(fontSize: 11)),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 12),
),
);
},
),
),
),
// Terrain toggle
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row(
children: [
Text('3D Terrain'),
Switch(
value: terrainEnabled,
onChanged: (val) {
setState(() => terrainEnabled = val);
if (val) {
_enableTerrain();
} else {
mapController?.setTerrain(
'terrain-source', exaggeration: 0.0);
}
},
),
Expanded(
child: Slider(
value: exaggeration,
min: 0.5,
max: 3.0,
divisions: 25,
label: '${exaggeration.toStringAsFixed(1)}x',
onChanged: terrainEnabled
? (val) {
setState(() => exaggeration = val);
mapController?.setTerrain(
'terrain-source', exaggeration: val);
}
: null,
),
),
],
),
),
),
),
],
),
);
}
void _enableTerrain() {
mapController?.addRasterDemSource(
'terrain-source',
'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
tileSize: 256,
);
mapController?.setTerrain('terrain-source', exaggeration: exaggeration);
}
}Next Steps
- 3D Terrain — Terrain-only examples
- 3D Buildings — Extruded buildings
- Customize Camera Animations — Cinematic camera
Tip: Satellite + terrain is very data-heavy. For production apps, enable terrain only when the user zooms past level 8 to save bandwidth and improve load times at global zoom levels.