Working with the map
Once you have a map control on screen (see the Quick Start), you drive it through methods on the control instance. The WinForms, WPF, .NET MAUI and Blazor controls expose the same methods with the same signatures. They differ in how you set properties, subscribe to events and switch providers; those parts of this page have a tab per framework.
The examples assume a map variable that refers to your control, and these usings:
using TMS.Maps.Core.Models; // Coordinate, Providers
using TMS.Maps.Core.Models.UI; // MapMarker, MapPolyline, MapCircle, event args, ...
Important
Call these methods only after the map is ready: in the MapReady handler (OnMapReady in
Blazor), or after await map.WaitForReadyAsync(). Calls made earlier do nothing: they are not
queued and they do not throw. map.IsMapReady tells you whether the map is ready.
Markers
// Simple marker
await map.AddMarkerAsync(51.0543, 3.7174, "Ghent");
// Marker with an id you choose, so you can update or remove it later
await map.AddMarkerWithIdAsync(51.2194, 4.4025, "Antwerp", markerId: "antwerp");
// Without markerId, a GUID is generated and returned
string id = await map.AddMarkerWithIdAsync(50.8503, 4.3517, "Brussels");
// Marker from a model, with styling and an info window
var marker = new MapMarker("grote-markt", new Coordinate(51.0547, 3.7216), "Grote Markt")
{
Description = "Market square",
IsDraggable = true,
Options = new MarkerOptions { Color = "#6E35B5", InfoWindowContent = "<b>Grote Markt</b>" },
};
await map.AddMarkerAsync(marker);
await map.ShowInfoWindowAsync("grote-markt");
// Markers are immutable records: change one with 'with', keeping the same Id
await map.UpdateMarkerAsync(marker with { Title = "Grote Markt, Ghent" });
// Batch add
await map.AddMarkersAsync(new[]
{
new MapMarker(new Coordinate(51.2093, 3.2247), "Bruges"),
new MapMarker(new Coordinate(50.8798, 4.7005), "Leuven"),
});
await map.RemoveMarkerAsync(id);
await map.ClearMarkersAsync();
AddMarkerAsync returns no id. To remove or update a marker later, either pass markerId: to
AddMarkerWithIdAsync, or build a MapMarker and read its Id.
Shapes
The shortcut overloads draw a shape with the default styling:
// Polyline from a list of coordinates
await map.AddPolylineAsync(new[]
{
new Coordinate(51.0543, 3.7174), // Ghent
new Coordinate(51.0890, 3.9640),
new Coordinate(51.2194, 4.4025), // Antwerp
});
// Polygon (outer ring)
await map.AddPolygonAsync(new[]
{
new Coordinate(51.050, 3.710),
new Coordinate(51.060, 3.710),
new Coordinate(51.060, 3.730),
new Coordinate(51.050, 3.730),
});
// Circle: centre and radius in metres
await map.AddCircleAsync(51.0543, 3.7174, radiusMetres: 500);
// Rectangle: north, east, south and west bounds
await map.AddRectangleAsync(north: 51.06, east: 3.73, south: 51.05, west: 3.71);
To set the stroke, fill, opacity or id, build a MapPolyline, MapPolygon, MapCircle or MapRectangle and pass it to the model overload:
var route = new MapPolyline("route", new List<Coordinate>
{
new(51.0543, 3.7174), new(51.2194, 4.4025)
})
{ StrokeColor = "#6E35B5", StrokeWeight = 4 };
await map.AddPolylineAsync(route);
await map.AddCircleAsync(new MapCircle(new Coordinate(51.0543, 3.7174), 500) { FillOpacity = 0.16 });
await map.AddRectangleAsync(new MapRectangle(MapBounds.Create(north: 51.06, east: 3.73, south: 51.05, west: 3.71)));
await map.RemovePolylineAsync("route");
Shapes have no Update…Async method: remove the shape by id, then add the changed record.
Clustering and heatmaps
// Cluster nearby markers (defaults, or pass MapClusterOptions)
await map.EnableClusteringAsync();
await map.EnableClusteringAsync(new MapClusterOptions { Radius = 80, MaxZoom = 15 });
await map.DisableClusteringAsync();
// Heatmap layer
var heatmap = new MapHeatmap("visitors", new[]
{
new HeatmapPoint(51.0543, 3.7174), // Weight defaults to 1.0
new HeatmapPoint(51.0550, 3.7200, Weight: 0.5),
});
await map.AddHeatmapAsync(heatmap);
await map.SetHeatmapVisibilityAsync("visitors", false);
Camera and navigation
await map.PanToAsync(new Coordinate(51.2194, 4.4025)); // Antwerp
await map.FitToMarkersAsync(); // zoom to show all markers
// Fit the view to an area or to a set of coordinates
await map.ZoomToBoundsAsync(MapBounds.Create(north: 51.30, east: 4.50, south: 51.00, west: 3.65));
await map.ZoomToBoundsAsync(new[] { new Coordinate(51.0543, 3.7174), new Coordinate(51.2194, 4.4025) });
await map.SetTiltAsync(45); // degrees
await map.SetBearingAsync(90); // degrees clockwise from north
Read the current view, or convert between coordinates and pixels in the map container:
Coordinate? center = await map.GetCenterAsync();
int zoom = await map.GetZoomAsync();
MapBounds? visible = await map.GetBoundsAsync();
(double X, double Y)? pixel = await map.LatLonToXYAsync(new Coordinate(51.0543, 3.7174));
Coordinate? atPixel = await map.XYToLatLonAsync(200, 150);
On WinForms, WPF and .NET MAUI, setting CenterLatitude, CenterLongitude or ZoomLevel also
moves the live map. These properties are input only: they do not follow the map when the user
pans or zooms. Use GetCenterAsync and GetZoomAsync, or the MapCenterChanged,
MapZoomChanged and MapMoveEnded events, to track the view. In Blazor the parameters are read
once, at first render; use the methods above afterwards.
Popups, labels and HTML overlays
// A popup at a coordinate; keep the returned id to close it
string popupId = await map.ShowPopupAsync(new Coordinate(51.0547, 3.7216), "Grote Markt");
await map.ClosePopupAsync(popupId);
// A text label (MapLabelType.Label or Balloon)
await map.AddLabelAsync(new MapLabel("ghent", new Coordinate(51.0543, 3.7174), "Ghent")
{
FontColor = "#FFFFFF",
BackgroundColor = "#6E35B5",
});
await map.RemoveLabelAsync("ghent");
// HTML anchored to a corner (or the centre) of the map
var legend = new MapElementContainer
{
Html = "<b>Legend</b><br>Purple: route",
Position = MapPosition.TopRight,
};
await map.AddElementContainerAsync(legend);
await map.RemoveElementContainerAsync(legend.Id);
ShowPopupAsync pans the map to the popup unless you pass panMap: false. CloseAllPopupsAsync,
ClearLabelsAsync and ClearElementContainersAsync remove all of them at once.
GeoJSON and GPX
Loading a file adds its features to the map and returns what was parsed. Exporting writes the markers and shapes you added through the API.
using TMS.Maps.Core.IO; // GeoJsonResult, GpxResult
// GeoJSON: points become markers, lines polylines, polygons polygons
GeoJsonResult geo = await map.LoadGeoJsonAsync("districts.geojson");
foreach (var error in geo.Errors)
Debug.WriteLine(error);
// GPX: waypoints become markers, tracks and routes polylines
GpxResult gpx = await map.LoadGpxFromStringAsync(gpxXml);
string geoJson = await map.ExportToGeoJsonAsync(); // markers, polylines and polygons
await map.ExportToGpxFileAsync("route.gpx"); // markers and polylines
Each format has a file and a string overload for loading (LoadGeoJsonAsync,
LoadGeoJsonFromStringAsync, LoadGpxAsync, LoadGpxFromStringAsync) and for exporting
(ExportToGeoJsonAsync, ExportToGeoJsonFileAsync, ExportToGpxAsync, ExportToGpxFileAsync).
Circles and rectangles are not exported. In Blazor, prefer the string overloads: file paths refer
to the server's file system (Blazor Server) or to the browser's in-memory one (WebAssembly).
Measuring
The static Measure* methods work on coordinates and need no map:
var ghent = new Coordinate(51.0543, 3.7174);
var antwerp = new Coordinate(51.2194, 4.4025);
double metres = MapControl.MeasureDistance(ghent, antwerp);
double bearing = MapControl.MeasureBearing(ghent, antwerp); // degrees
double length = MapControl.MeasurePathLength(route); // metres
MeasureArea returns square metres for a ring of coordinates or a MapPolygon.
Tile, traffic and transit layers
// Overlay a tile layer, then remove it again
MapTileLayer layer = await map.AddTileLayerAsync(
"https://tile.opentopomap.org/{z}/{x}/{y}.png", opacity: 0.6, attribution: "OpenTopoMap");
await map.RemoveTileLayerAsync(layer);
// Replace the base map tiles (Leaflet and OpenLayers)
await map.SetTileServerAsync("https://tiles.example.com/{z}/{x}/{y}.png");
await map.EnableTrafficLayerAsync();
await map.EnableTransitLayerAsync();
Not every provider supports every layer; see Provider support.
Drawing tools
Turn on drawing mode to let the user draw a shape. The control raises DrawingCompleted with the
result; add it to the map yourself if you want to keep it as an overlay.
map.DrawingCompleted += async (_, e) =>
{
if (e.Mode == DrawingMode.Polygon && e.Polygon is { } polygon)
await map.AddPolygonAsync(polygon);
};
await map.EnableDrawingModeAsync(DrawingMode.Polygon, new DrawingOptions { StrokeColor = "#6E35B5" });
// ...
await map.DisableDrawingModeAsync();
In Blazor, handle OnDrawingCompleted instead (see the next section).
Handling events
The WinForms, WPF and .NET MAUI controls expose standard .NET events. The Blazor component exposes
the same events as EventCallback parameters named On<Event>, for example OnMarkerClicked.
map.MapClicked += (_, e) =>
Debug.WriteLine($"Clicked at {e.Coordinate.Latitude}, {e.Coordinate.Longitude}");
map.MarkerClicked += (_, e) =>
Debug.WriteLine($"Marker clicked: {e.Marker.Title}");
map.PolygonClicked += (_, e) =>
Debug.WriteLine($"Polygon clicked: {e.Polygon.Id}");
Available events:
- Map:
MapReady,MapClicked,MapRightClicked,MapDoubleClicked,MapMouseMoved,MapMouseDown,MapMouseUp,MapMouseEntered,MapMouseLeft. - View:
MapCenterChanged(newCoordinate),MapZoomChanged(newZoomLevel),MapMoveStarted,MapMoveEnded. - Errors:
MapError(MessageandSource). In this release it reports license failures only. - Markers:
MarkerClicked,MarkerRightClicked,MarkerDoubleClicked,MarkerMouseEnter,MarkerMouseLeave,MarkerMouseDown,MarkerMouseUp,MarkerDragEnd. - Polylines and polygons:
Clicked,RightClicked,DoubleClicked,MouseEnter,MouseLeave,DragEndandVertexChanged, prefixed withPolylineorPolygon. - Circles and rectangles: the same set without
VertexChanged, prefixed withCircleorRectangle. - Drawing:
DrawingCompleted.
Marker and shape events fire only for overlays you added through the API. The event arguments
carry your model, so e.Marker.Id is the id you gave it. The sender of an event is not the
MapControl itself: use your map variable inside the handler instead of casting sender.
Switching providers at runtime
The map control supports six providers: Google Maps, Azure Maps, HERE, Apple MapKit, Leaflet and OpenLayers. Leaflet and OpenLayers need no API key. The other members of Providers are for the services only.
// Set the property, or await the method to catch errors
map.Provider = Providers.Leaflet;
map.ApiKey = "YOUR_AZURE_MAPS_KEY";
await map.SwitchProviderAsync(Providers.Azure);
A switch reloads the map. Markers, polylines, polygons, rectangles and circles are added again;
popups, labels, HTML overlays, heatmaps, clustering, tile layers, traffic and transit layers,
drawing mode, tilt and bearing are not. MapReady fires again when the new provider is ready. Switching to a provider the control
does not support throws NotSupportedException; check with map.IsProviderSupported(provider)
or map.GetSupportedProviders().
Provider support
Features a provider does not support never throw: the call completes and nothing happens. Use
map.CurrentProviderSupports(ProviderCapabilities...) (namespace TMS.Maps.Core.Attributes) to
check at runtime.
| Feature | Google Maps | Azure Maps | HERE | Apple MapKit | Leaflet | OpenLayers |
|---|---|---|---|---|---|---|
| Markers, shapes, info windows | Yes | Yes | Yes | Yes | Yes | Yes |
| Clustering, heatmaps | Yes | Yes | Yes | Yes | Yes | Yes |
AddTileLayerAsync |
Yes | Yes | Yes | Yes | ||
SetTileServerAsync |
Yes | Yes | ||||
| Traffic layer | Yes | Yes | Yes | |||
| Transit layer | Yes | |||||
| Drawing tools | Yes | Yes | Yes | Yes | ||
| Tilt | Yes | Yes | Yes | |||
| Bearing | Yes | Yes | Yes | Yes | Yes | |
| Needs an API key | Yes | Yes | Yes | Yes (MapKit JS token) | No | No |
See the API reference for every member and its overloads.