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;

type
  TFViewMap = class(TWebForm)
    btnMenu: TWebButton;
    btnAlerts: TWebButton;
    btnGroups: TWebButton;
    btnLocate: TWebButton;
    btnFilters: TWebButton;
    btnDisplay: TWebButton;
    pnlMap: TWebPanel;
    lfMap: TTMSFNCLeaflet;
    httpReqGeoJson: TWebHttpRequest;
    xdwcMap: TXDataWebClient;
    tmrRefresh: TWebTimer;

    procedure lfMapMapInitialized(Sender: TObject);
    [async] procedure httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
    procedure lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
    procedure lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
    procedure lfMapCustomizeMarker(Sender: TObject;
    var ACustomizeMarker: string);
    procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
  private
    FUnitsLoaded: Boolean;
    FComplaintsLoaded: Boolean;
    [async] procedure LoadPointsAsync;
    function CarIconForDistrict(const DistrictCode: string): string;
  public
  end;

var
  FViewMap: TFViewMap;

implementation

uses
  JS, Web;

{$R *.dfm}

procedure TFViewMap.lfMapMapInitialized(Sender: TObject);
begin
  ShowSpinner('spinner');
  FUnitsLoaded := False;
  FComplaintsLoaded := False;
  httpReqGeoJson.Execute;
end;


[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var
  i: Integer;
  P: TTMSFNCMapsPolygon;
  nm: string;
begin
  lfMap.BeginUpdate;
  try
    lfMap.Polygons.Clear;

    Console.Log('GeoJSON len=' + AResponse.Length.ToString);
    lfMap.LoadGeoJSONFromText(AResponse, True, False);
    Console.Log('Loaded polygons count=' + lfMap.Polygons.Count.ToString);

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

      case i of
        0: begin
             P.DisplayName := 'District A';
             P.FillColor   := HTMLToColor('#d3ffbe'); // light green
             P.StrokeColor := HTMLToColor('#6ea85c'); // darker green
           end;
        1: begin
             P.DisplayName := 'District B';
             P.FillColor   := HTMLToColor('#ffbebe'); // light red
             P.StrokeColor := HTMLToColor('#b34a4a'); // darker red
           end;
        2: begin
             P.DisplayName := 'District C';
             P.FillColor   := HTMLToColor('#ffd37f'); // light orange
             P.StrokeColor := HTMLToColor('#b36e00'); // darker orange
           end;
        3: begin
             P.DisplayName := 'District D';
             P.FillColor   := HTMLToColor('#bed2ff'); // light blue
             P.StrokeColor := HTMLToColor('#3c5ca8'); // darker blue
           end;
        4: begin
             P.DisplayName := 'District E';
             P.FillColor   := HTMLToColor('#ffffbe'); // light yellow
             P.StrokeColor := HTMLToColor('#a8a85c'); // darker yellow/olive
           end;
      end;

      P.FillOpacity := 0.60;
      P.StrokeOpacity := 1.0;
      P.StrokeWidth := 2;

    end;

    if lfMap.Polygons.Count > 0 then
      lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
  finally
    lfMap.EndUpdate;
  end;

  await(LoadPointsAsync);
end;

function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
var
  U: string;
  L: Char;
begin
  U := UpperCase(Trim(DistrictCode));

  if U = '' then
    Exit('assets/markers/car_default.png');

  L := U[1];
  case L of
    'A','B','C','D','E','X':
      Result := 'assets/markers/car_' + L + '.png';
  else
    Result := 'assets/markers/car_default.png';
  end;
end;


[async] procedure TFViewMap.LoadPointsAsync;
var
  resp: TXDataClientResponse;
  root, item: TJSObject;
  data: TJSArray;
  i: Integer;
  m: TTMSFNCMapsMarker;
  lat, lng: Double;
  uname, dist: string;
  complaintId, codeDesc, dispatchDist: string;
  pngName, iconUrl: string;
  priority: string;
begin
  ShowSpinner('spinner');
  FUnitsLoaded := False;
  FComplaintsLoaded := False;

  try
    resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
    root := TJSObject(resp.Result);
    data := TJSArray(root['data']);
    if data <> nil then
    begin
      lfMap.BeginUpdate;
      try
        for i := 0 to data.Length - 1 do
        begin
          item := TJSObject(data[i]);
          lat := Double(item['Lat']);
          lng := Double(item['Lng']);
          uname := string(item['UnitName']);
          dist := string(item['District']);

          m := lfMap.Markers.Add;
          m.Latitude := lat;
          m.Longitude := lng;
          m.Title := uname + IfThen(dist <> '', ' / ' + dist, '');
          m.DataString := 'unit';
          m.IconURL := CarIconForDistrict(dist);

        end;
      finally
        lfMap.EndUpdate;
      end;
    end;
    FUnitsLoaded := True;
  except
    on E: EXDataClientRequestException do
      Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
  end;

  try
    resp := await(xdwcMap.RawInvokeAsync('IApiService.GetComplaintMap', []));
    root := TJSObject(resp.Result);
    data := TJSArray(root['data']);
    if data <> nil then
    begin
      lfMap.BeginUpdate;
      try
        for i := 0 to data.Length - 1 do
        begin
          item        := TJSObject(data[i]);
          complaintId := string(item['ComplaintId']);
          codeDesc    := string(item['DispatchCodeDesc']);
          dispatchDist := string(item['DispatchDistrict']);
          priority    := string(item['Priority']);
          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 := string(item['pngName']);
          if Trim(pngName) <> '' then
            iconUrl := 'assets/markers/' + pngName
          else
            iconUrl := 'assets/markers/default_5-9.png';

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

          // Note: This is th HTML tooltip content: info + button
          m.Title :=
            '<div class="d-flex flex-column gap-1" style="min-width:180px;">' +
              '<div class="fw-semibold small">' +
                '<span class="fw-bold">Complaint ID:</span> ' + complaintId +
              '</div>' +
              '<div class="small">' +
                '<span class="fw-bold">Type:</span> ' + codeDesc +
              '</div>' +
              '<div class="small">' +
                '<span class="fw-bold">District:</span> ' + dispatchDist +
              '</div>' +
              '<div class="small mb-1">' +
                '<span class="fw-bold">Priority:</span> ' + priority +
              '</div>' +
              '<div class="d-flex justify-content-end mt-1">' +
                '<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
                'onclick="emiOpenComplaintDetails(''' + complaintId + ''')">' +
                  'Details' +
                '</button>' +
              '</div>' +
            '</div>';
          // Note: DataString not used for badges anymore, but keep for future
          // Note: m.DataString := 'complaint|' + complaintId;
          m.IconURL   := iconUrl;
        end;
      finally
        lfMap.EndUpdate;
      end;
    end;

    FComplaintsLoaded := True;
  except
    on E: EXDataClientRequestException do
      Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
  end;
    HideSpinner('spinner');
end;



procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
begin
  ACustomizeMarker :=
    'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
    // Grab the HTML title and move it off the native "title" attribute
    'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 +
    'o.tooltipHtml = rawTitle;' + #13#10 +      // store our HTML here
    'o.title = "";' + #13#10 +                  // prevent browser native tooltip
    'var t = o.tooltipHtml || "";' + #13#10 +
    'var u = (o.icon && o.icon.options && o.icon.options.iconUrl) ? o.icon.options.iconUrl : null;' + #13#10 +
    'try { m.unbindTooltip(); } catch(e) {}' + #13#10 +

    'if (!u) {' + #13#10 +
    '  m.bindTooltip(t, {' + #13#10 +
    '    className: "emi-tip",' + #13#10 +
    '    direction: "top",' + #13#10 +
    '    offset: [0,-28],' + #13#10 +
    '    sticky: true' + #13#10 +
    '  });' + #13#10 +
    '  return;' + #13#10 +
    '}' + #13#10 +

    // derive badgeText from the icon filename suffix: *_2.png, *_3.png, *_4.png, *_5-9.png, etc.
    'var badgeText = "";' + #13#10 +
    'try {' + #13#10 +
    '  var file = u.split("/").pop();' + #13#10 +
    '  var base = file.replace(/\.[^/.]+$/, "");' + #13#10 + // strip .png
    '  var idx  = base.lastIndexOf("_");' + #13#10 +
    '  if (idx >= 0 && idx < base.length - 1) {' + #13#10 +
    '    var suffix = base.substring(idx + 1);' + #13#10 +
    '    if (suffix && suffix.length > 0) {' + #13#10 +
    '      var ch = suffix.charAt(0);' + #13#10 +
    '      if (ch >= "1" && ch <= "9") {' + #13#10 +
    '        badgeText = ch;' + #13#10 +
    '      }' + #13#10 +
    '    }' + #13#10 +
    '  }' + #13#10 +
    '} catch(e) {}' + #13#10 +

    // map badgeText (priority) to background color
    'var badgeColor = "#4b5563"; // default for 5-9' + #13#10 +
    'if (badgeText === "1") badgeColor = "#bc28d9";' + #13#10 +
    'else if (badgeText === "2") badgeColor = "#b91c1c";' + #13#10 +
    'else if (badgeText === "3") badgeColor = "#b45309";' + #13#10 +
    'else if (badgeText === "4") badgeColor = "#047857";' + #13#10 +
    '// 5–9 remain #4b5563' + #13#10 +

    'var html = ''<div class="emi-marker-wrap"><img src="'' + u + ''" class="emi-marker-img">'';' + #13#10 +
    'if (badgeText) {' + #13#10 +
    '  html += ''<div class="emi-marker-badge" style="background:'' + badgeColor + '';">'' + badgeText + ''</div>'';' + #13#10 +
    '}' + #13#10 +
    'html += ''</div>'';' + #13#10 +

    'm.setIcon(L.divIcon({' + #13#10 +
    '  html: html,' + #13#10 +
    '  iconSize: [32,32],' + #13#10 +
    '  iconAnchor: [16,32],' + #13#10 +
    '  popupAnchor: [0,-20],' + #13#10 +
    '  className: ""' + #13#10 +
    '}));' + #13#10 +

    // extra safety: strip native title from the DOM element if present
    'try { if (m._icon) m._icon.removeAttribute("title"); } catch(e) {}' + #13#10 +

    'm.bindTooltip(t, {' + #13#10 +
    '  className: "emi-tip",' + #13#10 +
    '  direction: "top",' + #13#10 +
    '  offset: [0,-28],' + #13#10 +
    '  sticky: true,' + #13#10 +
    '  interactive: true' + #13#10 +
    '});';
end;



procedure TFViewMap.lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
begin
  ACustomizeCSS :=
    // Note: Tooltip Container
    '.leaflet-tooltip.emi-tip {' + #13#10 +
    '  background: rgba(245,245,245,0.99);' + #13#10 +
    '  color: #000;' + #13#10 +
    '  border-radius: 8px;' + #13#10 +
    '  padding: 10px 12px;' + #13#10 +
    '  border: 1px solid rgba(0,0,0,.20);' + #13#10 +
    '  box-shadow: 0 4px 14px rgba(0,0,0,.25);' + #13#10 +
    '  pointer-events: auto;' + #13#10 +
    '}' + #13#10 +

    // Note: Tooltip Arrow Colors
    '.leaflet-tooltip-top.emi-tip:before    { border-top-color:    rgba(245,245,245,0.92); }' + #13#10 +
    '.leaflet-tooltip-bottom.emi-tip:before { border-bottom-color: rgba(245,245,245,0.92); }' + #13#10 +
    '.leaflet-tooltip-left.emi-tip:before   { border-left-color:   rgba(245,245,245,0.92); }' + #13#10 +
    '.leaflet-tooltip-right.emi-tip:before  { border-right-color:  rgba(245,245,245,0.92); }' + #13#10 +

    // Note: Marker Badge Wrap
    '.emi-marker-wrap { position: relative; display: inline-block; }' + #13#10 +
    '.emi-marker-img { display: block; }' + #13#10 +
    '.emi-marker-badge {' + #13#10 +
    '  position: absolute;' + #13#10 +
    '  top: -4px;' + #13#10 +
    '  right: -4px;' + #13#10 +
    '  min-width: 16px;' + #13#10 +
    '  height: 16px;' + #13#10 +
    '  padding: 0 4px;' + #13#10 +
    '  border-radius: 999px;' + #13#10 +
    '  background: var(--bs-danger);' + #13#10 +
    '  color: #fff;' + #13#10 +
    '  font: 700 11px/16px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;' + #13#10 +
    '  text-align: center;' + #13#10 +
    '  box-shadow: 0 0 0 2px #fff;' + #13#10 +
    '}';
end;


procedure TFViewMap.lfMapPolyElementMouseEnter(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
begin
  if AElement is TTMSFNCMapsPolygon then
  begin
    TTMSFNCMapsPolygon(AElement).StrokeWidth := 3;
    TTMSFNCMapsPolygon(AElement).FillOpacity := 0.55;
  end;
end;


procedure TFViewMap.lfMapPolyElementMouseLeave(Sender: TObject; AElement: TTMSFNCMapsPolyElement);
begin
  if AElement is TTMSFNCMapsPolygon then
  begin
    TTMSFNCMapsPolygon(AElement).StrokeWidth := 2;
    TTMSFNCMapsPolygon(AElement).FillOpacity := 0.42;
  end;
end;

end.

