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

Add a Hillshade Layer in Flutter

This tutorial shows how to add a hillshade layer to your MapMetrics Flutter map — creating a shaded relief effect that makes terrain features like mountains, valleys, and ridges visible on a 2D map.

Prerequisites

Before you begin, ensure you have:

Basic Hillshade

Add a hillshade layer using a raster DEM (Digital Elevation Model) source:

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

class HillshadeScreen extends StatefulWidget {
  @override
  _HillshadeScreenState createState() => _HillshadeScreenState();
}

class _HillshadeScreenState extends State<HillshadeScreen> {
  MapController? mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hillshade Layer')),
      body: MapMetricsView(
        options: MapOptions(
          initCenter: Position(8.2275, 46.8182), // Swiss Alps (lng, lat)
          initZoom: 9.0,
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
        ),
        onMapCreated: (MapController controller) {
          mapController = controller;
        },
        onStyleLoaded: (StyleController style) {
          _addHillshade(style);
        },
      ),
    );
  }

  Future<void> _addHillshade(StyleController style) async {
    // Add terrain DEM source
    await style.addSource(
      const RasterDemSource(
        id: 'hillshade-source',
        tiles: ['https://gateway.mapmetrics-atlas.net/terrain/{z}/{x}/{y}.png'],
        tileSize: 256,
      ),
    );

    // Add hillshade layer
    await style.addLayer(
      const HillshadeStyleLayer(
        id: 'hillshade-layer',
        sourceId: 'hillshade-source',
        paint: {
          'hillshade-exaggeration': 0.5,
          'hillshade-shadow-color': '#000000',
          'hillshade-highlight-color': '#ffffff',
          'hillshade-accent-color': '#000000',
          'hillshade-illumination-direction': 315.0,
        },
      ),
    );
  }
}

Hillshade with Adjustable Light Direction

Let users control the sun direction to see terrain from different angles. HillshadeStyleLayer is an immutable value — there is no setPaintProperty call — so an update means removing the layer and adding it back with new paint values:

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

class AdjustableHillshadeScreen extends StatefulWidget {
  @override
  _AdjustableHillshadeScreenState createState() =>
      _AdjustableHillshadeScreenState();
}

class _AdjustableHillshadeScreenState
    extends State<AdjustableHillshadeScreen> {
  MapController? mapController;
  StyleController? styleController;
  double lightDirection = 315.0;
  double exaggeration = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Adjustable Hillshade')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initCenter: Position(8.2275, 46.8182), // lng, lat
              initZoom: 9.0,
              initStyle:
                  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
            ),
            onMapCreated: (MapController controller) {
              mapController = controller;
            },
            onStyleLoaded: (StyleController style) {
              styleController = style;
              _addHillshade(style);
            },
          ),
          // Controls
          Positioned(
            bottom: 16,
            left: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(12),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Row(
                      children: [
                        Icon(Icons.wb_sunny, size: 18),
                        SizedBox(width: 8),
                        Text('Light: ${lightDirection.toInt()}°'),
                        Expanded(
                          child: Slider(
                            value: lightDirection,
                            min: 0,
                            max: 360,
                            onChanged: (val) {
                              setState(() => lightDirection = val);
                              _updateHillshade();
                            },
                          ),
                        ),
                      ],
                    ),
                    Row(
                      children: [
                        Icon(Icons.terrain, size: 18),
                        SizedBox(width: 8),
                        Text('Relief: ${exaggeration.toStringAsFixed(1)}'),
                        Expanded(
                          child: Slider(
                            value: exaggeration,
                            min: 0.0,
                            max: 1.0,
                            onChanged: (val) {
                              setState(() => exaggeration = val);
                              _updateHillshade();
                            },
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _addHillshade(StyleController style) async {
    await style.addSource(
      const RasterDemSource(
        id: 'hillshade-source',
        tiles: ['https://gateway.mapmetrics-atlas.net/terrain/{z}/{x}/{y}.png'],
        tileSize: 256,
      ),
    );

    await style.addLayer(
      HillshadeStyleLayer(
        id: 'hillshade-layer',
        sourceId: 'hillshade-source',
        paint: {
          'hillshade-exaggeration': exaggeration,
          'hillshade-illumination-direction': lightDirection,
        },
      ),
    );
  }

  Future<void> _updateHillshade() async {
    final style = styleController;
    if (style == null) return;
    // Re-add the layer with the current slider values in place of the old one.
    await style.removeLayer('hillshade-layer');
    await style.addLayer(
      HillshadeStyleLayer(
        id: 'hillshade-layer',
        sourceId: 'hillshade-source',
        paint: {
          'hillshade-exaggeration': exaggeration,
          'hillshade-illumination-direction': lightDirection,
        },
      ),
    );
  }
}

Hillshade with Location Presets

Jump to famous mountain ranges to see the hillshade effect. Camera moves use MapController.animateCamera(center: Position(lng, lat), zoom: ...), not a CameraUpdate helper:

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

class HillshadePresetsScreen extends StatefulWidget {
  @override
  _HillshadePresetsScreenState createState() =>
      _HillshadePresetsScreenState();
}

class _HillshadePresetsScreenState extends State<HillshadePresetsScreen> {
  MapController? mapController;

  final List<Map<String, dynamic>> presets = [
    {'name': 'Swiss Alps', 'lat': 46.818, 'lng': 8.228, 'zoom': 9.0},
    {'name': 'Norwegian Fjords', 'lat': 61.0, 'lng': 6.5, 'zoom': 8.0},
    {'name': 'Pyrenees', 'lat': 42.695, 'lng': 0.041, 'zoom': 8.0},
    {'name': 'Scottish Highlands', 'lat': 57.0, 'lng': -5.0, 'zoom': 8.0},
    {'name': 'Dolomites', 'lat': 46.410, 'lng': 11.844, 'zoom': 10.0},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hillshade Presets')),
      body: Column(
        children: [
          Container(
            padding: EdgeInsets.all(8),
            child: Wrap(
              spacing: 6,
              runSpacing: 6,
              children: presets.map((p) {
                return ActionChip(
                  avatar: Icon(Icons.terrain, size: 16),
                  label: Text(p['name'], style: TextStyle(fontSize: 12)),
                  onPressed: () {
                    mapController?.animateCamera(
                      center: Position(p['lng'] as double, p['lat'] as double),
                      zoom: p['zoom'] as double,
                    );
                  },
                );
              }).toList(),
            ),
          ),
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initCenter: Position(8.228, 46.818), // Swiss Alps (lng, lat)
                initZoom: 9.0,
                initStyle:
                    'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              ),
              onMapCreated: (MapController controller) {
                mapController = controller;
              },
              onStyleLoaded: (StyleController style) async {
                await style.addSource(
                  const RasterDemSource(
                    id: 'hillshade-source',
                    tiles: [
                      'https://gateway.mapmetrics-atlas.net/terrain/{z}/{x}/{y}.png',
                    ],
                    tileSize: 256,
                  ),
                );
                await style.addLayer(
                  const HillshadeStyleLayer(
                    id: 'hillshade-layer',
                    sourceId: 'hillshade-source',
                    paint: {
                      'hillshade-exaggeration': 0.5,
                      'hillshade-illumination-direction': 315.0,
                    },
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Hillshade Properties

HillshadeStyleLayer takes plain MapLibre style spec keys in its paint map:

PropertyTypeDefaultDescription
hillshade-exaggerationdouble0.5Intensity of the shading (0.0 - 1.0)
hillshade-illumination-directiondouble335.0Sun angle in degrees (0-360)
hillshade-shadow-colorString#000000Color of shaded areas
hillshade-highlight-colorString#ffffffColor of illuminated areas
hillshade-accent-colorString#000000Color for emphasizing terrain

Next Steps


Tip: Hillshade works on flat 2D maps (no tilt needed) and is lighter on performance than full 3D terrain. It's ideal for hiking apps where you want terrain visibility without the rendering overhead of 3D.