Add Contour Lines in Flutter
This tutorial shows how to add elevation contour lines to your MapMetrics Flutter map — essential for topographic maps, hiking apps, and geographic analysis.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Basic Contour Lines
Add contour lines from a vector tile source. Sources and layers are added through the StyleController you receive in onStyleLoaded — call addSource before addLayer, since a layer references its source by id:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ContourLinesScreen extends StatefulWidget {
@override
_ContourLinesScreenState createState() => _ContourLinesScreenState();
}
class _ContourLinesScreenState extends State<ContourLinesScreen> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Contour Lines')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(8.2275, 46.8182), // Swiss Alps (lng, lat)
initZoom: 11.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) {
_addContourLines(style);
},
),
);
}
Future<void> _addContourLines(StyleController style) async {
// Add contour source (vector tiles with elevation data)
await style.addSource(
const VectorSource(
id: 'contour-source',
tiles: ['https://gateway.mapmetrics-atlas.net/contours/{z}/{x}/{y}.pbf'],
sourceLayer: 'contour',
),
);
// Add contour lines
await style.addLayer(
const LineStyleLayer(
id: 'contour-lines',
sourceId: 'contour-source',
paint: {
'line-color': '#8B4513',
'line-width': 0.75,
'line-opacity': 0.6,
},
),
);
// Add elevation labels, limited to index (major) contours
await style.addLayer(
const SymbolStyleLayer(
id: 'contour-labels',
sourceId: 'contour-source',
layout: {
'text-field': '{ele}m',
'text-size': 10.0,
'symbol-placement': 'line',
},
paint: {'text-color': '#8B4513'},
filter: ['==', ['%', ['get', 'ele'], 500], 0],
),
);
}
}Note on minor vs. major contours:
SymbolStyleLayeraccepts afilter, so the elevation labels above are correctly limited to index (500 m) contours.LineStyleLayerin the current SDK release only exposeslayoutandpaint— it does not yet forwardfilter,minZoom, ormaxZoom— so you cannot style minor and major contour lines differently from a single vector source with a modulo filter. If you need visually distinct minor/major contour lines, ask your tile provider to publish them as two separate source layers (for examplecontour_minorandcontour_major) and add oneVectorSource+LineStyleLayerpair per source layer.
Contour Lines with Hillshade
Combine contour lines with hillshade for a classic topographic map. Because the real SDK has no setLayerVisibility call, layer toggles are implemented by adding or removing the layer itself with StyleController.addLayer / removeLayer:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class TopoMapScreen extends StatefulWidget {
@override
_TopoMapScreenState createState() => _TopoMapScreenState();
}
class _TopoMapScreenState extends State<TopoMapScreen> {
MapController? mapController;
StyleController? styleController;
bool showContours = true;
bool showHillshade = true;
static const _hillshadeLayer = HillshadeStyleLayer(
id: 'hillshade-layer',
sourceId: 'dem-source',
paint: {
'hillshade-exaggeration': 0.4,
'hillshade-illumination-direction': 315.0,
},
);
static const _contourLinesLayer = LineStyleLayer(
id: 'contour-lines',
sourceId: 'contour-source',
paint: {
'line-color': '#8B4513',
'line-width': 0.5,
'line-opacity': 0.4,
},
);
static const _contourLabelsLayer = SymbolStyleLayer(
id: 'contour-labels',
sourceId: 'contour-source',
layout: {
'text-field': '{ele}m',
'text-size': 9.0,
'symbol-placement': 'line',
},
paint: {'text-color': '#654321'},
filter: ['==', ['%', ['get', 'ele'], 500], 0],
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Topographic Map')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(7.6586, 45.9763), // Matterhorn (lng, lat)
initZoom: 12.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;
_addLayers(style);
},
),
// Layer toggles
Positioned(
top: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: showHillshade,
onChanged: (val) {
setState(() => showHillshade = val!);
_toggleLayer(_hillshadeLayer, val!);
},
),
Text('Hillshade', style: TextStyle(fontSize: 13)),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: showContours,
onChanged: (val) {
setState(() => showContours = val!);
_toggleLayer(_contourLinesLayer, val!);
_toggleLayer(_contourLabelsLayer, val!);
},
),
Text('Contours', style: TextStyle(fontSize: 13)),
],
),
],
),
),
),
),
],
),
);
}
Future<void> _addLayers(StyleController style) async {
// Hillshade
await style.addSource(
const RasterDemSource(
id: 'dem-source',
tiles: ['https://gateway.mapmetrics-atlas.net/terrain/{z}/{x}/{y}.png'],
tileSize: 256,
),
);
await style.addLayer(_hillshadeLayer);
// Contour lines
await style.addSource(
const VectorSource(
id: 'contour-source',
tiles: ['https://gateway.mapmetrics-atlas.net/contours/{z}/{x}/{y}.pbf'],
sourceLayer: 'contour',
),
);
await style.addLayer(_contourLinesLayer);
await style.addLayer(_contourLabelsLayer);
}
Future<void> _toggleLayer(StyleLayer layer, bool visible) async {
final style = styleController;
if (style == null) return;
if (visible) {
await style.addLayer(layer);
} else {
await style.removeLayer(layer.id);
}
}
}Contour Line Properties
| Property | Value | Description |
|---|---|---|
line-color | #8B4513 | Brown for topographic convention |
line-width | 0.5 – 0.75 | Set uniformly; per-layer minor/major widths need separate source layers (see note above) |
line-opacity | 0.4 – 0.6 | Semi-transparent to not obscure the base map |
filter (labels only) | ['==', ['%', ['get', 'ele'], 500], 0] | Show elevation labels only every 500m |
Next Steps
- Add a Hillshade Layer — Shaded relief effect
- 3D Terrain — Full 3D elevation
- Satellite Terrain — Satellite with terrain
Tip: Use brown (#8B4513) for contour lines — it's the cartographic standard. Keep contour lines thin and semi-transparent so they don't obscure the base map, and reserve filter-driven styling (elevation labels, index contours) for SymbolStyleLayer, which is the layer type that currently supports it.