TMS FNC Google Maps Guides
Use TTMSFNCGoogleMaps when the application needs Google Maps JavaScript API features from the TMS FNC Maps component model. The component supports the shared map workflows, including markers, labels, shapes, popups, and events, while exposing Google-specific features such as KML layers, Street View, marker clustering, heat maps, overlay views, map styling, and keyboard shortcut control. The examples below assume a TTMSFNCGoogleMaps named TMSFNCGoogleMaps1 is already on the form and has a valid APIKey.
Labels And Keyboard Shortcuts
The Labels collection is accessible on Google Maps the same way it is on the shared map control. Use labels for persistent text that should remain anchored to a coordinate without becoming a marker popup. Options.ShowKeyboardShortcuts controls whether Google Maps keyboard shortcuts remain active, which is useful when the map shares focus with editors, grids, or shortcut-heavy application screens.
procedure TForm1.ConfigureGoogleMapsLabels;
var
LLabel: TTMSFNCMapsLabel;
begin
TMSFNCGoogleMaps1.Options.ShowKeyboardShortcuts := False;
TMSFNCGoogleMaps1.Labels.Clear;
LLabel := TMSFNCGoogleMaps1.Labels.Add;
LLabel.Coordinate.Latitude := 51.5074;
LLabel.Coordinate.Longitude := -0.1278;
LLabel.Text := 'Operations hub';
LLabel.BackgroundColor := gcWhite;
LLabel.BorderColor := gcDodgerblue;
end;
Use visible labels sparingly on dense maps. For many points, prefer marker titles, clustering, or an overlay view that appears only for selected data.
KML Layers And Click Events
KML layers are useful when external systems publish route, region, or asset overlays as KML files. Use AddKMLLayer for runtime setup and pass ASuppressInfoWindows when the application owns the click response instead of using Google Maps' default KML info windows. OnKMLLayerClick receives the map event data for application-level inspection or context actions.
procedure TForm1.ConfigureGoogleMapsKMLLayer;
begin
TMSFNCGoogleMaps1.ClearKMLLayers;
TMSFNCGoogleMaps1.AddKMLLayer(
'https://developers.google.com/maps/documentation/javascript/examples/kml/westcampus.kml',
True,
True);
TMSFNCGoogleMaps1.OnKMLLayerClick := TMSFNCGoogleMaps1KMLLayerClick;
end;
procedure TForm1.TMSFNCGoogleMaps1KMLLayerClick(Sender: TObject;
AEventData: TTMSFNCMapsEventData);
begin
{ Inspect AEventData.ID or AEventData.Coordinate and show your own detail UI. }
end;
The KML URL must be reachable by the embedded browser and accepted by Google Maps. If a KML layer is private or generated on demand, expose it through a URL that the map runtime can fetch.
Street View Coordinate And State Events
Options.StreetView controls whether the Google Street View panorama is shown and how its location is chosen. Set Location to slCoordinate when the panorama must open at a known coordinate, then assign Coordinate, Heading, Pitch, and Zoom. Use OnStreetViewEnabledChange when UI outside the map needs to react to the user entering or leaving Street View.
procedure TForm1.ConfigureGoogleMapsStreetView;
begin
TMSFNCGoogleMaps1.Options.ShowStreetViewControl := True;
TMSFNCGoogleMaps1.Options.StreetView.Location := slCoordinate;
TMSFNCGoogleMaps1.Options.StreetView.Coordinate.Latitude := 40.6892;
TMSFNCGoogleMaps1.Options.StreetView.Coordinate.Longitude := -74.0445;
TMSFNCGoogleMaps1.Options.StreetView.Heading := 120;
TMSFNCGoogleMaps1.Options.StreetView.Pitch := 0;
TMSFNCGoogleMaps1.Options.StreetView.Zoom := 1;
TMSFNCGoogleMaps1.Options.StreetView.Enabled := True;
TMSFNCGoogleMaps1.OnStreetViewEnabledChange :=
TMSFNCGoogleMaps1StreetViewEnabledChange;
end;
procedure TForm1.TMSFNCGoogleMaps1StreetViewEnabledChange(Sender: TObject;
AEventData: TTMSFNCMapsEventData;
AStreetViewData: TTMSFNCGoogleMapsStreetViewData);
begin
{ Use AStreetViewData.Enabled to synchronize external UI state. }
end;
Street View availability depends on Google coverage at the requested coordinate. Keep a normal map fallback available when Google cannot return a panorama for the selected location.
Heat Maps And Overlay Views
Heat maps show density while preserving the underlying Google basemap. Populate HeatMaps with weighted coordinates and use opacity or gradient colors to keep the overlay readable. Overlay views are different: they place custom HTML over a coordinate or bounds region, which is useful for rich callouts, badges, images, or status panels that need more layout control than a marker title.
procedure TForm1.ConfigureGoogleMapsHeatMapAndOverlayView;
var
LHeatMap: TTMSFNCGoogleMapsHeatMap;
LWeightedCoordinate: TTMSFNCMapsWeightedCoordinate;
LOverlayView: TTMSFNCGoogleMapsOverlayView;
begin
TMSFNCGoogleMaps1.ClearHeatMaps;
TMSFNCGoogleMaps1.ClearOverlayViews;
LHeatMap := TMSFNCGoogleMaps1.HeatMaps.Add;
LHeatMap.Opacity := 0.55;
LHeatMap.GradientStartColor := gcGreen;
LHeatMap.GradientMidColor := gcYellow;
LHeatMap.GradientEndColor := gcRed;
LWeightedCoordinate := LHeatMap.WeightedCoordinates.Add;
LWeightedCoordinate.Coordinate.Latitude := 34.0522;
LWeightedCoordinate.Coordinate.Longitude := -118.2437;
LWeightedCoordinate.Weight := 0.85;
LOverlayView := TMSFNCGoogleMaps1.AddOverlayView;
LOverlayView.Mode := omCoordinate;
LOverlayView.Coordinate.Latitude := 34.0522;
LOverlayView.Coordinate.Longitude := -118.2437;
LOverlayView.CoordinatePosition := cpBottomCenter;
LOverlayView.Text := '<strong>High activity</strong>';
LOverlayView.Width := 160;
LOverlayView.Clickable := True;
end;
Use heat maps for aggregate intensity and overlay views for selected, explainable content. Overlay views can consume pointer events, so keep Clickable aligned with the interaction you want users to have.
Marker Clustering
Marker clustering keeps dense point sets readable by grouping nearby markers through Google Maps' MarkerClusterer-backed rendering. Add a TTMSFNCGoogleMapsCluster, tune MinimumNumberOfMarkers, MaxZoom, and ZoomOnClick, then assign markers to the cluster and the cluster marker list.
procedure TForm1.ConfigureGoogleMapsClusters;
var
LCluster: TTMSFNCGoogleMapsCluster;
LMarker: TTMSFNCGoogleMapsMarker;
begin
TMSFNCGoogleMaps1.Markers.Clear;
TMSFNCGoogleMaps1.Clusters.Clear;
LCluster := TMSFNCGoogleMaps1.Clusters.Add;
LCluster.Title := 'Regional offices';
LCluster.MinimumNumberOfMarkers := 2;
LCluster.MaxZoom := 12;
LCluster.ZoomOnClick := True;
LCluster.ImagePath := 'images/google-cluster.png';
LMarker := TMSFNCGoogleMaps1.Markers.Add;
LMarker.Coordinate.Latitude := 48.8566;
LMarker.Coordinate.Longitude := 2.3522;
LMarker.Title := 'Paris office';
LMarker.Cluster := LCluster;
LCluster.Markers.Add(LMarker);
LMarker := TMSFNCGoogleMaps1.Markers.Add;
LMarker.Coordinate.Latitude := 50.8503;
LMarker.Coordinate.Longitude := 4.3517;
LMarker.Title := 'Brussels office';
LMarker.Cluster := LCluster;
LCluster.Markers.Add(LMarker);
end;
Clusters should be rebuilt when you change the marker set or cluster styling after the map has already rendered. Use Clusters.Recreate when the current cluster instances need to be regenerated.
Complex Polygons
Complex polygons use polygon holes to represent areas such as service territories with excluded islands, building footprints with courtyards, or administrative regions that omit protected zones. Add the outer polygon with AddPolygon, then add each inner ring with AddHole.
procedure TForm1.AddGoogleMapsComplexPolygon;
var
LOuter: TTMSFNCMapsCoordinateRecArray;
LInner: TTMSFNCMapsCoordinateRecArray;
LPolygon: TTMSFNCGoogleMapsPolygon;
begin
SetLength(LOuter, 4);
LOuter[0].Latitude := 37.782;
LOuter[0].Longitude := -122.447;
LOuter[1].Latitude := 37.782;
LOuter[1].Longitude := -122.425;
LOuter[2].Latitude := 37.768;
LOuter[2].Longitude := -122.425;
LOuter[3].Latitude := 37.768;
LOuter[3].Longitude := -122.447;
SetLength(LInner, 4);
LInner[0].Latitude := 37.778;
LInner[0].Longitude := -122.441;
LInner[1].Latitude := 37.778;
LInner[1].Longitude := -122.431;
LInner[2].Latitude := 37.772;
LInner[2].Longitude := -122.431;
LInner[3].Latitude := 37.772;
LInner[3].Longitude := -122.441;
LPolygon := TMSFNCGoogleMaps1.AddPolygon(LOuter, True);
LPolygon.FillColor := gcDodgerblue;
LPolygon.FillOpacity := 0.35;
LPolygon.StrokeColor := gcNavy;
LPolygon.AddHole(LInner);
end;
Keep outer and inner rings in a consistent coordinate order. When importing polygon data from GeoJSON or another source, verify that the hole coordinates describe only the excluded area and do not cross the outer boundary.
Dragging Markers And Shapes
Dragging turns a read-only map into a light editing surface: correcting a geocoded address by eye, moving a depot pin, or shifting a coverage circle onto the right block. Every Google Maps overlay item declares Draggable, and it is False by default, so nothing moves until you opt a specific item in — which is what you want on a map that also carries reference geometry the user must not disturb.
procedure TForm1.ConfigureGoogleMapsDragging;
var
LMarker: TTMSFNCGoogleMapsMarker;
LPolygon: TTMSFNCGoogleMapsPolygon;
LCircle: TTMSFNCGoogleMapsCircle;
LArea: TTMSFNCMapsCoordinateRecArray;
begin
TMSFNCGoogleMaps1.BeginUpdate;
try
LMarker := TMSFNCGoogleMaps1.AddMarker(51.5074, -0.1278, 'Depot');
LMarker.Draggable := True;
LMarker.Animation := True; { bounce, so the draggable pin stands out }
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 := TMSFNCGoogleMaps1.AddPolygon(LArea, True);
LPolygon.Draggable := True;
LCircle := TMSFNCGoogleMaps1.AddCircle(
CreateCoordinate(51.5033, -0.1196), 800);
LCircle.Draggable := True;
finally
TMSFNCGoogleMaps1.EndUpdate;
end;
TMSFNCGoogleMaps1.OnMarkerDragEnd := TMSFNCGoogleMaps1MarkerDragEnd;
TMSFNCGoogleMaps1.OnPolyElementDragEnd := TMSFNCGoogleMaps1PolyElementDragEnd;
end;
procedure TForm1.TMSFNCGoogleMaps1MarkerDragEnd(Sender: TObject;
AEventData: TTMSFNCMapsEventData);
begin
if not Assigned(AEventData.Marker) then
Exit;
{ The marker item still holds its pre-drag position: only the event
coordinate reflects the drop, so write it back explicitly. }
AEventData.Marker.Coordinate.Latitude := AEventData.Coordinate.Latitude;
AEventData.Marker.Coordinate.Longitude := AEventData.Coordinate.Longitude;
end;
procedure TForm1.TMSFNCGoogleMaps1PolyElementDragEnd(Sender: TObject;
AEventData: TTMSFNCMapsEventData);
begin
if not Assigned(AEventData.PolyElement) then
Exit;
{ Shapes report their new geometry as JSON in CustomData rather than
through Coordinate - see ApplyGoogleMapsGeometry. }
ApplyGoogleMapsGeometry(AEventData.PolyElement, AEventData.CustomData);
end;
Two events report the result, and they report it differently. OnMarkerDragEnd delivers the drop position in AEventData.Coordinate, while OnPolyElementDragEnd delivers the shape's whole new geometry as a JSON payload in AEventData.CustomData — a coordinate array for polygons and polylines, a north-east/south-west pair for rectangles, and a centre plus radius for circles.
Neither event writes the new position back into the item. AEventData.Marker and AEventData.PolyElement are looked up by map ID only; their Coordinate, Coordinates, Bounds, and Center still hold the pre-drag values. Any code that later reads the object model — a save routine, a distance calculation, a JSON export — will use stale geometry unless the handler assigns it, so treat the write-back shown above as part of enabling dragging rather than an optional extra.
Editing Shapes On The Map
Editable is the counterpart to Draggable: instead of moving a shape as a whole, it puts vertex handles on it so the user can reshape a service area, insert a waypoint into a route, or resize a coverage circle by dragging its edge. It is available on polygons, polylines, rectangles, and circles (markers have no shape to edit), and it defaults to False.
procedure TForm1.ConfigureGoogleMapsEditing;
var
LPolygon: TTMSFNCGoogleMapsPolygon;
LRoute: TTMSFNCGoogleMapsPolyline;
LZone: TTMSFNCGoogleMapsCircle;
LArea: TTMSFNCMapsCoordinateRecArray;
begin
TMSFNCGoogleMaps1.BeginUpdate;
try
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 := TMSFNCGoogleMaps1.AddPolygon(LArea, True);
LPolygon.Editable := True;
LPolygon.DataString := 'service-area';
LRoute := TMSFNCGoogleMaps1.AddPolyline(LArea, False);
LRoute.Editable := True;
LRoute.DataString := 'inspection-route';
LZone := TMSFNCGoogleMaps1.AddCircle(
CreateCoordinate(51.5033, -0.1196), 800);
LZone.Editable := True;
LZone.DataString := 'coverage';
finally
TMSFNCGoogleMaps1.EndUpdate;
end;
TMSFNCGoogleMaps1.OnPolyElementEditEnd := TMSFNCGoogleMaps1PolyElementEditEnd;
end;
procedure TForm1.TMSFNCGoogleMaps1PolyElementEditEnd(Sender: TObject;
AEventData: TTMSFNCMapsEventData);
begin
if Assigned(AEventData.PolyElement) then
ApplyGoogleMapsGeometry(AEventData.PolyElement, AEventData.CustomData);
end;
{ CustomData shape depends on the element:
polygon / polyline -> [ { "Latitude": .., "Longitude": .. }, ... ]
rectangle -> { "NorthEast": {..}, "SouthWest": {..} }
circle -> { "Radius": .., "Center": { .. } }
Nothing writes it back into the item, so do it here. Note that only
TTMSFNCMapsPolygon and TTMSFNCMapsPolyline publish Coordinates - on
TTMSFNCMapsPolyElement itself it is protected, so branch and cast. }
procedure TForm1.ApplyGoogleMapsGeometry(
APolyElement: TTMSFNCMapsPolyElement; const AJSON: string);
var
LValue: TJSONValue;
begin
if AJSON = '' then
Exit;
LValue := TTMSFNCUtils.ParseJSON(AJSON);
if not Assigned(LValue) then
Exit;
try
if APolyElement is TTMSFNCMapsCircle then
ApplyCircleGeometry(TTMSFNCMapsCircle(APolyElement), LValue)
else if APolyElement is TTMSFNCMapsRectangle then
ApplyBoundsGeometry(TTMSFNCMapsRectangle(APolyElement), LValue)
else if APolyElement is TTMSFNCMapsPolygon then
ApplyPathGeometry(TTMSFNCMapsPolygon(APolyElement).Coordinates, LValue)
else if APolyElement is TTMSFNCMapsPolyline then
ApplyPathGeometry(TTMSFNCMapsPolyline(APolyElement).Coordinates, LValue);
finally
LValue.Free;
end;
end;
procedure TForm1.ApplyCircleGeometry(ACircle: TTMSFNCMapsCircle;
AValue: TJSONValue);
var
LCenter: TJSONValue;
begin
ACircle.Radius := TTMSFNCUtils.GetJSONDoubleValue(AValue, 'Radius');
LCenter := TTMSFNCUtils.GetJSONValue(AValue, 'Center');
if Assigned(LCenter) then
begin
ACircle.Center.Latitude :=
TTMSFNCUtils.GetJSONDoubleValue(LCenter, 'Latitude');
ACircle.Center.Longitude :=
TTMSFNCUtils.GetJSONDoubleValue(LCenter, 'Longitude');
end;
end;
procedure TForm1.ApplyBoundsGeometry(ARectangle: TTMSFNCMapsRectangle;
AValue: TJSONValue);
var
LNorthEast: TJSONValue;
LSouthWest: TJSONValue;
begin
LNorthEast := TTMSFNCUtils.GetJSONValue(AValue, 'NorthEast');
LSouthWest := TTMSFNCUtils.GetJSONValue(AValue, 'SouthWest');
if not (Assigned(LNorthEast) and Assigned(LSouthWest)) then
Exit;
ARectangle.Bounds.NorthEast.Latitude :=
TTMSFNCUtils.GetJSONDoubleValue(LNorthEast, 'Latitude');
ARectangle.Bounds.NorthEast.Longitude :=
TTMSFNCUtils.GetJSONDoubleValue(LNorthEast, 'Longitude');
ARectangle.Bounds.SouthWest.Latitude :=
TTMSFNCUtils.GetJSONDoubleValue(LSouthWest, 'Latitude');
ARectangle.Bounds.SouthWest.Longitude :=
TTMSFNCUtils.GetJSONDoubleValue(LSouthWest, 'Longitude');
end;
procedure TForm1.ApplyPathGeometry(ACoordinates: TTMSFNCMapsCoordinates;
AValue: TJSONValue);
var
LArray: TJSONArray;
LPoint: TJSONValue;
LItem: TTMSFNCMapsCoordinateItem;
I: Integer;
begin
if not (AValue is TJSONArray) then
Exit;
LArray := TJSONArray(AValue);
ACoordinates.BeginUpdate;
try
ACoordinates.Clear;
for I := 0 to TTMSFNCUtils.GetJSONArraySize(LArray) - 1 do
begin
LPoint := TTMSFNCUtils.GetJSONArrayItem(LArray, I);
LItem := ACoordinates.Add;
LItem.Latitude := TTMSFNCUtils.GetJSONDoubleValue(LPoint, 'Latitude');
LItem.Longitude := TTMSFNCUtils.GetJSONDoubleValue(LPoint, 'Longitude');
end;
finally
ACoordinates.EndUpdate;
end;
end;
OnPolyElementEditEnd reports the reshaped geometry in AEventData.CustomData using the same three JSON shapes as the drag-end event, so one write-back helper serves both. The helper has to branch on the element class, because Coordinates is published on TTMSFNCMapsPolygon and TTMSFNCMapsPolyline but is protected on the shared TTMSFNCMapsPolyElement base — a handler typed against the base class cannot reach it without a cast.
Its name is the one real trap: OnPolyElementEditEnd does not fire once at the end of an editing session. For polygons and polylines it is raised on every individual vertex change — each move, insert, and delete — so a user dragging a boundary around produces a stream of events. For circles it is raised on radius and centre changes, so a single drag of the edge can raise it twice. Keep the handler cheap: update the item and set a dirty flag, and do the expensive work (persisting, recalculating a route, re-querying a service) on an explicit save or a short debounce timer rather than inside the event.
If you need a shape to be reshapeable but not movable, set Editable := True and leave Draggable := False; the two are independent, and enabling both on the same shape gives the user handles and a drag target, which is easy to trigger by accident on a touch screen.
Combined Google Maps Setup
Most production screens combine several Google-specific features: API options, labels, clusters, KML, heat maps, and event wiring. Group setup in an update block so the component can apply the final state without repainting after every individual property assignment.
procedure TForm1.BuildGoogleMapsOperationsScene;
var
LCluster: TTMSFNCGoogleMapsCluster;
LMarker: TTMSFNCGoogleMapsMarker;
LHeatMap: TTMSFNCGoogleMapsHeatMap;
LWeightedCoordinate: TTMSFNCMapsWeightedCoordinate;
begin
TMSFNCGoogleMaps1.BeginUpdate;
try
TMSFNCGoogleMaps1.APIKey := '<your Google Maps JavaScript API key>';
TMSFNCGoogleMaps1.Options.ShowKeyboardShortcuts := False;
TMSFNCGoogleMaps1.Options.StreetView.Location := slCoordinate;
TMSFNCGoogleMaps1.Options.StreetView.Coordinate.Latitude := 52.52;
TMSFNCGoogleMaps1.Options.StreetView.Coordinate.Longitude := 13.405;
TMSFNCGoogleMaps1.AddKMLLayer(
'https://developers.google.com/maps/documentation/javascript/examples/kml/westcampus.kml',
True,
True);
LCluster := TMSFNCGoogleMaps1.Clusters.Add;
LCluster.Title := 'Operations markers';
LCluster.MinimumNumberOfMarkers := 2;
LCluster.ZoomOnClick := True;
LMarker := TMSFNCGoogleMaps1.Markers.Add;
LMarker.Coordinate.Latitude := 52.52;
LMarker.Coordinate.Longitude := 13.405;
LMarker.Title := 'Berlin depot';
LMarker.Cluster := LCluster;
LCluster.Markers.Add(LMarker);
TMSFNCGoogleMaps1.AddLabel(52.52, 13.405, 'Berlin depot', gcWhite, gcDarkgreen);
LHeatMap := TMSFNCGoogleMaps1.HeatMaps.Add;
LHeatMap.Opacity := 0.45;
LWeightedCoordinate := LHeatMap.WeightedCoordinates.Add;
LWeightedCoordinate.Coordinate.Latitude := 52.52;
LWeightedCoordinate.Coordinate.Longitude := 13.405;
LWeightedCoordinate.Weight := 0.8;
TMSFNCGoogleMaps1.OnKMLLayerClick := TMSFNCGoogleMaps1KMLLayerClick;
TMSFNCGoogleMaps1.OnStreetViewEnabledChange :=
TMSFNCGoogleMaps1StreetViewEnabledChange;
finally
TMSFNCGoogleMaps1.EndUpdate;
end;
end;
Split this combined pattern when users can toggle overlays at runtime, but keep API key assignment and option setup close to the map initialization path.
Common Pitfalls
- Assign
APIKeybefore adding Google-specific overlays or Street View options. - Use
AddKMLLayerwhen you needSuppressInfoWindows; the KML item exposes those values through the overload rather than writable published properties. - Disable
Options.ShowKeyboardShortcutswhen the map sits inside a shortcut-heavy application screen. - Check Street View availability for the requested coordinate and keep a map fallback.
- Recreate clusters after substantial marker or cluster styling changes.
- Keep heat map weights consistent across refreshes so colors remain meaningful.
- Write drag and edit results back into the item yourself; the drag-end and edit-end events never update the overlay's own coordinates.
- Treat
OnPolyElementEditEndas a per-vertex notification, not an end-of-session one, and debounce any expensive work it triggers.