**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
@markersLog
private BitMap 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 {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**:
Redraw Clear vectors
@vectorsLog
private BitMap 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
@geoJsonLog
private BitMap 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
@eventsLog
private BitMap 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
@advLog
private BitMap 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" };