Commit 5c3199e6 by Mac Stephens

Add user location and filtering functionality, Improve map marker handling fix…

Add user location and filtering functionality, Improve map marker handling fix list scrolling and details navigation; default detail summaries expanded; add dynamic main navbar title.
parent dc203ba1
[Settings] [Settings]
LogFileNum=579 LogFileNum=583
webClientVersion=0.1.0 webClientVersion=0.1.0
...@@ -2,31 +2,6 @@ ...@@ -2,31 +2,6 @@
<!-- Header / controls (non-scrolling) --> <!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<!-- Local navbar (Complaints) -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="complaints_title" class="navbar-brand mb-0 h5 text-white">Complaints</span>
</div>
<div class="col">
<button id="complaints_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="complaints_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="complaints_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"> <div class="bg-light border-bottom py-2">
......
...@@ -113,46 +113,25 @@ begin ...@@ -113,46 +113,25 @@ begin
if FLoading then Exit; if FLoading then Exit;
FLoading := True; FLoading := True;
console.log('GetComplaints: Invoking API...');
try try
try try
xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', [])); xdcResponse := await(xdwcComplaints.RawInvokeAsync('IApiService.GetComplaintList', []));
console.log('RawInvoke returned:', xdcResponse.Result);
respObj := TJSObject(xdcResponse.Result); respObj := TJSObject(xdcResponse.Result);
xdwdsComplaints.Close; xdwdsComplaints.Close;
console.log('Dataset closed');
xdwdsComplaints.SetJsonData(respObj['data']); xdwdsComplaints.SetJsonData(respObj['data']);
console.log('JsonData set on dataset:', respObj['data']);
xdwdsComplaints.Open; xdwdsComplaints.Open;
console.log('PriorityColor field name = ' +
xdwdsComplaintsPriorityColor.FieldName +
' sample value = ' +
xdwdsComplaintsPriorityColor.AsString);
if xdwdsComplaints.RecordCount > 0 then
begin
console.log('First record - Complaint:' + xdwdsComplaints.FieldByName('Complaint').AsString);
end;
complaintsCount := Integer(respObj['count']); complaintsCount := Integer(respObj['count']);
lblEntries.Caption := Format('%d active complaints', [complaintsCount]); lblEntries.Caption := Format('%d active complaints', [complaintsCount]);
console.log('Label updated:' + lblEntries.Caption);
except except
on E: EXDataClientRequestException do on E: EXDataClientRequestException do
begin begin
console.log('XData exception:' + E.ErrorResult.ErrorMessage);
Utils.ShowErrorModal(E.ErrorResult.ErrorMessage); Utils.ShowErrorModal(E.ErrorResult.ErrorMessage);
end; end;
end; end;
finally finally
console.log('GetComplaints complete');
end; end;
HideSpinner('spinner'); HideSpinner('spinner');
end; end;
...@@ -161,7 +140,6 @@ end; ...@@ -161,7 +140,6 @@ end;
procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject); procedure TFViewComplaints.tmrRefreshTimer(Sender: TObject);
begin begin
GetComplaints; GetComplaints;
console.log('tmrRefreshTimer fired');
end; end;
end. end.
......
...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain ...@@ -77,6 +77,17 @@ object FViewMain: TFViewMain
OnClick = lblUsersClick OnClick = lblUsersClick
Caption = 'Users' Caption = 'Users'
end end
object lblMainTitle: TWebLabel
Left = 131
Top = 31
Width = 61
Height = 15
ElementID = 'lbl_main_title'
ElementFont = efCSS
HeightStyle = ssAuto
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object WebPanel1: TWebPanel object WebPanel1: TWebPanel
Left = 136 Left = 136
Top = 110 Top = 110
......
...@@ -3,17 +3,17 @@ ...@@ -3,17 +3,17 @@
<!-- Top Nav --> <!-- Top Nav -->
<nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0"> <nav class="navbar navbar-light bg-primary border-light text-light py-2 flex-shrink-0">
<div class="container-fluid"> <div class="container-fluid">
<!-- Left: Font button -->
<button id="view.main.btnfont" type="button" class="btn btn-outline-primary text-light border-light btn-sm">
font
</button>
<!-- Center: App title --> <!-- App title + current view title -->
<a id="view.main.apptitle" class="navbar-brand text-light ms-3" href="index.html">emiMobile</a> <div class="d-flex align-items-center ms-3">
<a id="view.main.apptitle" class="navbar-brand fw-bold text-light mb-0 me-1" href="index.html">emiMobile</a>
<span class="navbar-brand text-light mb-0 mx-0">-</span>
<span id="lbl_main_title" class="navbar-brand text-light mb-0 ms-1"></span>
</div>
<!-- Right: Connection label --> <!-- Right: Connection label -->
<span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span> <span id="view.main.lblconnection" class="navbar-text text-light ms-auto"></span>
</div> </div>
</nav> </nav>
<!-- Main content: fills space between navbars --> <!-- Main content: fills space between navbars -->
......
...@@ -24,6 +24,7 @@ type ...@@ -24,6 +24,7 @@ type
btnComplaints: TWebButton; btnComplaints: TWebButton;
btnUnits: TWebButton; btnUnits: TWebButton;
tmrBadgeCounts: TWebTimer; tmrBadgeCounts: TWebTimer;
lblMainTitle: TWebLabel;
procedure WebFormCreate(Sender: TObject); procedure WebFormCreate(Sender: TObject);
procedure mnuLogoutClick(Sender: TObject); procedure mnuLogoutClick(Sender: TObject);
procedure wllblUserProfileClick(Sender: TObject); procedure wllblUserProfileClick(Sender: TObject);
...@@ -90,16 +91,18 @@ begin ...@@ -90,16 +91,18 @@ begin
lblUsers.Visible := false; lblUsers.Visible := false;
Utils.HideSpinner('spinner'); Utils.HideSpinner('spinner');
ShowForm(TFViewMap); ShowForm(TFViewMap);
SetHeaderTitle('Map');
RefreshBadgesAsync; RefreshBadgesAsync;
end; end;
procedure TFViewMain.SetHeaderTitle(const title: string); procedure TFViewMain.SetHeaderTitle(const title: string);
var var el: TJSElement;
el: TJSElement;
begin begin
el := Document.getElementById('view.main.lbltitle'); el := Document.getElementById('lbl_main_title');
if el <> nil then if el <> nil then
TJSHtmlElement(el).innerText := title; el.innerHTML := title
else
console.log('SetHeaderTitle: lbl_main_title not found');
end; end;
......
...@@ -2,72 +2,6 @@ object FViewMap: TFViewMap ...@@ -2,72 +2,6 @@ object FViewMap: TFViewMap
Width = 475 Width = 475
Height = 802 Height = 802
ElementFont = efCSS ElementFont = efCSS
object btnMenu: TWebButton
Left = 62
Top = 66
Width = 41
Height = 25
Caption = 'Menu'
ChildOrder = 1
ElementID = 'map.btnmenu'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnAlerts: TWebButton
Left = 148
Top = 66
Width = 35
Height = 25
Caption = 'Alerts'
ChildOrder = 1
ElementID = 'map.btnalerts'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnGroups: TWebButton
Left = 194
Top = 66
Width = 41
Height = 25
Caption = 'Groups'
ChildOrder = 1
ElementID = 'map.btngroups'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnLocate: TWebButton
Left = 246
Top = 66
Width = 39
Height = 25
Caption = 'Locate'
ChildOrder = 1
ElementID = 'map.btnlocate'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnFilters: TWebButton
Left = 297
Top = 66
Width = 35
Height = 25
Caption = 'Filters'
ChildOrder = 1
ElementID = 'map.btnfilters'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object btnDisplay: TWebButton
Left = 351
Top = 66
Width = 40
Height = 25
Caption = 'Display'
ChildOrder = 1
ElementID = 'map.btndisplay'
HeightPercent = 100.000000000000000000
WidthPercent = 100.000000000000000000
end
object pnlMap: TWebPanel object pnlMap: TWebPanel
Left = 62 Left = 62
Top = 120 Top = 120
...@@ -76,7 +10,7 @@ object FViewMap: TFViewMap ...@@ -76,7 +10,7 @@ object FViewMap: TFViewMap
ElementID = 'map_pnlmap' ElementID = 'map_pnlmap'
ChildOrder = 7 ChildOrder = 7
ElementPosition = epIgnore ElementPosition = epIgnore
TabOrder = 6 TabOrder = 0
object lfMap: TTMSFNCLeaflet object lfMap: TTMSFNCLeaflet
Left = 0 Left = 0
Top = 0 Top = 0
......
<div id="map.root" class="d-flex flex-column h-100 w-100"> <div id="map.root" class="d-flex flex-column h-100 w-100">
<nav class="d-flex gap-2 bg-primary p-2 overflow-x-auto border-bottom border-primary shadow-sm flex-shrink-0"> <!-- New: Offcanvas -->
<div class="offcanvas offcanvas-top"
tabindex="-1"
id="map_filters_offcanvas"
aria-labelledby="map_filters_offcanvas_label"
style="--bs-offcanvas-width: 280px;">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="map_filters_offcanvas_label">Map Filters</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<button id="map.btnmenu" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <div class="offcanvas-body">
<i class="fa fa-bars me-2"></i><span class="d-none d-sm-inline">Menu</span> <div class="mb-3">
</button> <div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_units" checked>
<label class="form-check-label" for="map_filter_units">Show Units</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="map_filter_complaints" checked>
<label class="form-check-label" for="map_filter_complaints">Show Complaints</label>
</div>
</div>
<button id="map.btnalerts" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <div class="d-grid gap-2 mt-4">
<i class="fa fa-exclamation-circle me-2"></i><span class="d-none d-sm-inline">Alerts</span> <button type="button" class="btn btn-primary" id="map_filters_apply" data-bs-dismiss="offcanvas">
Apply
</button> </button>
<button type="button" class="btn btn-outline-secondary" id="map_filters_reset">
<button id="map.btngroups" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> Reset
<i class="fa fa-users me-2"></i><span class="d-none d-sm-inline">Groups</span>
</button> </button>
</div>
<button id="map.btnlocate" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <div class="mt-3 small text-muted">
<i class="fa fa-location-arrow me-2"></i><span class="d-none d-sm-inline">Locate</span> <span class="fw-semibold">Active:</span>
</button> <span id="map_filters_summary"></span>
</div>
</div>
</div>
<button id="map.btnfilters" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <div class="flex-grow-1 position-relative" style="min-height:0;">
<i class="fa fa-sliders-h me-2"></i><span class="d-none d-sm-inline">Filter</span> <div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div>
</button>
<button id="map.btndisplay" type="button" class="btn btn-primary d-flex align-items-center justify-content-center flex-fill"> <button id="btn_map_filters"
<i class="fa fa-sun me-2"></i><span class="d-none d-sm-inline">Display</span> type="button"
class="btn btn-primary position-absolute top-0 end-0 m-2 shadow"
style="z-index:1000;"
data-bs-toggle="offcanvas"
data-bs-target="#map_filters_offcanvas"
aria-controls="map_filters_offcanvas">
<i class="fa fa-sliders-h me-1"></i>Filters
</button> </button>
</nav>
<div class="flex-grow-1 position-relative" style="min-height: 0;">
<div id="map_pnlmap" class="position-absolute w-100 h-100 top-0 start-0"></div>
</div> </div>
</div> </div>
...@@ -8,16 +8,10 @@ uses ...@@ -8,16 +8,10 @@ uses
WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils, WEBLib.ExtCtrls, DB, WEBLib.WebCtrls, WEBLib.REST, VCL.TMSFNCTypes, VCL.TMSFNCUtils,
VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser, VCL.TMSFNCGraphics, VCL.TMSFNCGraphicsTypes, VCL.TMSFNCCustomControl, VCL.TMSFNCWebBrowser,
VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client, VCL.TMSFNCMaps, VCL.TMSFNCLeaflet, VCL.TMSFNCMapsCommonTypes, System.StrUtils, XData.Web.Client,
XData.Web.Connection, ConnectionModule, Utils; XData.Web.Connection, ConnectionModule, Utils, uMapFilters;
type type
TFViewMap = class(TWebForm) TFViewMap = class(TWebForm)
btnMenu: TWebButton;
btnAlerts: TWebButton;
btnGroups: TWebButton;
btnLocate: TWebButton;
btnFilters: TWebButton;
btnDisplay: TWebButton;
pnlMap: TWebPanel; pnlMap: TWebPanel;
lfMap: TTMSFNCLeaflet; lfMap: TTMSFNCLeaflet;
httpReqGeoJson: TWebHttpRequest; httpReqGeoJson: TWebHttpRequest;
...@@ -31,12 +25,18 @@ type ...@@ -31,12 +25,18 @@ type
procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string); procedure lfMapCustomizeCSS(Sender: TObject; var ACustomizeCSS: string);
procedure tmrRefreshTimer(Sender: TObject); procedure tmrRefreshTimer(Sender: TObject);
private private
userLocationMarker: TTMSFNCMapsMarker;
geoWatchId: Integer;
FUnitsLoaded: Boolean; FUnitsLoaded: Boolean;
FComplaintsLoaded: Boolean; FComplaintsLoaded: Boolean;
FZoomPending: Boolean; FZoomPending: Boolean;
FLoadingPoints: Boolean; FLoadingPoints: Boolean;
mapFilters: TMapFilters;
[async] procedure LoadPointsAsync; [async] procedure LoadPointsAsync;
function CarIconForDistrict(const DistrictCode: string): string; function CarIconForDistrict(const DistrictCode: string): string;
procedure UpdateDeviceLocation(lat, lng: Double);
procedure StartDeviceLocation;
procedure StopDeviceLocation;
public public
end; end;
...@@ -77,9 +77,82 @@ begin ...@@ -77,9 +77,82 @@ begin
}; };
end; end;
{$ENDIF} {$ENDIF}
StartDeviceLocation;
end;
procedure TFViewMap.UpdateDeviceLocation(lat, lng: Double);
begin
if userLocationMarker = nil then
begin
userLocationMarker := lfMap.Markers.Add;
userLocationMarker.DataString := 'device';
userLocationMarker.IconURL := 'assets/markers/location_dot.png';
userLocationMarker.Title := ''; // keeps popup code from binding anything for this marker
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; end;
procedure TFViewMap.StopDeviceLocation;
begin
asm
const self = this;
if (self.geoWatchId != null && self.geoWatchId !== 0 && navigator.geolocation) {
try { navigator.geolocation.clearWatch(self.geoWatchId); } catch(e) {}
self.geoWatchId = 0;
}
end;
end;
[async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string); [async] procedure TFViewMap.httpReqGeoJsonResponse(Sender: TObject; AResponse: string);
var var
i: Integer; i: Integer;
...@@ -136,6 +209,12 @@ begin ...@@ -136,6 +209,12 @@ begin
end; end;
await(LoadPointsAsync); await(LoadPointsAsync);
if mapFilters = nil then
begin
mapFilters := TMapFilters.Create(lfMap);
mapFilters.Init;
end;
end; end;
function TFViewMap.CarIconForDistrict(const DistrictCode: string): string; function TFViewMap.CarIconForDistrict(const DistrictCode: string): string;
...@@ -162,7 +241,7 @@ end; ...@@ -162,7 +241,7 @@ end;
var var
resp: TXDataClientResponse; resp: TXDataClientResponse;
root, item, uo: TJSObject; root, item, uo: TJSObject;
data, units: TJSArray; units: TJSArray;
i, ui: Integer; i, ui: Integer;
m: TTMSFNCMapsMarker; m: TTMSFNCMapsMarker;
lat, lng: Double; lat, lng: Double;
...@@ -177,23 +256,61 @@ var ...@@ -177,23 +256,61 @@ var
canShowDetails: Boolean; canShowDetails: Boolean;
canShowDetailsText: string; canShowDetailsText: string;
detailsBtnHtml: string; detailsBtnHtml: string;
unitsData: TJSArray;
complaintsData: TJSArray;
begin begin
if FLoadingPoints then
Exit;
FLoadingPoints := True;
ShowSpinner('spinner'); ShowSpinner('spinner');
try
FUnitsLoaded := False; FUnitsLoaded := False;
FComplaintsLoaded := False; FComplaintsLoaded := False;
// --- Units ---------------------------------------------------------------
unitsData := nil;
complaintsData := nil;
// --- Fetch Units ---------------------------------------------------------
try try
resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', [])); resp := await(xdwcMap.RawInvokeAsync('IApiService.GetUnitMap', []));
root := TJSObject(resp.Result); root := TJSObject(resp.Result);
data := TJSArray(root['data']); unitsData := TJSArray(root['data']);
FUnitsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Units XData error: ' + E.ErrorResult.ErrorMessage);
end;
if data <> nil then // --- Fetch Complaints ----------------------------------------------------
begin 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; lfMap.BeginUpdate;
try try
for i := 0 to data.Length - 1 do // Delete old unit/complaint markers right before adding new ones
for i := lfMap.Markers.Count - 1 downto 0 do
begin
m := lfMap.Markers[i];
if SameText(m.DataString, 'unit') or StartsText('complaint|', m.DataString) then
lfMap.Markers.Delete(i);
end;
// Add unit markers
if unitsData <> nil then
begin begin
item := TJSObject(data[i]); for i := 0 to unitsData.Length - 1 do
begin
item := TJSObject(unitsData[i]);
lat := Double(item['Lat']); lat := Double(item['Lat']);
lng := Double(item['Lng']); lng := Double(item['Lng']);
...@@ -203,7 +320,6 @@ begin ...@@ -203,7 +320,6 @@ begin
callType := string(item['CallType']); callType := string(item['CallType']);
priorityText := string(item['Priority']); priorityText := string(item['Priority']);
statusText := string(item['Status']); statusText := string(item['Status']);
updateTimeText := string(item['UpdateTime']); updateTimeText := string(item['UpdateTime']);
officer1Lname := string(item['Officer1Lname']); officer1Lname := string(item['Officer1Lname']);
...@@ -228,9 +344,7 @@ begin ...@@ -228,9 +344,7 @@ begin
'</button>'; '</button>';
end end
else else
begin
detailsBtnHtml := ''; detailsBtnHtml := '';
end;
officer1Display := ''; officer1Display := '';
if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then if Trim(officer1Lname + officer1Fname + officer1Empnum) <> '' then
...@@ -298,30 +412,14 @@ begin ...@@ -298,30 +412,14 @@ begin
m.DataString := 'unit'; m.DataString := 'unit';
m.IconURL := CarIconForDistrict(dist); m.IconURL := CarIconForDistrict(dist);
end; end;
finally
lfMap.EndUpdate;
end;
end; end;
FUnitsLoaded := True; // Add complaint markers
except if complaintsData <> nil then
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 begin
lfMap.BeginUpdate; for i := 0 to complaintsData.Length - 1 do
try
for i := 0 to data.Length - 1 do
begin begin
item := TJSObject(data[i]); item := TJSObject(complaintsData[i]);
complaintId := string(item['ComplaintId']); complaintId := string(item['ComplaintId']);
codeDesc := string(item['DispatchCodeDesc']); codeDesc := string(item['DispatchCodeDesc']);
...@@ -360,11 +458,13 @@ begin ...@@ -360,11 +458,13 @@ begin
else else
rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>'; rowsHtml := '<tr><td colspan="3" class="text-muted">No units</td></tr>';
// Complaint Markers
m := lfMap.Markers.Add; m := lfMap.Markers.Add;
m.Latitude := lat; m.Latitude := lat;
m.Longitude := lng; m.Longitude := lng;
m.DataString := 'complaint|' + complaintId;
m.IconURL := iconUrl;
m.Title := m.Title :=
'<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' + '<div class="d-flex flex-column gap-1 px-1 py-1" style="width:260px;">' +
'<div class="fw-semibold small">' + '<div class="fw-semibold small">' +
...@@ -404,30 +504,33 @@ begin ...@@ -404,30 +504,33 @@ begin
'</button>' + '</button>' +
'</div>' + '</div>' +
'</div>'; '</div>';
end;
m.IconURL := iconUrl;
end; end;
finally finally
lfMap.EndUpdate; lfMap.EndUpdate;
end; end;
end;
FComplaintsLoaded := True;
except
on E: EXDataClientRequestException do
Console.Log('Complaints XData error: ' + E.ErrorResult.ErrorMessage);
end;
if mapFilters <> nil then
mapFilters.Apply;
finally
HideSpinner('spinner'); HideSpinner('spinner');
FLoadingPoints := False;
end;
end; end;
procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string); procedure TFViewMap.lfMapCustomizeMarker(Sender: TObject; var ACustomizeMarker: string);
begin begin
ACustomizeMarker := ACustomizeMarker :=
'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 + 'var m=' + MARKERVAR + ', o=m.options||{};' + #13#10 +
'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 + 'var rawTitle = (o && o.title) ? o.title : "";' + #13#10 +
'var ds = (o && o.datastring) ? o.datastring : "";' + #13#10 +
'if (ds === "device") {' + #13#10 +
' try { if (m.unbindTooltip) m.unbindTooltip(); } catch(e) {}' + #13#10 +
' try { if (m.unbindPopup) m.unbindPopup(); } catch(e) {}' + #13#10 +
' return;' + #13#10 +
'}' + #13#10 +
'o.tooltipHtml = rawTitle;' + #13#10 + 'o.tooltipHtml = rawTitle;' + #13#10 +
'o.title = "";' + #13#10 + 'o.title = "";' + #13#10 +
'var t = o.tooltipHtml || "";' + #13#10 + 'var t = o.tooltipHtml || "";' + #13#10 +
...@@ -521,7 +624,7 @@ begin ...@@ -521,7 +624,7 @@ begin
// --- Marker Badge ------------------------------------------------------------ // --- Marker Badge ------------------------------------------------------------
'.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+ '.emi-marker-wrap{position:relative;display:inline-block;}'+#13#10+
'.emi-marker-img{display: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;}'; '.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;
end; end;
procedure TFViewMap.tmrRefreshTimer(Sender: TObject); procedure TFViewMap.tmrRefreshTimer(Sender: TObject);
......
...@@ -61,10 +61,6 @@ ...@@ -61,10 +61,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -2,31 +2,6 @@ ...@@ -2,31 +2,6 @@
<!-- Header / controls (non-scrolling) --> <!-- Header / controls (non-scrolling) -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<!-- Local navbar (Units) -->
<nav class="navbar navbar-dark bg-primary py-2">
<div class="container-fluid">
<div class="row w-100 g-2 align-items-stretch">
<div class="col">
<span id="units_title" class="navbar-brand mb-0 h5 text-white">Units</span>
</div>
<div class="col">
<button id="units_btnrefresh" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sync-alt me-1"></i><span class="d-none d-sm-inline">Refresh</span>
</button>
</div>
<div class="col">
<button id="units_btngroup" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-layer-group me-1"></i><span class="d-none d-sm-inline">Group</span>
</button>
</div>
<div class="col">
<button id="units_btnfilter" type="button" class="btn btn-primary w-100 h-100">
<i class="fa fa-sliders-h me-1"></i><span class="d-none d-sm-inline">Filter</span>
</button>
</div>
</div>
</div>
</nav>
<!-- Search bar under local navbar --> <!-- Search bar under local navbar -->
<div class="bg-light border-bottom py-2"> <div class="bg-light border-bottom py-2">
......
unit uMapFilters;
interface
uses
System.SysUtils, System.StrUtils,
JS, Web,
VCL.TMSFNCLeaflet, VCL.TMSFNCMaps, VCL.TMSFNCMapsCommonTypes;
type
TMapFilters = class
private
FMap: TTMSFNCLeaflet;
FShowUnits: Boolean;
FShowComplaints: Boolean;
function GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
procedure SetCheckBoxChecked(const elementId: string; checked: Boolean);
procedure SetText(const elementId: string; const value: string);
procedure ReadUi;
procedure ResetUi;
procedure UpdateSummary;
procedure ApplyToMap;
procedure HandleDocClick(e: TJSMouseEvent);
procedure HandleDocChange(e: TJSEvent);
public
constructor Create(AMap: TTMSFNCLeaflet);
procedure Init;
procedure Apply;
procedure Reset;
end;
implementation
{ TMapFilters }
constructor TMapFilters.Create(AMap: TTMSFNCLeaflet);
begin
inherited Create;
FMap := AMap;
FShowUnits := True;
FShowComplaints := True;
end;
procedure TMapFilters.Init;
begin
Document.addEventListener('click', @HandleDocClick);
Document.addEventListener('change', @HandleDocChange);
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Apply;
begin
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.Reset;
begin
ResetUi;
ReadUi;
UpdateSummary;
ApplyToMap;
end;
procedure TMapFilters.HandleDocClick(e: TJSMouseEvent);
var
el: TJSElement;
id: string;
begin
el := TJSElement(e.target);
if el = nil then
Exit;
// Note: This supports clicking inner icons/spans by walking up to a parent with an id
asm
while (el && !el.id) { el = el.parentElement; }
end;
if (el <> nil) and (el is TJSHtmlElement) then
begin
id := string(TJSHtmlElement(el).id);
if id = 'map_filters_apply' then
begin
e.preventDefault;
e.stopPropagation;
Apply;
Exit;
end;
if id = 'map_filters_reset' then
begin
e.preventDefault;
e.stopPropagation;
Reset;
Exit;
end;
end;
end;
procedure TMapFilters.HandleDocChange(e: TJSEvent);
begin
//Note: Summary text changes when toggles change
ReadUi;
UpdateSummary;
end;
procedure TMapFilters.ReadUi;
begin
FShowUnits := GetCheckBoxChecked('map_filter_units', True);
FShowComplaints := GetCheckBoxChecked('map_filter_complaints', True);
end;
procedure TMapFilters.ResetUi;
begin
SetCheckBoxChecked('map_filter_units', True);
SetCheckBoxChecked('map_filter_complaints', True);
end;
procedure TMapFilters.UpdateSummary;
var
summaryText: string;
begin
summaryText := '';
if FShowUnits then
summaryText := 'Units';
if FShowComplaints then
begin
if summaryText <> '' then
summaryText := summaryText + ', ';
summaryText := summaryText + 'Complaints';
end;
if summaryText = '' then
summaryText := 'None';
SetText('map_filters_summary', summaryText);
end;
procedure TMapFilters.ApplyToMap;
var
i: Integer;
m: TTMSFNCMapsMarker;
ds: string;
showMarker: Boolean;
begin
if FMap = nil then
Exit;
FMap.BeginUpdate;
try
for i := 0 to FMap.Markers.Count - 1 do
begin
m := FMap.Markers[i];
ds := Trim(string(m.DataString));
// Note: Map form sets:
// - units: m.DataString := 'unit'
// - complaints: (currently none) but you can set m.DataString := 'complaint|<id>'
showMarker := True;
if SameText(ds, 'unit') then
showMarker := FShowUnits
else if StartsText('complaint', LowerCase(ds)) then
showMarker := FShowComplaints;
// Note: TTMSFNCMapsMarker supports visibility toggling
m.Visible := showMarker;
end;
finally
FMap.EndUpdate;
end;
end;
function TMapFilters.GetCheckBoxChecked(const elementId: string; defaultValue: Boolean): Boolean;
var
el: TJSElement;
begin
Result := defaultValue;
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
Result := TJSHtmlInputElement(el).checked;
end;
procedure TMapFilters.SetCheckBoxChecked(const elementId: string; checked: Boolean);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlInputElement) then
TJSHtmlInputElement(el).checked := checked;
end;
procedure TMapFilters.SetText(const elementId: string; const value: string);
var
el: TJSElement;
begin
el := Document.getElementById(elementId);
if (el <> nil) and (el is TJSHtmlElement) then
TJSHtmlElement(el).innerText := value;
end;
end.
...@@ -22,7 +22,8 @@ uses ...@@ -22,7 +22,8 @@ uses
Utils in 'Utils.pas', Utils in 'Utils.pas',
View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html}, View.ErrorPage in 'View.ErrorPage.pas' {FViewErrorPage: TWebForm} {*.html},
View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html}, View.ComplaintDetails in 'View.ComplaintDetails.pas' {FViewComplaintDetails: TWebForm} {*.html},
View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html}; View.UnitDetails in 'View.UnitDetails.pas' {FViewUnitDetails: TWebForm} {*.html},
uMapFilters in 'uMapFilters.pas';
{$R *.res} {$R *.res}
......
...@@ -186,6 +186,7 @@ ...@@ -186,6 +186,7 @@
<FormType>dfm</FormType> <FormType>dfm</FormType>
<DesignClass>TWebForm</DesignClass> <DesignClass>TWebForm</DesignClass>
</DCCReference> </DCCReference>
<DCCReference Include="uMapFilters.pas"/>
<None Include="index.html"/> <None Include="index.html"/>
<None Include="css\app.css"/> <None Include="css\app.css"/>
<None Include="css\spinner.css"/> <None Include="css\spinner.css"/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment