A .NET MAUI navigation and routing plugin for MapLibre Native, built on top of MaplibreNativeMAUI.
| Package | Description |
|---|---|
MaplibreNative.Routing.Core |
Platform-agnostic routing models, engines, and navigation logic |
MaplibreNative.Routing |
MAUI handlers, NavigationPanel control, and UseMapLibreRouting builder extension |
MaplibreNative.Routing.Wpf |
WPF handlers, NavigationPanel UserControl, and AddMaplibreRouting DI extension |
- Valhalla routing — pedestrian, bicycle, motorcycle, and auto profiles with highway avoidance
- Offline MVT routing — pure C# road routing from OpenMapTiles vector tiles, no external server needed (
OfflineAuto,OfflineBicycle,OfflinePedestrian) - Track-graph routing — offline A* pathfinding over imported GeoJSON/GPX track layers
- Hybrid routing — automatically stitches road segments with trail A* segments (Valhalla or MVT road engine)
- Shared tile cache — optional
ITileCacheProviderlets the routing plugin share tiles with the host app's map renderer cache - Turn-by-turn navigation —
RouteProgressTrackersnaps GPS to route shape and advances steps - Maneuver announcements — fires events at 1000 m / 200 m / 30 m thresholds
- Highway warnings — detects motorway/trunk segments and surfaces a dismissible warning bar
NavigationPanelcontrol — dark overlay with maneuver icon, instruction, step distance, and ETA
- .NET 10 MAUI (net10.0-android36.0, net10.0-windows10.0.19041.0) or .NET 10 WPF (net10.0-windows10.0.19041.0)
- MaplibreNative.Maui.Handlers ≥ 4.1.0
- Valhalla HTTP API endpoint (for Valhalla-based profiles) or an OpenMapTiles-compatible MVT tile source (for offline profiles)
// MauiProgram.cs
builder.UseMapLibreRouting(routeDataSource: myLayerRepository);
// In your page/viewmodel
var session = serviceProvider.GetRequiredService<NavigationSession>();
await session.StartAsync(new RouteOptions(origin, destination, RouteProfile.HybridMotorcycle)
{
ValhallaUrl = "https://your-valhalla-host/route",
TrackFeatures = await routeDataSource.GetRoutableTrackFeaturesAsync()
});
session.ProgressUpdated += (_, progress) => navigationPanel.Apply(progress);// App startup (e.g. App.xaml.cs / host builder)
services.AddMaplibreRouting(routeDataSource: myLayerRepository);
// In your window/viewmodel
var session = serviceProvider.GetRequiredService<NavigationSession>();
session.ProgressUpdated += (_, progress) => navigationPanel.Apply(progress);
await session.StartAsync(new RouteOptions(origin, destination, RouteProfile.HybridMotorcycle)
{
ValhallaUrl = "https://your-valhalla-host/route",
});
// From your GPS source (Windows Location API, NMEA parser, VistumblerCS, etc.)
void OnLocationChanged(double lat, double lon) => session.UpdateLocation(lat, lon);The offline profiles (OfflineAuto, OfflineBicycle, OfflinePedestrian) build a road graph from OpenMapTiles-compatible MVT tiles — no Valhalla server required. Set MvtTileJsonUrl to your TileJSON endpoint:
await session.StartAsync(new RouteOptions
{
Origin = (47.6062, -122.3321),
Destination = (47.6205, -122.3493),
Profile = RouteProfile.OfflineBicycle,
MvtTileJsonUrl = "https://your-tileserver/data/openmaptiles.json",
});Hybrid offline profiles (HybridOfflineMotorcycle, HybridOfflineBicycle) route primarily on GeoJSON trail features and use MVT roads to fill gaps between trail segments.
By default the routing plugin fetches MVT tiles over HTTP and caches them in memory (lost on restart). If your app already caches tiles — for example via MaplibreNativeMAUI's offline manager — you can share that cache with the routing plugin by implementing ITileCacheProvider:
public interface ITileCacheProvider
{
Task<byte[]?> GetTileAsync(TileCoord coord, CancellationToken ct = default);
Task SetTileAsync(TileCoord coord, byte[] data, CancellationToken ct = default);
Task RequestAreaCacheAsync(
double minLat, double minLon, double maxLat, double maxLon,
int zoom, CancellationToken ct = default);
}GetTileAsync— return decompressed PBF bytes from your cache, ornullon missSetTileAsync— store a tile the routing plugin downloaded (so the map renderer benefits too)RequestAreaCacheAsync— the routing plugin asks your app to pre-cache a corridor; map this to your offline download manager (e.g.MbglOfflineManager.CreateRegionAsync)
public class MapLibreTileCacheProvider : ITileCacheProvider
{
private readonly string _dbPath;
public MapLibreTileCacheProvider(string cacheDbPath)
{
_dbPath = cacheDbPath;
}
public async Task<byte[]?> GetTileAsync(TileCoord coord, CancellationToken ct)
{
await using var conn = new SqliteConnection($"Data Source={_dbPath}");
await conn.OpenAsync(ct);
var cmd = conn.CreateCommand();
cmd.CommandText = @"SELECT data, compressed FROM tiles
WHERE url_template = $url AND z = $z AND x = $x AND y = $y AND pixel_ratio = 1";
cmd.Parameters.AddWithValue("$url", _urlTemplate);
cmd.Parameters.AddWithValue("$z", coord.Z);
cmd.Parameters.AddWithValue("$x", coord.X);
cmd.Parameters.AddWithValue("$y", coord.Y);
await using var reader = await cmd.ExecuteReaderAsync(ct);
if (!await reader.ReadAsync(ct)) return null;
var data = (byte[])reader["data"];
var compressed = (long)reader["compressed"] == 1;
if (compressed)
{
using var input = new MemoryStream(data);
using var deflate = new DeflateStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
await deflate.CopyToAsync(output, ct);
return output.ToArray();
}
return data;
}
public async Task SetTileAsync(TileCoord coord, byte[] data, CancellationToken ct)
{
// Write to the same SQLite cache so the map renderer can use it
await using var conn = new SqliteConnection($"Data Source={_dbPath}");
await conn.OpenAsync(ct);
var cmd = conn.CreateCommand();
cmd.CommandText = @"INSERT OR IGNORE INTO tiles
(url_template, pixel_ratio, z, x, y, data, compressed)
VALUES ($url, 1, $z, $x, $y, $data, 0)";
cmd.Parameters.AddWithValue("$url", _urlTemplate);
cmd.Parameters.AddWithValue("$z", coord.Z);
cmd.Parameters.AddWithValue("$x", coord.X);
cmd.Parameters.AddWithValue("$y", coord.Y);
cmd.Parameters.AddWithValue("$data", data);
await cmd.ExecuteNonQueryAsync(ct);
}
public Task RequestAreaCacheAsync(
double minLat, double minLon, double maxLat, double maxLon,
int zoom, CancellationToken ct)
{
// Delegate to MapLibre's offline manager to download the region
return MbglOfflineManager.CreateRegionAsync(
styleUrl: "https://your-tileserver/styles/your-style/style.json",
latSw: minLat, lonSw: minLon,
latNe: maxLat, lonNe: maxLon,
minZoom: zoom, maxZoom: zoom);
}
}Pass the provider on RouteOptions:
var cacheProvider = new MapLibreTileCacheProvider(MbglCache.DefaultPath);
await session.StartAsync(new RouteOptions
{
Origin = (47.6062, -122.3321),
Destination = (47.6205, -122.3493),
Profile = RouteProfile.OfflineAuto,
MvtTileJsonUrl = "https://your-tileserver/data/openmaptiles.json",
TileCacheProvider = cacheProvider,
});When TileCacheProvider is null (the default), the plugin uses HTTP + in-memory caching only — no behavioral change from before.
Routing model and navigation patterns adapted from maplibre-navigation-android (MIT License). See NOTICE.md.
MIT — see LICENSE.