Skip to content

Add a Color Relief Layer in Flutter

This tutorial shows how to add a color relief (hypsometric tinting) layer to your MapMetrics Flutter map — coloring terrain by elevation bands from green lowlands to white mountain peaks.

Prerequisites

Before you begin, ensure you have:

Basic Color Relief

Color the map terrain based on elevation bands using a raster DEM source:

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

class ColorReliefScreen extends StatefulWidget {
  @override
  _ColorReliefScreenState createState() => _ColorReliefScreenState();
}

class _ColorReliefScreenState extends State<ColorReliefScreen> {
  MapMetricsController? mapController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Color Relief Layer')),
      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), // Swiss Alps
              zoom: 8.0,
            ),
            onStyleLoaded: () {
              _addColorRelief();
            },
          ),
          // Elevation legend
          Positioned(
            bottom: 16,
            left: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(10),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text('Elevation',
                        style: TextStyle(
                            fontWeight: FontWeight.bold, fontSize: 12)),
                    SizedBox(height: 6),
                    _elevRow(Color(0xFF1a6e1a), '0 - 200m'),
                    _elevRow(Color(0xFF4ca64c), '200 - 500m'),
                    _elevRow(Color(0xFFb3d98c), '500 - 1000m'),
                    _elevRow(Color(0xFFe6d96e), '1000 - 2000m'),
                    _elevRow(Color(0xFFc9854c), '2000 - 3000m'),
                    _elevRow(Color(0xFF8c5a2e), '3000 - 4000m'),
                    _elevRow(Color(0xFFffffff), '4000m+'),
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _elevRow(Color color, String label) {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 1),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 16,
            height: 12,
            decoration: BoxDecoration(
              color: color,
              border: Border.all(color: Colors.grey[400]!, width: 0.5),
            ),
          ),
          SizedBox(width: 6),
          Text(label, style: TextStyle(fontSize: 11)),
        ],
      ),
    );
  }

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

    // Add color relief raster layer with elevation-based color ramp
    mapController?.addRasterLayer(
      'color-relief',
      'terrain-dem',
      rasterColor: [
        'interpolate',
        ['linear'],
        ['raster-value'],
        0.0, '#1a6e1a',     // Deep green (sea level)
        0.05, '#4ca64c',    // Green (200m)
        0.125, '#b3d98c',   // Light green (500m)
        0.25, '#e6d96e',    // Yellow-green (1000m)
        0.5, '#c9854c',     // Brown (2000m)
        0.75, '#8c5a2e',    // Dark brown (3000m)
        1.0, '#ffffff',     // White (4000m+)
      ],
      rasterOpacity: 0.6,
    );
  }
}

Color Relief with Hillshade

Combine elevation coloring with hillshade for a professional topographic look:

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

class ReliefHillshadeScreen extends StatefulWidget {
  @override
  _ReliefHillshadeScreenState createState() =>
      _ReliefHillshadeScreenState();
}

class _ReliefHillshadeScreenState extends State<ReliefHillshadeScreen> {
  MapMetricsController? mapController;
  bool showRelief = true;
  bool showHillshade = true;
  double reliefOpacity = 0.5;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Relief + 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(45.9763, 7.6586), // Matterhorn
              zoom: 10.0,
            ),
            onStyleLoaded: () {
              _addLayers();
            },
          ),
          // Controls
          Positioned(
            top: 16,
            right: 16,
            child: Card(
              child: Padding(
                padding: EdgeInsets.all(10),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text('Layers',
                        style: TextStyle(fontWeight: FontWeight.bold)),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Checkbox(
                          value: showRelief,
                          onChanged: (val) {
                            setState(() => showRelief = val!);
                            mapController?.setLayerVisibility(
                                'color-relief', val!);
                          },
                        ),
                        Text('Color Relief', style: TextStyle(fontSize: 13)),
                      ],
                    ),
                    Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Checkbox(
                          value: showHillshade,
                          onChanged: (val) {
                            setState(() => showHillshade = val!);
                            mapController?.setLayerVisibility(
                                'hillshade', val!);
                          },
                        ),
                        Text('Hillshade', style: TextStyle(fontSize: 13)),
                      ],
                    ),
                    SizedBox(height: 4),
                    Text('Opacity', style: TextStyle(fontSize: 12)),
                    SizedBox(
                      width: 150,
                      child: Slider(
                        value: reliefOpacity,
                        min: 0.1,
                        max: 1.0,
                        onChanged: (val) {
                          setState(() => reliefOpacity = val);
                          mapController?.setPaintProperty(
                            'color-relief',
                            'raster-opacity',
                            val,
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
          // Location presets
          Positioned(
            bottom: 16,
            left: 8,
            right: 8,
            child: SizedBox(
              height: 40,
              child: ListView(
                scrollDirection: Axis.horizontal,
                children: [
                  _locationChip('Matterhorn', 45.976, 7.659, 10.0),
                  SizedBox(width: 6),
                  _locationChip('Mont Blanc', 45.833, 6.865, 10.0),
                  SizedBox(width: 6),
                  _locationChip('Swiss Alps', 46.818, 8.228, 8.0),
                  SizedBox(width: 6),
                  _locationChip('Dolomites', 46.410, 11.844, 10.0),
                  SizedBox(width: 6),
                  _locationChip('Pyrenees', 42.695, 0.041, 8.0),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _locationChip(String name, double lat, double lng, double zoom) {
    return ActionChip(
      avatar: Icon(Icons.terrain, size: 14),
      label: Text(name, style: TextStyle(fontSize: 12)),
      onPressed: () {
        mapController?.animateCamera(
          CameraUpdate.newLatLngZoom(LatLng(lat, lng), zoom),
        );
      },
    );
  }

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

    // Hillshade layer (below relief for shadow effect)
    mapController?.addHillshadeLayer(
      'hillshade',
      'terrain-dem',
      hillshadeExaggeration: 0.5,
      hillshadeIlluminationDirection: 315.0,
    );

    // Color relief on top with transparency
    mapController?.addRasterLayer(
      'color-relief',
      'terrain-dem',
      rasterColor: [
        'interpolate',
        ['linear'],
        ['raster-value'],
        0.0, '#1a6e1a',
        0.05, '#4ca64c',
        0.125, '#b3d98c',
        0.25, '#e6d96e',
        0.5, '#c9854c',
        0.75, '#8c5a2e',
        1.0, '#ffffff',
      ],
      rasterOpacity: reliefOpacity,
    );
  }
}

Color Scheme Switcher

Switch between different elevation color palettes:

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

class ReliefSchemesScreen extends StatefulWidget {
  @override
  _ReliefSchemesScreenState createState() => _ReliefSchemesScreenState();
}

class _ReliefSchemesScreenState extends State<ReliefSchemesScreen> {
  MapMetricsController? mapController;
  String activeScheme = 'natural';

  final Map<String, Map<String, dynamic>> schemes = {
    'natural': {
      'name': 'Natural',
      'colors': [
        0.0, '#1a6e1a', 0.05, '#4ca64c', 0.125, '#b3d98c',
        0.25, '#e6d96e', 0.5, '#c9854c', 0.75, '#8c5a2e', 1.0, '#ffffff',
      ],
    },
    'ocean': {
      'name': 'Ocean Blue',
      'colors': [
        0.0, '#0d47a1', 0.1, '#1565c0', 0.25, '#42a5f5',
        0.5, '#90caf9', 0.75, '#bbdefb', 1.0, '#e3f2fd',
      ],
    },
    'thermal': {
      'name': 'Thermal',
      'colors': [
        0.0, '#00008B', 0.15, '#0000FF', 0.3, '#00FFFF',
        0.5, '#00FF00', 0.7, '#FFFF00', 0.85, '#FF8C00', 1.0, '#FF0000',
      ],
    },
    'grayscale': {
      'name': 'Grayscale',
      'colors': [
        0.0, '#1a1a1a', 0.25, '#4d4d4d', 0.5, '#808080',
        0.75, '#b3b3b3', 1.0, '#ffffff',
      ],
    },
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Relief Color Schemes')),
      body: Column(
        children: [
          Container(
            padding: EdgeInsets.all(8),
            child: Wrap(
              spacing: 8,
              children: schemes.entries.map((entry) {
                return ChoiceChip(
                  label: Text(entry.value['name']),
                  selected: activeScheme == entry.key,
                  onSelected: (selected) {
                    if (selected) {
                      setState(() => activeScheme = entry.key);
                      _applyScheme(entry.value['colors']);
                    }
                  },
                );
              }).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: 8.0,
              ),
              onStyleLoaded: () {
                _setupRelief();
              },
            ),
          ),
        ],
      ),
    );
  }

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

    final colors = schemes[activeScheme]!['colors'] as List;

    mapController?.addRasterLayer(
      'color-relief',
      'terrain-dem',
      rasterColor: [
        'interpolate',
        ['linear'],
        ['raster-value'],
        ...colors,
      ],
      rasterOpacity: 0.6,
    );
  }

  void _applyScheme(List colors) {
    // Remove and re-add with new color ramp
    mapController?.removeLayer('color-relief');

    mapController?.addRasterLayer(
      'color-relief',
      'terrain-dem',
      rasterColor: [
        'interpolate',
        ['linear'],
        ['raster-value'],
        ...colors,
      ],
      rasterOpacity: 0.6,
    );
  }
}

Color Relief Properties

PropertyTypeDescription
rasterColorListColor interpolation expression mapping elevation to colors
rasterOpacitydoubleLayer transparency (0.0 - 1.0)
rasterValueExpressionNormalized elevation value from DEM (0.0 - 1.0)

Standard Elevation Color Bands

ElevationColorTerrain Type
0 - 200mDark greenLowlands, valleys
200 - 500mGreenHills, foothills
500 - 1000mLight greenUplands
1000 - 2000mYellow-greenMountains
2000 - 3000mBrownHigh mountains
3000 - 4000mDark brownAlpine zone
4000m+WhiteGlaciers, snow

Next Steps


Tip: Combine color relief + hillshade + contour lines for a complete topographic map. Set the relief layer opacity to 0.4-0.6 so the base map labels remain readable underneath.