Table of Contents

TMS FNC Leaflet — Guides

Map options and attribution

Options is where the map is configured before a single overlay is added: the view the map opens on, which interactions the user gets, and — for Leaflet specifically — which raster source draws the world underneath everything else. Reach for it when a map should start somewhere other than the middle of the ocean, when wheel zoom or double-click zoom fights the surrounding form, or when the default OpenStreetMap tiles are not the right basemap for the data.

procedure TForm1.ConfigureLeafletOptions;
begin
  TMSFNCLeaflet1.BeginUpdate;
  try
    { Start view: used until the map is moved or a Coordinate/Zoom is applied. }
    TMSFNCLeaflet1.Options.DefaultLatitude := 51.5074;
    TMSFNCLeaflet1.Options.DefaultLongitude := -0.1278;
    TMSFNCLeaflet1.Options.DefaultZoomLevel := 12;

    { Leaflet-specific: replace the built-in OpenStreetMap raster source. }
    TMSFNCLeaflet1.Options.ShowBaseLayer := True;
    TMSFNCLeaflet1.Options.TileServer :=
      'https://tile.opentopomap.org/{z}/{x}/{y}.png';

    { Most public tile providers require visible attribution. }
    TMSFNCLeaflet1.Options.ShowAttribution := True;
    TMSFNCLeaflet1.Options.AttributionPrefix := 'Leaflet';
    TMSFNCLeaflet1.Options.AttributionText := 'Map data: OpenTopoMap (CC-BY-SA)';

    { Interaction switches shared by every map engine. }
    TMSFNCLeaflet1.Options.ShowZoomControl := True;
    TMSFNCLeaflet1.Options.ZoomOnWheelScroll := True;
    TMSFNCLeaflet1.Options.ZoomOnDblClick := False;
    TMSFNCLeaflet1.Options.Panning := True;
  finally
    TMSFNCLeaflet1.EndUpdate;
  end;
end;

TileServer takes an XYZ URL template and replaces the built-in OpenStreetMap source for the base layer; leaving it empty keeps that default. ShowAttribution, AttributionPrefix, and AttributionText render the credit box in the map corner — most public tile providers require it by licence, so set the text to match whatever your chosen TileServer demands rather than leaving the Leaflet default.

Two behaviours are worth knowing before tuning these:

  • TileServer is ignored while ShowBaseLayer is False. The two properties are read together, so a custom URL only takes effect with the base layer enabled. Set ShowBaseLayer := False when your own tile layers (below) are meant to be the only imagery, and apply it as part of the same option pass rather than toggling it after the map has loaded.
  • DefaultLatitude / DefaultLongitude / DefaultZoomLevel describe the initial view only. Once the user pans or zooms, or once the map is recentered from code, changing them has no visible effect — use the map's own navigation for later view changes.

Tile layers

Tile layers are the overlays stacked on top of the base layer: a hillshade, a weather radar sweep, a cadastral WMS layer. Add them with the public AddTileLayer(AURL, AOpacity) method, which returns the new TTMSFNCLeafletTileLayer so the source kind and any service parameters can be set on the result. TileLayers itself is a protected collection, so application code goes through AddTileLayer and ClearTileLayers rather than TileLayers.Add.

procedure TForm1.ConfigureLeafletTileLayers;
var
  LOverlay: TTMSFNCLeafletTileLayer;
  LWeather: TTMSFNCLeafletTileLayer;
begin
  TMSFNCLeaflet1.BeginUpdate;
  try
    TMSFNCLeaflet1.ClearTileLayers;

    { XYZ template overlay, drawn on top of the base layer. }
    LOverlay := TMSFNCLeaflet1.AddTileLayer(
      'https://tile.opentopomap.org/{z}/{x}/{y}.png', 0.45);
    LOverlay.Source := lsXYZ;

    { WMS service: the URL is the service endpoint and Params.Layers names
      the layer(s) to request. }
    LWeather := TMSFNCLeaflet1.AddTileLayer(
      'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi', 0.6);
    LWeather.Source := lsWMS;
    LWeather.Params.Layers := 'nexrad-n0r-900913';
  finally
    TMSFNCLeaflet1.EndUpdate;
  end;
end;

Source selects how the URL is interpreted: lsXYZ treats it as a {z}/{x}/{y} tile template, while lsWMS treats it as a WMS service endpoint and sends Params.Layers as the requested layer name — a WMS URL added as lsXYZ silently renders nothing. Layers draw in the order they were added, so add the broad imagery first and the detail overlay last, and give overlays an Opacity below 1 so the base layer stays readable through them.

Leaflet WMS tile layer

Markers

Add markers via Markers.Add and set Coordinate.Latitude, Coordinate.Longitude, and Title.

Polygons and polylines

Use Polygons.Add (TTMSFNCLeafletPolygon) and Polylines.Add (TTMSFNCLeafletPolyline) to add area and route overlays.

Polygons with holes

A hole excludes an interior ring from a filled polygon — a lake inside a park, an exclusion zone inside a delivery area, a courtyard inside a building footprint. Drawing two separate polygons cannot express this, because the inner one would still be filled; AddHole makes the region genuinely transparent so the tiles and any lower overlay show through.

procedure TForm1.ConfigureLeafletPolygonHole;
var
  LPolygon: TTMSFNCLeafletPolygon;
  LOuter: TTMSFNCMapsCoordinateRecArray;
  LInner: TTMSFNCMapsCoordinateRecArray;
begin
  SetLength(LOuter, 4);
  LOuter[0] := CreateCoordinate(51.55, -0.20);
  LOuter[1] := CreateCoordinate(51.55, -0.05);
  LOuter[2] := CreateCoordinate(51.46, -0.05);
  LOuter[3] := CreateCoordinate(51.46, -0.20);

  { The hole ring must lie inside the outer ring. }
  SetLength(LInner, 4);
  LInner[0] := CreateCoordinate(51.52, -0.14);
  LInner[1] := CreateCoordinate(51.52, -0.11);
  LInner[2] := CreateCoordinate(51.50, -0.11);
  LInner[3] := CreateCoordinate(51.50, -0.14);

  TMSFNCLeaflet1.BeginUpdate;
  try
    LPolygon := TMSFNCLeaflet1.AddPolygon(LOuter, True);
    LPolygon.AddHole(LInner);
  finally
    TMSFNCLeaflet1.EndUpdate;
  end;
end;

AddHole takes a TTMSFNCMapsCoordinateRecArray and returns the created hole item, and can be called more than once to punch several holes out of the same polygon. The ring must lie inside the polygon's outer ring; a hole that crosses the outline produces undefined fill. Holes belong to the polygon, so clearing or rebuilding the polygon discards them — re-add them after any rebuild.

Dragging overlays

Leaflet can let the user reposition an overlay directly on the map, which is the natural gesture for correcting a geocoded address, moving a depot, or nudging a service area. Every Leaflet overlay item — marker, polygon, polyline, circle, rectangle — declares Draggable, and it is False by default so a map stays read-only until you opt in.

procedure TForm1.ConfigureLeafletDragging;
var
  LMarker: TTMSFNCLeafletMarker;
  LPolygon: TTMSFNCLeafletPolygon;
  LCircle: TTMSFNCLeafletCircle;
  LArea: TTMSFNCMapsCoordinateRecArray;
begin
  TMSFNCLeaflet1.BeginUpdate;
  try
    { Markers.Add and AddMarker both return the Leaflet marker class,
      so Draggable is reachable without a cast. }
    LMarker := TMSFNCLeaflet1.AddMarker(51.5074, -0.1278, 'Depot');
    LMarker.Draggable := True;

    SetLength(LArea, 4);
    LArea[0] := CreateCoordinate(51.53, -0.16);
    LArea[1] := CreateCoordinate(51.53, -0.09);
    LArea[2] := CreateCoordinate(51.49, -0.09);
    LArea[3] := CreateCoordinate(51.49, -0.16);

    LPolygon := TMSFNCLeaflet1.AddPolygon(LArea, True);
    LPolygon.Draggable := True;

    { Use AddCircle / AddRectangle rather than Circles.Add / Rectangles.Add:
      only the Add* methods are typed as the Leaflet item class that
      declares Draggable. }
    LCircle := TMSFNCLeaflet1.AddCircle(CreateCoordinate(51.5033, -0.1196), 800);
    LCircle.Draggable := True;
  finally
    TMSFNCLeaflet1.EndUpdate;
  end;

  TMSFNCLeaflet1.OnMarkerDragEnd := TMSFNCLeaflet1MarkerDragEnd;
end;

procedure TForm1.TMSFNCLeaflet1MarkerDragEnd(Sender: TObject;
  AEventData: TTMSFNCMapsEventData);
begin
  if not Assigned(AEventData.Marker) then
    Exit;

  { Only AEventData.Coordinate carries the drop position - the marker item
    still holds its pre-drag coordinate, so write it back explicitly. }
  AEventData.Marker.Coordinate.Latitude := AEventData.Coordinate.Latitude;
  AEventData.Marker.Coordinate.Longitude := AEventData.Coordinate.Longitude;

  StatusLabel.Text := Format('%s moved to %.6f, %.6f',
    [AEventData.Marker.Title,
     AEventData.Coordinate.Latitude, AEventData.Coordinate.Longitude]);
end;

OnMarkerDragEnd fires once, when the user releases the marker, not continuously during the gesture — so it is safe to persist from directly inside the handler. AEventData.Coordinate holds the drop position and AEventData.Marker is the item that moved, resolved by its map ID.

The marker item is not updated for you. AEventData.Marker.Coordinate still holds the pre-drag latitude and longitude; only the event's own Coordinate reflects the drop. Assign it back to the item inside the handler whenever later code reads the marker's position, otherwise the map and the object model drift apart the first time the user drags anything.

The one trap is typing. Draggable is declared on the Leaflet item classes, but only some of the collections are re-typed to them: Markers, Polygons, and Polylines return Leaflet items, while Circles and Rectangles are still typed as the shared maps classes. Add circles and rectangles through AddCircle and AddRectangle — which are typed as TTMSFNCLeafletCircle and TTMSFNCLeafletRectangle — instead of Circles.Add / Rectangles.Add, and no cast is needed anywhere. There is no matching drag-end event for shapes, so when a dragged polygon's position must be persisted, read its coordinates on your own trigger (a Save button, or OnMapClick) rather than waiting for a callback.

Heat maps

Heat maps show density and intensity across a region without turning every data point into a clickable marker. Add a heat map via HeatMaps.Add (TTMSFNCLeafletHeatMap), or call AddHeatMap when a coordinate array is already available. Populate WeightedCoordinates with latitude, longitude, and a Weight value per point, then tune Opacity and the GradientStartColor/GradientMidColor/GradientEndColor colors so the overlay stays readable against the active tile layer.

procedure TForm1.ConfigureLeafletHeatMap;
var
  LHeatMap: TTMSFNCLeafletHeatMap;
  LWeightedCoordinate: TTMSFNCMapsWeightedCoordinate;
begin
  TMSFNCLeaflet1.HeatMaps.Clear;

  LHeatMap := TMSFNCLeaflet1.HeatMaps.Add;
  LHeatMap.Opacity := 0.6;
  LHeatMap.GradientStartColor := gcGreen;
  LHeatMap.GradientMidColor := gcYellow;
  LHeatMap.GradientEndColor := gcRed;

  LWeightedCoordinate := LHeatMap.WeightedCoordinates.Add;
  LWeightedCoordinate.Coordinate.Latitude := 51.5074;
  LWeightedCoordinate.Coordinate.Longitude := -0.1278;
  LWeightedCoordinate.Weight := 0.9;

  LWeightedCoordinate := LHeatMap.WeightedCoordinates.Add;
  LWeightedCoordinate.Coordinate.Latitude := 51.5155;
  LWeightedCoordinate.Coordinate.Longitude := -0.1419;
  LWeightedCoordinate.Weight := 0.6;
end;

Prefer a heat map when the audience needs a regional trend (busy areas, sensor readings, request volume) and markers or labels when they need an exact point. Call HeatMaps.Clear or ClearHeatMaps before repopulating a layer so refreshed data does not stack on top of the previous set.

Right-click events

Leaflet exposes right-click hooks for the map canvas, markers, and poly elements. Wire OnMapRightClick, OnMarkerRightClick, and OnPolyElementRightClick when the application needs context menus, quick edit actions, or inspection panels without taking over the normal left-click navigation and selection flow.

procedure TForm1.ConfigureLeafletContextEvents;
begin
  TMSFNCLeaflet1.OnMapRightClick := TMSFNCLeaflet1MapRightClick;
  TMSFNCLeaflet1.OnMarkerRightClick := TMSFNCLeaflet1MarkerRightClick;
  TMSFNCLeaflet1.OnPolyElementRightClick := TMSFNCLeaflet1PolyElementRightClick;
end;

procedure TForm1.TMSFNCLeaflet1MapRightClick(Sender: TObject;
  AEventData: TTMSFNCMapsEventData);
begin
  { Show a map-level context menu at AEventData.Coordinate. }
end;

procedure TForm1.TMSFNCLeaflet1MarkerRightClick(Sender: TObject;
  AEventData: TTMSFNCMapsEventData);
begin
  if Assigned(AEventData.Marker) then
    AEventData.Marker.Title := 'Selected marker';
end;

procedure TForm1.TMSFNCLeaflet1PolyElementRightClick(Sender: TObject;
  AEventData: TTMSFNCMapsEventData);
begin
  if Assigned(AEventData.PolyElement) then
    AEventData.PolyElement.DataString := 'Selected poly element';
end;

Each handler receives a TTMSFNCMapsEventData argument, so the same pattern reads the clicked Coordinate, Marker, or PolyElement depending on which event fired. Keep handlers short; long-running work should be deferred so the embedded browser stays responsive.

See also