Skip to content

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> {
  MapMetricsController? mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hillshade Layer')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(46.8182, 8.2275), // Swiss Alps
          zoom: 9.0,
        ),
        onStyleLoaded: () {
          _addHillshade();
        },
      ),
    );
  }

  void _addHillshade() {
    // Add terrain DEM source
    mapController?.addRasterDemSource(
      'hillshade-source',
      'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
      tileSize: 256,
    );

    // Add hillshade layer
    mapController?.addHillshadeLayer(
      'hillshade-layer',
      'hillshade-source',
      hillshadeExaggeration: 0.5,
      hillshadeShadowColor: '#000000',
      hillshadeHighlightColor: '#ffffff',
      hillshadeAccentColor: '#000000',
      hillshadeIlluminationDirection: 315.0,
    );
  }
}

Hillshade with Adjustable Light Direction

Let users control the sun direction to see terrain from different angles:

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Adjustable Hillshade')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(46.8182, 8.2275),
              zoom: 9.0,
            ),
            onStyleLoaded: () {
              _addHillshade();
            },
          ),
          // 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);
                              mapController?.setPaintProperty(
                                'hillshade-layer',
                                'hillshade-illumination-direction',
                                val,
                              );
                            },
                          ),
                        ),
                      ],
                    ),
                    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);
                              mapController?.setPaintProperty(
                                'hillshade-layer',
                                'hillshade-exaggeration',
                                val,
                              );
                            },
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _addHillshade() {
    mapController?.addRasterDemSource(
      'hillshade-source',
      'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
      tileSize: 256,
    );

    mapController?.addHillshadeLayer(
      'hillshade-layer',
      'hillshade-source',
      hillshadeExaggeration: exaggeration,
      hillshadeIlluminationDirection: lightDirection,
    );
  }
}

Hillshade with Location Presets

Jump to famous mountain ranges to see the hillshade effect:

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

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

class _HillshadePresetsScreenState extends State<HillshadePresetsScreen> {
  MapMetricsController? 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(
                      CameraUpdate.newLatLngZoom(
                        LatLng(p['lat'], p['lng']),
                        p['zoom'],
                      ),
                    );
                  },
                );
              }).toList(),
            ),
          ),
          Expanded(
            child: MapMetrics(
              styleUrl:
                  'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
              onMapCreated: (MapMetricsController controller) {
                mapController = controller;
              },
              initialCameraPosition: CameraPosition(
                target: LatLng(46.818, 8.228),
                zoom: 9.0,
              ),
              onStyleLoaded: () {
                mapController?.addRasterDemSource(
                  'hillshade-source',
                  'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
                  tileSize: 256,
                );
                mapController?.addHillshadeLayer(
                  'hillshade-layer',
                  'hillshade-source',
                  hillshadeExaggeration: 0.5,
                  hillshadeIlluminationDirection: 315.0,
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Hillshade Properties

PropertyTypeDefaultDescription
hillshadeExaggerationdouble0.5Intensity of the shading (0.0 - 1.0)
hillshadeIlluminationDirectiondouble335.0Sun angle in degrees (0-360)
hillshadeShadowColorString#000000Color of shaded areas
hillshadeHighlightColorString#ffffffColor of illuminated areas
hillshadeAccentColorString#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.