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;
    tmrZoomToBounds: 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 tmrZoomToBoundsTimer(Sender: TObject);
  private
    FUnitsLoaded: Boolean;
    FComplaintsLoaded: Boolean;
    FZoomPending: 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;
  {$IFNDEF WIN32}
  asm
    window.showComplaintDetails = function (id) {
      console.log('JS bridge showComplaintDetails called, id=', id);
      try {
        pas['View.Main'].FViewMain.ShowComplaintDetails(id);
      } catch (e) {
        console.log('Error in TFViewMain.ShowComplaintDetails', e);
      }
    };

    window.showUnitDetails = function (id) {
      console.log('JS bridge showUnitDetails called, id=', id);
      try {
        pas['View.Main'].FViewMain.ShowUnitDetails(id);
      } catch (e) {
        console.log('Error in TFViewMain.ShowUnitDetails', e);
      }
    };
  end;
  {$ENDIF}
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
    begin
      FZoomPending := True;
      tmrZoomToBounds.Enabled := True;
    end;
  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, uo: TJSObject;
  data, units: TJSArray;
  i, ui: Integer;
  m: TTMSFNCMapsMarker;
  lat, lng: Double;
  uName, dist: string;
  unitId, callType, priorityText, statusText: string;
  complaintId, codeDesc, dispatchDist, priority: string;
  pngName, iconUrl, rowsHtml: string;
  officer1Lname, officer1Fname, officer1Empnum: string;
  officer2Lname, officer2Fname, officer2Empnum: string;
  officer1Display, officer2Display: string;
  updateTimeText: string;
begin
  ShowSpinner('spinner');
  FUnitsLoaded := False;
  FComplaintsLoaded := False;
  // --- Units ---------------------------------------------------------------
  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']);
          unitId := string(item['UnitId']);
          callType := string(item['CallType']);
          priorityText := string(item['Priority']);
          statusText := string(item['Status']);

          updateTimeText := string(item['UpdateTime']);

          officer1Lname := string(item['Officer1Lname']);
          officer1Fname := string(item['Officer1Fname']);
          officer1Empnum := string(item['Officer1Empnum']);

          officer2Lname := string(item['Officer2Lname']);
          officer2Fname := string(item['Officer2Fname']);
          officer2Empnum := string(item['Officer2Empnum']);

          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 :=
        '<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(dist <> '',
            '<div class="small"><span class="fw-bold">District:</span> ' + dist + '</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>'
          ) +

          '<div class="d-flex justify-content-end mt-0">' +
            '<button type="button" class="btn btn-primary btn-sm px-2 py-1" ' +
              'onclick="window.showUnitDetails(''' + unitId + ''')">' +
              'Details' +
            '</button>' +
          '</div>' +
        '</div>';

          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;

  // --- Complaints ----------------------------------------------------------
  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';

          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>' + string(uo['Unit'])    + '</td>' +
                  '<td>' + string(uo['Status'])  + '</td>' +
                  '<td>' + string(uo['Updated']) + '</td>' +
                '</tr>';
            end;
          end
          else
            rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';

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

          m.Title :=
            '<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">Dispatch District:</span> ' + dispatchDist +
              '</div>' +
              '<div class="small mb-1">' +
                '<span class="fw-bold">Address:</span> ' + string(item['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>';


          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 +
    'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 +
    'o.tooltipHtml = rawTitle;' + #13#10 +
    'o.title = "";' + #13#10 +
    '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 { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
    'try { if (m.unbindPopup)   m.unbindPopup();   } catch(e) {}' + #13#10 +

    // Note: we derive badgeText from icon filename suffix: *_2.png, *_3.png, *_4.png, *_5-9.png, etc.
    'var badgeText = "";' + #13#10 +
    'if (u) {' + #13#10 +
    '  try {' + #13#10 +
    '    var file = u.split("/").pop();' + #13#10 +
    '    var base = file.replace(/\.[^/.]+$/, "");' + #13#10 +
    '    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") badgeText = ch;' + #13#10 +
    '      }' + #13#10 +
    '    }' + #13#10 +
    '  } catch(e) {}' + #13#10 +
    '}' + #13#10 +

    'var badgeColor = "#4b5563";' + #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 +

    'if (u) {' + #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 +
    '  try { if (m._icon) m._icon.removeAttribute("title"); } catch(e) {}' + #13#10 +
    '}' + #13#10 +

    'm.bindPopup(t, {' + #13#10 +
    '  className: "emi-tip",' + #13#10 +
    '  maxWidth: 260,' + #13#10 +
    '  closeButton: false,' + #13#10 +
    '  autoPan: true' + #13#10 +
    '});';
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 +
  // --- 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-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;}';
end;

procedure TFViewMap.tmrZoomToBoundsTimer(Sender: TObject);
begin
  tmrZoomToBounds.Enabled := False;

  if not FZoomPending then
    Exit;

  FZoomPending := False;

  if lfMap.Polygons.Count = 0 then
    Exit;

  // Note: Delaying avoids Leaflet moveend firing while the internal map is still null.
  lfMap.ZoomToBounds(lfMap.Polygons.ToCoordinateArray);
end;

end.

