**Map** BitMap is a generic, provider-pluggable interactive map component. It supports Leaflet, MapLibre GL, Mapbox GL, OpenLayers, ArcGIS, Azure Maps, and CesiumJS.
**Basic**:
**Markers**:
Add random marker
Clear all
Open London popup
Fit to markers
markersMapRef = default!;
private readonly BitLeafletMapProvider markersProvider = new() { Center = new(48.8566, 2.3522), Zoom = 5 };
private string markersLog = "Seed markers are added on OnReady. Try the buttons.";
private int _markerCounter;
private async Task OnMarkersReady()
{
await markersMapRef.AddMarker(new BitMapMarker
{
Id = "paris", Position = new(48.8566, 2.3522),
Title = "Paris", PopupHtml = (MarkupString)"Paris
Click to open popup.", }); await markersMapRef.AddMarker(new BitMapMarker { Id = "london", Position = new(51.5074, -0.1278), Title = "London", PopupHtml = (MarkupString)"London
Draggable marker.", Draggable = true, TooltipHtml = (MarkupString)"Drag me!", }); await markersMapRef.FitBoundsToMarkers(); } private async Task AddRandomMarker() { _markerCounter++; var id = $"m{_markerCounter}"; // Scatter inside the current viewport so new markers are always visible. var view = await markersMapRef.GetView(); var sw = view.Bounds.SouthWest; var ne = view.Bounds.NorthEast; var latSpan = ne.Latitude - sw.Latitude; var lngSpan = ne.Longitude - sw.Longitude; if (lngSpan < 0) lngSpan += 360; // antimeridian const double inset = 0.1; var lat = sw.Latitude + (inset + Random.Shared.NextDouble() * (1 - 2 * inset)) * latSpan; var lng = sw.Longitude + (inset + Random.Shared.NextDouble() * (1 - 2 * inset)) * lngSpan; lat = Math.Clamp(lat, -85, 85); if (lng > 180) lng -= 360; else if (lng < -180) lng += 360; // Roll a coin so some markers come in draggable. var draggable = Random.Shared.Next(2) == 0; await markersMapRef.AddMarker(new BitMapMarker { Id = id, Position = new(lat, lng), Title = $"Marker {id}{(draggable ? " (draggable)" : "")}", PopupHtml = (MarkupString)($"Marker
{lat:F4}, {lng:F4}" + (draggable ? "
Drag me!" : "")), Draggable = draggable, TooltipHtml = draggable ? (MarkupString?)(MarkupString)"Drag me!" : null, }); markersLog = $"Added {id}{(draggable ? " (draggable)" : "")} at {lat:F4}, {lng:F4}"; } private async Task ClearMarkers() { await markersMapRef.ClearMarkers(); markersLog = "All markers cleared."; } private async Task OpenLondonPopup() { await markersMapRef.OpenMarkerPopup("london"); markersLog = "Opened London popup."; } private async Task FitToMarkers() { await markersMapRef.FitBoundsToMarkers(); markersLog = "Fitted view to all markers."; } private Task OnMarkerClick(string id) { markersLog = $"Marker click: {id}"; return Task.CompletedTask; } private Task OnMarkerDragEnd(BitMapMarkerDragEndArgs e) { markersLog = $"Drag end {e.Id} → {e.Position.Latitude:F5}, {e.Position.Longitude:F5}"; return Task.CompletedTask; } **Vectors**:
Redraw
Clear vectors
vectorsMapRef = default!;
private readonly BitLeafletMapProvider vectorsProvider = new() { Center = new(37.7749, -122.4194), Zoom = 12 };
private string vectorsLog = "Click Redraw to draw shapes, then click a shape.";
private async Task OnVectorsReady() => await DrawVectors();
private async Task DrawVectors()
{
await vectorsMapRef.AddPolyline("route",
[
new(37.80, -122.42), new(37.79, -122.41),
new(37.78, -122.40), new(37.77, -122.395),
], new BitMapVectorPathStyle { Color = "#f85149", Weight = 5, Opacity = 0.9 });
await vectorsMapRef.AddPolygon("park",
[
new(37.769, -122.486), new(37.771, -122.475),
new(37.765, -122.472), new(37.762, -122.482),
], new BitMapVectorPathStyle { Color = "#3fb950", FillOpacity = 0.35, Weight = 2 });
await vectorsMapRef.AddCircle("radius", new(37.7849, -122.4094), 900,
new BitMapVectorPathStyle { Color = "#58a6ff", FillOpacity = 0.15, Weight = 2 });
await vectorsMapRef.AddRectangle("box",
new BitMapLatLngBounds(new(37.748, -122.44), new(37.756, -122.42)),
new BitMapVectorPathStyle { Color = "#d29922", FillOpacity = 0.12, Weight = 2, DashArray = "6,4" });
await vectorsMapRef.FitBounds(
new BitMapLatLngBounds(new(37.755, -122.49), new(37.805, -122.38)));
}
private async Task RedrawVectors()
{
await vectorsMapRef.ClearVectorLayers();
await DrawVectors();
vectorsLog = "Vectors redrawn.";
}
private async Task ClearVectors()
{
await vectorsMapRef.ClearVectorLayers();
vectorsLog = "All vector layers cleared.";
}
private Task OnVectorClick(BitMapVectorClickArgs e)
{
// e.Kind = "polyline" | "polygon" | "circle" | "rectangle"
// e.LayerId = the id you passed to AddPolyline/AddPolygon/…
vectorsLog = $"{e.Kind} \"{e.LayerId}\" @ {e.Position.Latitude:F5}, {e.Position.Longitude:F5}";
return Task.CompletedTask;
}
**GeoJSON**:
Load GeoJSON
Remove layer
geoJsonMapRef = default!;
private readonly BitLeafletMapProvider geoJsonProvider = new() { Center = new(40.7128, -74.0060), Zoom = 11 };
private string geoJsonLog = "Click 'Load GeoJSON', then click a feature.";
private async Task LoadGeoJson()
{
await geoJsonMapRef.RemoveLayer("demo");
await geoJsonMapRef.AddGeoJson("demo", SampleGeoJson,
new BitMapVectorPathStyle { Color = "#a371f7", Weight = 3, FillOpacity = 0.25 });
await geoJsonMapRef.FitBounds(new BitMapLatLngBounds(new(40.71, -74.03), new(40.83, -73.96)));
geoJsonLog = "GeoJSON loaded. Click a feature.";
}
private async Task RemoveGeoJson()
{
await geoJsonMapRef.RemoveLayer("demo");
geoJsonLog = "Layer \"demo\" removed.";
}
private Task OnGeoJsonFeatureClick(BitMapGeoJsonFeatureClickArgs e)
{
// e.LayerId = "demo"
// e.Properties = JsonElement of feature.properties
var name = "(no name)";
if (e.Properties.ValueKind == System.Text.Json.JsonValueKind.Object
&& e.Properties.TryGetProperty("name", out var n))
{
name = n.ValueKind == System.Text.Json.JsonValueKind.String ? n.GetString() : n.ToString();
}
geoJsonLog = $"Layer {e.LayerId} - properties.name = {name}";
return Task.CompletedTask;
}
// Minimal GeoJSON FeatureCollection used by LoadGeoJson above.
private const string SampleGeoJson = """
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "name": "Central Park" },
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.981, 40.768], [-73.958, 40.768],
[-73.958, 40.800], [-73.981, 40.800],
[-73.981, 40.768]
]]
}
},
{
"type": "Feature",
"properties": { "name": "Brooklyn Bridge" },
"geometry": {
"type": "LineString",
"coordinates": [[-73.9969, 40.7061], [-73.9875, 40.7026]]
}
}
]
}
""";
**Custom tiles**:
OSM default
Carto Voyager
OpenTopoMap
@* @key forces a new map instance when the provider changes *@
private string tileProvider = "osm";
private BitLeafletMapProvider currentTileLeafletProvider = new() { Center = new(51.505, -0.09), Zoom = 13 };
private void SetTileProvider(string p)
{
tileProvider = p;
currentTileLeafletProvider = p switch
{
"carto" => new BitLeafletMapProvider
{
Center = new(20, 0), Zoom = 2,
TileUrl = "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png",
TileAttribution = "© OpenStreetMap contributors © CARTO",
},
"topo" => new BitLeafletMapProvider
{
Center = new(46.5, 11.3), Zoom = 10,
TileUrl = "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
TileAttribution = "Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap",
TileMaxZoom = 17,
},
_ => new BitLeafletMapProvider { Center = new(51.505, -0.09), Zoom = 13 },
};
}
**Events**:
Fly to Tokyo
Log viewport
eventsMapRef = default!;
private readonly BitLeafletMapProvider eventsProvider = new() { Center = new(35.6762, 139.6503), Zoom = 11 };
private string eventsLog = "Pan/zoom or click the map.";
private Task OnMapClick(BitMapLatLng p)
{
eventsLog = $"Click → {p.Latitude:F5}, {p.Longitude:F5}";
return Task.CompletedTask;
}
private Task OnMapDoubleClick(BitMapLatLng p)
{
eventsLog = $"Double-click → {p.Latitude:F5}, {p.Longitude:F5}";
return Task.CompletedTask;
}
private Task OnViewChanged(BitMapViewState v)
{
eventsLog = $"View: zoom {v.Zoom:F1}, center {v.Center.Latitude:F4},{v.Center.Longitude:F4}";
return Task.CompletedTask;
}
private async Task FlyToTokyo()
{
await eventsMapRef.FlyTo(new(35.6762, 139.6503), 12);
eventsLog = "Flying to Tokyo…";
}
private async Task ReadView()
{
var v = await eventsMapRef.GetView();
eventsLog = $"GetView → zoom {v.Zoom:F2}, center {v.Center.Latitude:F4},{v.Center.Longitude:F4}, " +
$"NE {v.Bounds.NorthEast.Latitude:F4},{v.Bounds.NorthEast.Longitude:F4}";
}
**Advanced**:
@* Bind a stable field, not a method call: a method call reallocates the provider on every render. *@
Add tooltip markers + fit
@(advOverlayOn ? "Remove overlay" : "Add tile overlay")
Log viewport
advMapRef = default!;
private bool advScrollWheel = true;
private bool advDragging = true;
private bool advScaleBar = true;
private bool advMaxBounds;
private bool advOverlayOn;
private string advLog = "Toggle options or use the buttons.";
private BitLeafletMapProvider advProvider = new()
{
Center = new(51.5074, -0.1278), Zoom = 11,
ScrollWheelZoom = true,
Dragging = true,
ShowScaleControl = true,
MaxBounds = null,
};
// Mutate the stable field only when an option actually changes - not on every render.
private BitLeafletMapProvider BuildAdvancedProvider()
{
advProvider = new BitLeafletMapProvider
{
Center = new(51.5074, -0.1278), Zoom = 11,
ScrollWheelZoom = advScrollWheel,
Dragging = advDragging,
ShowScaleControl = advScaleBar,
MaxBounds = advMaxBounds
? new BitMapLatLngBounds(new(51.25, -0.55), new(51.75, 0.35))
: null,
};
// Rebuilding the provider replaces the underlying Leaflet map instance,
// so any previously-added overlays no longer exist on the new map.
// Reset the toggle state so the UI label/branch reflects that.
advOverlayOn = false;
return advProvider;
}
private async Task OnAdvancedReady() => await AddTooltipMarkers();
private async Task AddTooltipMarkers()
{
await advMapRef.ClearMarkers();
await advMapRef.AddMarker(new BitMapMarker { Id = "a", Position = new(51.52, -0.10), TooltipHtml = (MarkupString)"West End", PopupHtml = (MarkupString)"Popup A", ZIndexOffset = 10 });
await advMapRef.AddMarker(new BitMapMarker { Id = "b", Position = new(51.50, -0.08), TooltipHtml = (MarkupString)"City", PopupHtml = (MarkupString)"Popup B" });
await advMapRef.AddMarker(new BitMapMarker { Id = "c", Position = new(51.48, -0.06), TooltipHtml = (MarkupString)"South Bank", PopupHtml = (MarkupString)"Popup C" });
await advMapRef.FitBoundsToMarkers(56);
advLog = "Three tooltip markers added; view fitted.";
}
private async Task ToggleTileOverlay()
{
if (advOverlayOn)
{
await advMapRef.RemoveTileOverlay("labels");
advOverlayOn = false;
advLog = "Tile overlay removed.";
}
else
{
await advMapRef.AddTileOverlay(new BitMapTileOverlay
{
Id = "labels",
UrlTemplate = "https://tiles.stadiamaps.com/tiles/stamen_toner_labels/{z}/{x}/{y}{r}.png",
Attribution = "Map tiles by Stamen Design, hosted by Stadia Maps. Data by OpenStreetMap.",
Opacity = 0.85,
ZIndex = 400,
MaxZoom = 20,
});
advOverlayOn = true;
advLog = "Tile overlay added (may fail if the tile host blocks your origin).";
}
}
private async Task ReadAdvancedView()
{
var v = await advMapRef.GetView();
advLog = $"GetView → zoom {v.Zoom:F2}, center {v.Center.Latitude:F4},{v.Center.Longitude:F4}, " +
$"NE {v.Bounds.NorthEast.Latitude:F4},{v.Bounds.NorthEast.Longitude:F4}";
}
private Task OnAdvancedDoubleClick(BitMapLatLng p)
{
advLog = $"Double-click at {p.Latitude:F4}, {p.Longitude:F4}";
return Task.CompletedTask;
}
**MapLibre GL**:
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitMapLibreMapProvider maplibreProvider = new() { Center = new(48.8566, 2.3522), Zoom = 5 };
**OpenLayers**:
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitOpenLayersMapProvider olProvider = new() { Center = new(35.6762, 139.6503), Zoom = 4 };
**Mapbox GL (token required)**:
// Get your token from https://account.mapbox.com/access-tokens/
// and pass it via the AccessToken property on BitMapboxMapProvider.
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitMapboxMapProvider mapboxProvider = new()
{
AccessToken = "YOUR_MAPBOX_TOKEN",
Center = new(40, 0),
Zoom = 2,
};
**ArcGIS Maps SDK**:
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitArcGisMapProvider arcGisProvider = new() { Center = new(40, 0), Zoom = 2, BasemapId = "osm" };
**Azure Maps (subscription key required)**:
// Get your key from Azure Portal > Maps account > Authentication > Shared Key
// and pass it via the SubscriptionKey property on BitAzureMapsMapProvider.
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitAzureMapsMapProvider azureMapsProvider = new()
{
SubscriptionKey = "YOUR_AZURE_MAPS_KEY",
Center = new(40, 0),
Zoom = 2,
};
**CesiumJS 3D globe**:
// Bind a stable field so the provider isn't reallocated on every render.
private readonly BitCesiumMapProvider cesiumProvider = new() { Center = new(20, 0), Zoom = 2, SceneMode = "scene3d" };
@markersLogprivate BitMap
Click to open popup.", }); await markersMapRef.AddMarker(new BitMapMarker { Id = "london", Position = new(51.5074, -0.1278), Title = "London", PopupHtml = (MarkupString)"London
Draggable marker.", Draggable = true, TooltipHtml = (MarkupString)"Drag me!", }); await markersMapRef.FitBoundsToMarkers(); } private async Task AddRandomMarker() { _markerCounter++; var id = $"m{_markerCounter}"; // Scatter inside the current viewport so new markers are always visible. var view = await markersMapRef.GetView(); var sw = view.Bounds.SouthWest; var ne = view.Bounds.NorthEast; var latSpan = ne.Latitude - sw.Latitude; var lngSpan = ne.Longitude - sw.Longitude; if (lngSpan < 0) lngSpan += 360; // antimeridian const double inset = 0.1; var lat = sw.Latitude + (inset + Random.Shared.NextDouble() * (1 - 2 * inset)) * latSpan; var lng = sw.Longitude + (inset + Random.Shared.NextDouble() * (1 - 2 * inset)) * lngSpan; lat = Math.Clamp(lat, -85, 85); if (lng > 180) lng -= 360; else if (lng < -180) lng += 360; // Roll a coin so some markers come in draggable. var draggable = Random.Shared.Next(2) == 0; await markersMapRef.AddMarker(new BitMapMarker { Id = id, Position = new(lat, lng), Title = $"Marker {id}{(draggable ? " (draggable)" : "")}", PopupHtml = (MarkupString)($"Marker
{id}{lat:F4}, {lng:F4}" + (draggable ? "
Drag me!" : "")), Draggable = draggable, TooltipHtml = draggable ? (MarkupString?)(MarkupString)"Drag me!" : null, }); markersLog = $"Added {id}{(draggable ? " (draggable)" : "")} at {lat:F4}, {lng:F4}"; } private async Task ClearMarkers() { await markersMapRef.ClearMarkers(); markersLog = "All markers cleared."; } private async Task OpenLondonPopup() { await markersMapRef.OpenMarkerPopup("london"); markersLog = "Opened London popup."; } private async Task FitToMarkers() { await markersMapRef.FitBoundsToMarkers(); markersLog = "Fitted view to all markers."; } private Task OnMarkerClick(string id) { markersLog = $"Marker click: {id}"; return Task.CompletedTask; } private Task OnMarkerDragEnd(BitMapMarkerDragEndArgs e) { markersLog = $"Drag end {e.Id} → {e.Position.Latitude:F5}, {e.Position.Longitude:F5}"; return Task.CompletedTask; } **Vectors**:
@vectorsLogprivate BitMap
@geoJsonLogprivate BitMap
@eventsLogprivate BitMap
@advLogprivate BitMap