Skip to main content
Table of Contents

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 (new Coordinate), MapZoomChanged (new ZoomLevel), MapMoveStarted, MapMoveEnded.
  • Errors: MapError (Message and Source). 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, DragEnd and VertexChanged, prefixed with Polyline or Polygon.
  • Circles and rectangles: the same set without VertexChanged, prefixed with Circle or Rectangle.
  • 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.