unit View.Map;

interface

uses
  System.SysUtils, System.Classes, WEBLib.Graphics, WEBLib.Forms, WEBLib.Dialogs,
  Vcl.Controls, Vcl.StdCtrls, WEBLib.StdCtrls, WEBLib.Controls, WEBLib.Grids,
  WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
  VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
  VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
  XData.Web.Connection, ConnectionModule, Utils, uMapFilters, JS, Web, uMapMarkerJs;

type
  TFViewMap = class(TWebForm)
    pnlMap: TWebPanel;
    lfMap: TTMSFNCLeaflet;
    httpReqGeoJson: TWebHttpRequest;
    xdwcMap: TXDataWebClient;
    btnFindLocation: TWebButton;
    tmrLocate: TWebTimer;

    procedure lfMapMapInitialized(Sender: TObject);
    [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
    procedure lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
    procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
    procedure btnFindLocationClick(Sender: TObject);
    procedure tmrLocateTimer(Sender: TObject);
  private
    userLocationMarker: TTMSFNCMapsMarker;
    geoWatchId: Integer;
    FUnitsLoaded: Boolean;
    FComplaintsLoaded: Boolean;
    FLoadingPoints: Boolean;
    mapFilters: TMapFilters;
    FPendingUnitId: string;
    FPendingComplaintId: string;
    FPendingFocusCoord: TTMSFNCMapsCoordinateRec;
    FPendingFocusZoom: Integer;
    FDoFocusZoom: Boolean;
    FGeoJsonLoadStep: Integer;

    [async] procedure LoadPointsAsync(showBusy: Boolean);
    procedure UpdateDeviceLocation(lat, lng: Double);
    procedure StartDeviceLocation;
    procedure ApplyPendingUnitFocus;
    procedure ApplyPendingComplaintFocus;
    procedure StartGeoJsonLoad;
    procedure LoadGeoJsonLayer(const AResponse, ALayerName: string; FitLayer: Boolean);
    function GetJsonString(obj: TJSObject; name: string): string;
    function HtmlAttrEncode(const value: string): string;
  public
    procedure FocusUnit(const unitId: string);
    procedure FocusComplaint(const complaintId: string);
    procedure RefreshData;
  end;

var
  FViewMap: TFViewMap;

implementation

{$R *.dfm}

procedure TFViewMap.lfMapMapInitialized(Sender: TObject);
begin
  RegisterMapMarkerJs;
  ShowSpinner('spinner');
  FUnitsLoaded := False;
  FComplaintsLoaded := False;
  StartGeoJsonLoad;
  asm
    window.showComplaintDetails = function (id) {
      try {
        pas['View.Main'].FViewMain.ShowComplaintDetails(id);
      } catch (e) {
      }
    };

    window.showUnitDetails = function (id) {
      try {
        pas['View.Main'].FViewMain.ShowUnitDetails(id);
      } catch (e) {
      }
    };
  end;
  StartDeviceLocation;
end;


procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
  if userLocationMarker = nil then
  begin
    userLocationMarker := lfMap.Markers.Add;
    userLocationMarker.Title := '<div class="small fw-semibold">You are here</div>';
    userLocationMarker.DataString := 'device';
    userLocationMarker.IconURL := 'assets/markers/location_dot.png';
    userLocationMarker.Visible := False;
  end;

  userLocationMarker.Latitude := lat;
  userLocationMarker.Longitude := lng;
end;


procedure TFViewMap.StartDeviceLocation;
begin
  asm
    const self = this;

    if (!navigator.geolocation) {
      console.log('Geolocation not supported');
      return;
    }

    const onPos = function (pos) {
      const lat = pos.coords.latitude;
      const lng = pos.coords.longitude;

      try {
        self.UpdateDeviceLocation(lat, lng);
      } catch (e) {
        console.log('UpdateDeviceLocation failed', e);
      }
    };

    const onErr = function (err) {
      console.log('Geolocation error:', err && err.message ? err.message : err);
    };

    if (self.geoWatchId != null && self.geoWatchId !== 0) {
      try { navigator.geolocation.clearWatch(self.geoWatchId); } catch(e) {}
      self.geoWatchId = 0;
    }

    self.geoWatchId = navigator.geolocation.watchPosition(onPos, onErr, {
      enableHighAccuracy: true,
      maximumAge: 5000,
      timeout: 15000
    });
  end;
end;

procedure TFViewMap.StartGeoJsonLoad;
begin
  lfMap.BeginUpdate;
  try
    lfMap.Polygons.Clear;
  finally
    lfMap.EndUpdate;
  end;

  FGeoJsonLoadStep := 1;
  httpReqGeoJson.URL := 'assets/wyomingtowns.geojson';
  httpReqGeoJson.Execute;
end;

procedure TFViewMap.LoadGeoJsonLayer(const AResponse, ALayerName: string; FitLayer: Boolean);
var
  startIndex: Integer;
  i: Integer;
  P: TTMSFNCMapsPolygon;
begin
  startIndex := lfMap.Polygons.Count;

  lfMap.BeginUpdate;
  try
    lfMap.LoadGeoJSONFromText(AResponse, True, FitLayer);

    for i := startIndex to lfMap.Polygons.Count - 1 do
    begin
      P := lfMap.Polygons[i];

      if SameText(ALayerName, 'Towns') then
      begin
        P.FillColor := HTMLToColor('#ffffff');
        P.FillOpacity := 0.0;
        P.StrokeColor := HTMLToColor('#000000');
        P.StrokeOpacity := 1.0;
        P.StrokeWidth := 2;
      end
      else if SameText(ALayerName, 'Villages') then
      begin
        P.FillColor := HTMLToColor('#0d6efd');
        P.FillOpacity := 0.0;
        P.StrokeColor := HTMLToColor('#0d6efd');
        P.StrokeOpacity := 1.0;
        P.StrokeWidth := 2;
      end;
    end;
  finally
    lfMap.EndUpdate;
  end;
end;


[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var
  layerName: string;
  fitLayer: Boolean;
begin
  if FGeoJsonLoadStep = 1 then
    layerName := 'Towns'
  else
    layerName := 'Villages';

  fitLayer :=
    (FGeoJsonLoadStep = 1) and
    (Trim(FPendingUnitId) = '') and
    (Trim(FPendingComplaintId) = '');

  LoadGeoJsonLayer(AResponse, layerName, fitLayer);

  if FGeoJsonLoadStep = 1 then
  begin
    FGeoJsonLoadStep := 2;
    httpReqGeoJson.URL := 'assets/wyomingvillages.geojson';
    httpReqGeoJson.Execute;
    Exit;
  end;

  FGeoJsonLoadStep := 0;

  await(LoadPointsAsync(True));

  if mapFilters = nil then
  begin
    mapFilters := TMapFilters.Create(lfMap);
    mapFilters.Init;
  end;
end;


function TFViewMap.GetJsonString(obj: TJSObject; name: string): string;
begin
  Result := '';

  if obj = nil then
    Exit;

  if obj[name] = nil then
    Exit;

  Result := string(obj[name]);

  if SameText(Trim(Result), 'undefined') or SameText(Trim(Result), 'null') then
    Result := '';
end;


function TFViewMap.HtmlAttrEncode(const value: string): string;
begin
  Result := value;
  Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
  Result := StringReplace(Result, #39, '&#39;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
end;


[async] procedure TFViewMap.LoadPointsAsync(showBusy: Boolean);
var
  resp: TXDataClientResponse;
  root, item, uo: TJSObject;
  units: TJSArray;
  i, ui: Integer;
  m: TTMSFNCMapsMarker;
  lat, lng: Double;
  uName, unitBadge, agencyType, agencyId, agencyName, agency: string;
  unitId, callType, priorityText, statusText: string;
  complaintId, codeDesc, priority, priorityBadge, complaintStatusKey, business, address: string;
  pngName, iconUrl, rowsHtml: string;
  officer1Lname, officer1Fname, officer1Empnum: string;
  officer2Lname, officer2Fname, officer2Empnum: string;
  officer1Display, officer2Display: string;
  updateTimeText: string;
  canShowDetails: Boolean;
  canShowDetailsText: string;
  detailsBtnHtml: string;
  unitsData: TJSArray;
  complaintsData: TJSArray;
begin
  if FLoadingPoints then
    Exit;

  FLoadingPoints := True;

  if showBusy then
    ShowSpinner('spinner');
  try
    FUnitsLoaded := False;
    FComplaintsLoaded := False;

    unitsData := nil;
    complaintsData := nil;

    // --- Fetch Units ---------------------------------------------------------
    try
      resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
      root := TJSObject(resp.Result);
      unitsData := TJSArray(root['data']);
      FUnitsLoaded := True;
    except
      on E: EXDataClientRequestException do
        Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
    end;

    // --- Fetch Complaints ----------------------------------------------------
    try
      resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
      root := TJSObject(resp.Result);
      complaintsData := TJSArray(root['data']);
      FComplaintsLoaded := True;
    except
      on E: EXDataClientRequestException do
        Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
    end;

    // --- Swap Markers (no blank map while loading) ---------------------------
    lfMap.BeginUpdate;
    try
      for i := lfMap.Markers.Count - 1 downto 0 do
      begin
        m := lfMap.Markers[i];

        if StartsText('unit|', m.DataString) or StartsText('complaint|', m.DataString) then
          lfMap.Markers.Delete(i);
      end;

      // Unit markers
      if unitsData <> nil then
      begin
        for i := 0 to unitsData.Length - 1 do
        begin
          item := TJSObject(unitsData[i]);

          lat := Double(item['Lat']);
          lng := Double(item['Lng']);
          uName := GetJsonString(item, 'UnitName');
          unitId := GetJsonString(item, 'UnitId');

          unitBadge := GetJsonString(item, 'UnitBadge');
          if Trim(unitBadge) = '' then
            unitBadge := uName;
          if Trim(unitBadge) = '' then
            unitBadge := unitId;

          agencyId := GetJsonString(item, 'Agency');
          agencyName := GetJsonString(item, 'AgencyName');
          agency := agencyName;
          if Trim(agency) = '' then
            agency := agencyId;

          agencyType := UpperCase(Trim(GetJsonString(item, 'AgencyType')));

          if agencyType = 'FIR' then
            iconUrl := 'assets/markers/car_fire.png'
          else if agencyType = 'EMS' then
            iconUrl := 'assets/markers/car_ems.png'
          else if agencyType = 'POL' then
            iconUrl := 'assets/markers/car_police.png'
          else
            iconUrl := 'assets/markers/car_police.png';

          callType := GetJsonString(item, 'CallType');
          priorityText := GetJsonString(item, 'Priority');
          statusText := GetJsonString(item, 'Status');
          updateTimeText := GetJsonString(item, 'UpdateTime');

          officer1Lname := GetJsonString(item, 'Officer1Lname');
          officer1Fname := GetJsonString(item, 'Officer1Fname');
          officer1Empnum := GetJsonString(item, 'Officer1Empnum');

          officer2Lname := GetJsonString(item, 'Officer2Lname');
          officer2Fname := GetJsonString(item, 'Officer2Fname');
          officer2Empnum := GetJsonString(item, 'Officer2Empnum');

          canShowDetailsText := '';
          if item['CanShowDetails'] <> nil then
            canShowDetailsText := string(item['CanShowDetails']);
          canShowDetails := SameText(Trim(canShowDetailsText), 'true');

          if canShowDetails then
          begin
            detailsBtnHtml :=
              '<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
                'onclick="window.showUnitDetails(''' + unitId + ''')">' +
                'Details' +
              '</button>';
          end
          else
            detailsBtnHtml := '';

          officer1Display := '';
          if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then
          begin
            officer1Display := Trim(officer1Lname);
            if Trim(officer1Fname) <> '' then
              officer1Display := officer1Display + ', ' + Trim(officer1Fname);
            if Trim(officer1Empnum) <> '' then
              officer1Display := officer1Display + ' (' + Trim(officer1Empnum) + ')';
          end;

          officer2Display := '';
          if Trim(officer2Lname + officer2Fname + officer2Empnum) <> '' then
          begin
            officer2Display := Trim(officer2Lname);
            if Trim(officer2Fname) <> '' then
              officer2Display := officer2Display + ', ' + Trim(officer2Fname);
            if Trim(officer2Empnum) <> '' then
              officer2Display := officer2Display + ' (' + Trim(officer2Empnum) + ')';
          end;

          m := lfMap.Markers.Add;
          m.Latitude := lat;
          m.Longitude := lng;

        m.Title :=
          '<span class="emi-marker-meta" data-marker-type="unit" data-unit-badge="' +
            HtmlAttrEncode(unitBadge) +
          '" data-picker-label="' +
            HtmlAttrEncode('Unit ' + unitBadge) +
          '" style="display:none"></span>' +
            '<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
              '<div class="fw-semibold small">' +
                '<span class="fw-bold">Unit:</span> ' + uName +
              '</div>' +
              IfThen(agency <> '',
                '<div class="small"><span class="fw-bold">Agency:</span> ' + agency + '</div>',
                ''
              ) +
              IfThen(Trim(callType) <> '',
                '<div class="small"><span class="fw-bold">Call Type:</span> ' + callType + '</div>',
                ''
              ) +
              IfThen(Trim(priorityText) <> '',
                '<div class="small"><span class="fw-bold">Priority:</span> ' + priorityText + '</div>',
                ''
              ) +
              IfThen(Trim(statusText) <> '',
                '<div class="small"><span class="fw-bold">Status:</span> ' + statusText + '</div>',
                ''
              ) +
              IfThen(Trim(officer1Display) <> '',
                '<div class="small"><span class="fw-bold">Officer 1:</span> ' + officer1Display + '</div>',
                ''
              ) +
              IfThen(Trim(officer2Display) <> '',
                '<div class="small"><span class="fw-bold">Officer 2:</span> ' + officer2Display + '</div>',
                ''
              ) +
              IfThen(Trim(updateTimeText) <> '',
                '<div class="small mb-1"><span class="fw-bold">Updated:</span> ' + updateTimeText + '</div>',
                '<div class="small mb-1"></div>'
              ) +
              IfThen(detailsBtnHtml <> '',
                '<div class="d-flex justify-content-end mt-0">' + detailsBtnHtml + '</div>',
                ''
              ) +
            '</div>';

          m.DataString := 'unit|' + unitId + '|' + StringReplace(unitBadge, '|', '/', [rfReplaceAll]);
          m.IconURL := iconUrl;

        end;
      end;

      // Complaint markers
      if complaintsData <> nil then
      begin
        for i := 0 to complaintsData.Length - 1 do
        begin
          item := TJSObject(complaintsData[i]);

          complaintId := GetJsonString(item, 'ComplaintId');
          codeDesc := GetJsonString(item, 'DispatchCodeDesc');

          agencyId := GetJsonString(item, 'Agency');
          agencyName := GetJsonString(item, 'AgencyName');
          agency := agencyName;
          if Trim(agency) = '' then
            agency := agencyId;

          priority := GetJsonString(item, 'Priority');

          priorityBadge := GetJsonString(item, 'PriorityBadge');
          if Trim(priorityBadge) = '' then
            priorityBadge := '?';

          complaintStatusKey := LowerCase(Trim(GetJsonString(item, 'ComplaintStatusKey')));
          if complaintStatusKey = '' then
            complaintStatusKey := 'notattached';

          business := GetJsonString(item, 'Business');
          address := GetJsonString(item, 'Address');

          lat := Double(item['Lat']);
          lng := Double(item['Lng']);

          if ((lat = 0) and (lng = 0)) or (Abs(lat) > 90) or (Abs(lng) > 180) then
            Continue;

          pngName := GetJsonString(item, 'pngName');
          if Trim(pngName) <> '' then
            iconUrl := 'assets/markers/' + pngName
          else
            iconUrl := 'assets/markers/default_notattached.png';

          rowsHtml := '';
          units := TJSArray(item['Units']);

          if Assigned(units) and (units.Length > 0) then
          begin
            for ui := 0 to units.Length - 1 do
            begin
              uo := TJSObject(units[ui]);

              rowsHtml := rowsHtml +
                '<tr>' +
                  '<td>' + GetJsonString(uo, 'Unit') + '</td>' +
                  '<td>' + GetJsonString(uo, 'Status') + '</td>' +
                  '<td>' + GetJsonString(uo, 'Updated') + '</td>' +
                '</tr>';
            end;
          end
          else
            rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';

          m := lfMap.Markers.Add;
          m.Latitude := lat;
          m.Longitude := lng;

          m.DataString := 'complaint|' + complaintId;
          m.IconURL := iconUrl;

          m.Title :=
            '<span class="emi-marker-meta" data-marker-type="complaint" data-priority-badge="' +
              HtmlAttrEncode(priorityBadge) +
            '" data-complaint-status="' +
              HtmlAttrEncode(complaintStatusKey) +
            '" data-picker-label="' +
              HtmlAttrEncode('Complaint ' + complaintId + IfThen(Trim(codeDesc) <> '', ' - ' + codeDesc, '')) +
            '" style="display:none"></span>' +
            '<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
                '<div class="fw-semibold small">' +
                '<span class="fw-bold">Complaint:</span> ' + complaintId +
              '</div>' +
              '<div class="small">' +
                '<span class="fw-bold">Priority:</span> ' + priority +
              '</div>' +
              '<div class="small">' +
                '<span class="fw-bold">Dispatch Code:</span> ' + codeDesc +
              '</div>' +
              '<div class="small">' +
                '<span class="fw-bold">Agency:</span> ' + agency +
              '</div>' +
              IfThen(Trim(business) <> '',
                '<div class="small">' +
                  '<span class="fw-bold">Business:</span> ' + business +
                '</div>',
                ''
              ) +
              '<div class="small mb-1">' +
                '<span class="fw-bold">Address:</span> ' + address +
              '</div>' +
              '<table class="table table-sm table-bordered mb-1 emi-tip-table">' +
                '<colgroup>' +
                  '<col style="width:34%">' +
                  '<col style="width:33%">' +
                  '<col style="width:33%">' +
                '</colgroup>' +
                '<thead class="table-light">' +
                  '<tr>' +
                    '<th>Unit</th>' +
                    '<th>Status</th>' +
                    '<th>Updated</th>' +
                  '</tr>' +
                '</thead>' +
                '<tbody>' + rowsHtml + '</tbody>' +
              '</table>' +
              '<div class="d-flex justify-content-end mt-0">' +
                '<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
                  'onclick="window.showComplaintDetails(''' + complaintId + ''')">' +
                  'Details' +
                '</button>' +
              '</div>' +
            '</div>';
        end;
      end;
    finally
      lfMap.EndUpdate;
    end;

    if mapFilters <> nil then
      mapFilters.Apply;

    ApplyPendingUnitFocus;
    ApplyPendingComplaintFocus;
  finally
    if showBusy then
      HideSpinner('spinner');
  FLoadingPoints := False;
  end;
end;


procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
begin
  ACustomizeMarker :=
    'if (window.emiCustomizeMarker) window.emiCustomizeMarker(' + MARKERVAR + ');';
end;


procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin
  ACustomizeCSS :=
  // --- Popup container----------------------------------------------------------
    '.emi-tip .leaflet-popup-content-wrapper{' +
      'border-radius:8px;' +
      'box-shadow:0 4px 14px rgba(0,0,0,.25);' +
    '}' + #13#10 +
  // --- Popup content------------------------------------------------------------
    '.emi-tip .leaflet-popup-content{' +
      'margin:0;' +
      'padding:0;' +
      'width:auto;' +
      'max-width:260px;' +
    '}' + #13#10 +
    '.emi-marker-picker .btn{font-size:12px;line-height:1.2;border:1px solid rgba(0,0,0,.15);}'+#13#10+
    '.emi-marker-picker .btn:hover{background:#e9ecef;}'+#13#10+
    '.emi-marker-picker-section{font-size:11px;font-weight:700;color:#6c757d;margin:.15rem 0 .25rem;}'+#13#10+
    '.emi-marker-picker-divider{border-top:1px solid rgba(0,0,0,.18);margin:.35rem 0 .45rem;}'+#13#10+
  // --- Media Query: on very small screens, lets it grow almost full width ------
    '@media (max-width:480px){' +
      '.emi-tip .leaflet-popup-content{' +
        'max-width:calc(100vw - 32px);' +
      '}' +
    '}' + #13#10 +
  // --- Table: compact, wraps text ----------------------------------------------
    '.emi-tip .emi-tip-table{display:table;border-collapse:collapse;table-layout:auto;width:auto;margin:.25rem 0;}'+#13#10+
    '.emi-tip .emi-tip-table thead{display:table-header-group;}'+#13#10+
    '.emi-tip .emi-tip-table tbody{display:table-row-group;}'+#13#10+
    '.emi-tip .emi-tip-table tr{display:table-row;}'+#13#10+
    '.emi-tip .emi-tip-table th,.emi-tip .emi-tip-table td{' +
      'display:table-cell;' +
      'white-space:normal;' +
      'word-break:break-word;' +
      'padding:.125rem .25rem;' +
      'font-size:11px;' +
      'vertical-align:middle;' +
    '}' + #13#10 +
  // --- Marker Badge ------------------------------------------------------------
    '.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
    '.emi-marker-wrap-unit{padding-top:13px;}'+#13#10+
    '.emi-marker-img{display:block;}'+#13#10+
    '.emi-marker-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;background:var(--bs-danger);color:#fff;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}'+#13#10+
    '.emi-marker-wrap-complaint{padding-top:0;}'+#13#10+
    '.emi-complaint-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:999px;color:#111827;font:700 11px/16px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;box-shadow:0 0 0 2px #fff;}'+#13#10+
    '.emi-unit-badge{position:absolute;top:0;left:50%;transform:translateX(-50%);min-width:34px;max-width:58px;height:15px;padding:0 5px;border-radius:4px;background:#111827;color:#fff;font:700 10px/15px system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 0 0 2px #fff;z-index:2;}'+#13#10;
end;

procedure TFViewMap.tmrLocateTimer(Sender: TObject);
begin
  tmrLocate.Enabled := False;

  if not FDoFocusZoom then
    Exit;

  lfMap.SetCenterCoordinate(FPendingFocusCoord);
  lfMap.SetZoomLevel(FPendingFocusZoom);

  FDoFocusZoom := False;
end;

procedure TFViewMap.btnFindLocationClick(Sender: TObject);
var
  coord: TTMSFNCMapsCoordinateRec;
begin
  if userLocationMarker = nil then
    Exit;

  coord := CreateCoordinate(userLocationMarker.Latitude, userLocationMarker.Longitude);
  lfMap.SetCenterCoordinate(coord);

  FPendingFocusCoord := coord;
  FPendingFocusZoom := 17;
  FDoFocusZoom := True;

  tmrLocate.Interval := 250;
  tmrLocate.Enabled := True;
end;


procedure TFViewMap.FocusUnit(const unitId: string);
begin
  FPendingUnitId := Trim(unitId);
  if mapFilters <> nil then
    LoadPointsAsync(True);
end;


procedure TFViewMap.FocusComplaint(const complaintId: string);
begin
  FPendingComplaintId := Trim(complaintId);
  if mapFilters <> nil then
    LoadPointsAsync(True);
end;


procedure TFViewMap.ApplyPendingUnitFocus;
var
  i: Integer;
  m: TTMSFNCMapsMarker;
  coord: TTMSFNCMapsCoordinateRec;
  targetDs: string;
  found: Boolean;
begin
  if Trim(FPendingUnitId) = '' then
    Exit;

  targetDs := 'unit|' + FPendingUnitId;
  found := False;

  for i := 0 to lfMap.Markers.Count - 1 do
  begin
    m := lfMap.Markers[i];
    if SameText(m.DataString, targetDs) or StartsText(targetDs + '|', m.DataString) then
    begin
      coord := CreateCoordinate(m.Latitude, m.Longitude);
      lfmap.SetCenterCoordinate(coord);

      FPendingFocusCoord := coord;
      FPendingFocusZoom := 17;
      FDoFocusZoom := True;

      tmrLocate.Interval := 250;
      tmrLocate.Enabled := True;

      found := True;
      Break;
    end;
  end;

  if found then
    FPendingUnitId := '';
end;

procedure TFViewMap.ApplyPendingComplaintFocus;
var
  i: Integer;
  m: TTMSFNCMapsMarker;
  coord: TTMSFNCMapsCoordinateRec;
  targetDs: string;
  found: Boolean;
begin
  if Trim(FPendingComplaintId) = '' then
    Exit;

  targetDs := 'complaint|' + FPendingComplaintId;
  found := False;

  for i := 0 to lfMap.Markers.Count - 1 do
  begin
    m := lfMap.Markers[i];
    if SameText(m.DataString, targetDs) then
    begin
      coord := CreateCoordinate(m.Latitude, m.Longitude);
      lfmap.SetCenterCoordinate(coord);

      FPendingFocusCoord := coord;
      FPendingFocusZoom := 17;
      FDoFocusZoom := True;

      tmrLocate.Interval := 250;
      tmrLocate.Enabled := True;

      found := True;
      Break;
    end;
  end;

  if found then
    FPendingComplaintId := '';
end;


procedure TFViewMap.RefreshData;
begin
  Console.Log('Map.RefreshData');
  LoadPointsAsync(False);
end;


end.

