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

Satellite Map with Terrain Relief in Flutter

This tutorial shows how to display satellite imagery combined with a tilted 3D-style camera and hillshade relief — ideal for outdoor, hiking, and geographic exploration apps.

Prerequisites

Before you begin, ensure you have:

What this SDK actually supports for terrain

There is no mapController.setTerrain(...) method and no "exaggeration" parameter in this SDK — StyleController's real methods are addSource, addLayer, updateGeoJsonSource, removeLayer, removeSource, getAttributions, addImage/addImages/addSprite, removeImage, and setProjection. There's no call that displaces the map surface into 3D elevation.

What is real and gives you a comparable outdoor-map effect:

  • Tilted camera perspectiveMapOptions.initPitch/initBearing, and MapController.animateCamera(pitch: ..., bearing: ...), genuinely tilt and rotate the camera. This is what makes mountains and terrain "look 3D" even without true elevation displacement.
  • Hillshade relief shading — add a RasterDemSource (elevation tiles) and a HillshadeStyleLayer via StyleController.addSource/addLayer in onStyleLoaded. This draws shaded relief directly into the map's 2D surface — a real, supported way to convey terrain shape.
  • Satellite imagery — just point initStyle (or setStyleUri) at a style whose raster/vector layers include satellite tiles. There's no separate "satellite mode" flag; it's simply a different style document.

Basic Satellite View

Switch to a satellite style URL and use a tilted, rotated camera for a 3D-feeling perspective:

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

class SatelliteMapScreen extends StatefulWidget {
  @override
  _SatelliteMapScreenState createState() => _SatelliteMapScreenState();
}

class _SatelliteMapScreenState extends State<SatelliteMapScreen> {
  MapController? mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Satellite View')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_SATELLITE_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: Position(8.2275, 46.8182), // Swiss Alps (lng, lat)
          initZoom: 10.0,
          initPitch: 60.0,
          initBearing: 30.0,
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
      ),
    );
  }
}

Toggle Between Map and Satellite

Let users switch styles at runtime with MapController.setStyleUri — switching in-place, without destroying and recreating the native map:

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

class MapToggleScreen extends StatefulWidget {
  @override
  _MapToggleScreenState createState() => _MapToggleScreenState();
}

class _MapToggleScreenState extends State<MapToggleScreen> {
  MapController? mapController;
  bool isSatellite = false;

  final String mapStyleUrl =
      'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY';
  final String satelliteStyleUrl =
      'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_SATELLITE_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Map / Satellite Toggle')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initStyle: mapStyleUrl,
              initCenter: Position(2.2945, 48.8584), // lng, lat
              initZoom: 15.0,
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
          ),
          // 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) async {
                  final satellite = index == 1;
                  setState(() => isSatellite = satellite);
                  await mapController?.setStyleUri(
                    satellite ? satelliteStyleUrl : mapStyleUrl,
                  );
                  await mapController?.animateCamera(
                    pitch: satellite ? 45.0 : 0.0,
                  );
                },
                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 Hillshade Relief

Combine satellite imagery with a RasterDemSource + HillshadeStyleLayer for shaded terrain relief, plus a tilted camera:

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

class SatelliteTerrainScreen extends StatefulWidget {
  @override
  _SatelliteTerrainScreenState createState() =>
      _SatelliteTerrainScreenState();
}

const _terrainSourceId = 'terrain-source';
const _hillshadeLayerId = 'hillshade-layer';

class _SatelliteTerrainScreenState extends State<SatelliteTerrainScreen> {
  MapController? mapController;
  bool hillshadeEnabled = true;

  final List<Map<String, dynamic>> locations = [
    {'name': 'Swiss Alps', 'position': Position(8.228, 46.818), 'zoom': 10.0, 'bearing': 30.0},
    {'name': 'Grand Canyon', 'position': Position(-112.113, 36.107), 'zoom': 11.0, 'bearing': 90.0},
    {'name': 'Mount Fuji', 'position': Position(138.727, 35.361), 'zoom': 11.0, 'bearing': 200.0},
    {'name': 'Himalayas', 'position': Position(86.925, 27.988), 'zoom': 10.0, 'bearing': 45.0},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Satellite + Hillshade')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_SATELLITE_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: Position(8.228, 46.818), // lng, lat
              initZoom: 10.0,
              initPitch: 60.0,
              initBearing: 30.0,
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            onStyleLoaded: (style) async {
              if (hillshadeEnabled) await _addHillshade(style);
            },
          ),
          // 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(
                        center: loc['position'] as Position,
                        zoom: loc['zoom'] as double,
                        pitch: 60.0,
                        bearing: loc['bearing'] as double,
                      );
                    },
                    child: Text(loc['name'], style: TextStyle(fontSize: 11)),
                    style: ElevatedButton.styleFrom(
                      padding: EdgeInsets.symmetric(horizontal: 12),
                    ),
                  );
                },
              ),
            ),
          ),
          // Hillshade toggle
          Positioned(
            bottom: 16,
            left: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                child: Row(
                  children: [
                    Text('Hillshade relief'),
                    Switch(
                      value: hillshadeEnabled,
                      onChanged: (val) async {
                        setState(() => hillshadeEnabled = val);
                        final style = mapController?.style;
                        if (style == null) return;
                        if (val) {
                          await _addHillshade(style);
                        } else {
                          await style.removeLayer(_hillshadeLayerId);
                          await style.removeSource(_terrainSourceId);
                        }
                      },
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _addHillshade(StyleController style) async {
    await style.addSource(
      const RasterDemSource(
        id: _terrainSourceId,
        url: 'https://gateway.mapmetrics-atlas.net/terrain/tiles.json',
        tileSize: 256,
      ),
    );
    await style.addLayer(
      const HillshadeStyleLayer(
        id: _hillshadeLayerId,
        sourceId: _terrainSourceId,
        paint: {'hillshade-shadow-color': '#473B24'},
      ),
    );
  }
}

Next Steps


Tip: Satellite imagery + hillshade relief is data-heavy. For production apps, enable the hillshade layer only past a reasonable zoom level (e.g. 8+) to save bandwidth and improve load times at global zoom levels.