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 PlaceUnitMarkers(aData: TJSArray);
    procedure PlaceComplaintMarkers(aData: TJSArray);
    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;
    // Called by View.Main when the server broadcasts a WebSocket push.
    procedure ApplyWsUnitMapData(aData: TJSArray);
    procedure ApplyWsComplaintMapData(aData: TJSArray);
  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: TJSObject;
  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;

    // --- Place markers (BeginUpdate wraps both so the map redraws once) ------
    lfMap.BeginUpdate;
    try
      PlaceUnitMarkers(unitsData);
      PlaceComplaintMarkers(complaintsData);
    finally
      lfMap.EndUpdate;
    end;

    if mapFilters <> nil then
      mapFilters.Apply;

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

procedure TFViewMap.PlaceUnitMarkers(aData: TJSArray);
var
  item: TJSObject;
  i: Integer;
  m: TTMSFNCMapsMarker;
  lat, lng: Double;
  uName, unitBadge, agencyType, agencyId, agencyName, agency: string;
  unitId, callType, priorityText, statusText, updateTimeText: string;
  officer1Lname, officer1Fname, officer1Empnum: string;
  officer2Lname, officer2Fname, officer2Empnum: string;
  officer1Display, officer2Display: string;
  iconUrl, canShowDetailsText, detailsBtnHtml: string;
  canShowDetails: Boolean;
begin
  // Remove stale unit markers
  for i := lfMap.Markers.Count - 1 downto 0 do
    if StartsText('unit|', lfMap.Markers[i].DataString) then
      lfMap.Markers.Delete(i);

  if aData = nil then
    Exit;

  for i := 0 to aData.Length - 1 do
  begin
    item := TJSObject(aData[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 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
      detailsBtnHtml :=
        '<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
          'onclick="window.showUnitDetails(''' + unitId + ''')">' +
          'Details' +
        '</button>'
    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.DataString := 'unit|' + unitId + '|' + StringReplace(unitBadge, '|', '/', [rfReplaceAll]);
    m.IconURL    := iconUrl;

    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>';
  end;
end;

procedure TFViewMap.PlaceComplaintMarkers(aData: TJSArray);
var
  item, uo: TJSObject;
  units: TJSArray;
  i, ui: Integer;
  m: TTMSFNCMapsMarker;
  lat, lng: Double;
  complaintId, codeDesc, priority, priorityBadge, complaintStatusKey: string;
  agencyId, agencyName, agency, business, address: string;
  pngName, iconUrl, rowsHtml: string;
begin
  // Remove stale complaint markers
  for i := lfMap.Markers.Count - 1 downto 0 do
    if StartsText('complaint|', lfMap.Markers[i].DataString) then
      lfMap.Markers.Delete(i);

  if aData = nil then
    Exit;

  for i := 0 to aData.Length - 1 do
  begin
    item := TJSObject(aData[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;

// --- WebSocket push entry points -------------------------------------------

procedure TFViewMap.ApplyWsUnitMapData(aData: TJSArray);
begin
  // Skip if the map is still initialising or an HTTP load is in flight.
  if (not Assigned(mapFilters)) or FLoadingPoints then
    Exit;

  lfMap.BeginUpdate;
  try
    PlaceUnitMarkers(aData);
  finally
    lfMap.EndUpdate;
  end;

  mapFilters.Apply;
  ApplyPendingUnitFocus;
end;

procedure TFViewMap.ApplyWsComplaintMapData(aData: TJSArray);
begin
  if (not Assigned(mapFilters)) or FLoadingPoints then
    Exit;

  lfMap.BeginUpdate;
  try
    PlaceComplaintMarkers(aData);
  finally
    lfMap.EndUpdate;
  end;

  mapFilters.Apply;
  ApplyPendingComplaintFocus;
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.

