@memberjunction/ng-map-view 5.28.0 → 5.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/map-view.component.d.ts +12 -78
- package/dist/lib/map-view.component.d.ts.map +1 -1
- package/dist/lib/map-view.component.js +123 -463
- package/dist/lib/map-view.component.js.map +1 -1
- package/dist/lib/map-view.types.d.ts +1 -1
- package/dist/lib/map-view.types.d.ts.map +1 -1
- package/dist/lib/map-view.types.js.map +1 -1
- package/package.json +6 -5
|
@@ -11,9 +11,11 @@ import * as i0 from "@angular/core";
|
|
|
11
11
|
* Supports three rendering modes:
|
|
12
12
|
* - **point** (default): Individual markers with optional clustering
|
|
13
13
|
* - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)
|
|
14
|
-
* - **heatmap**: Density visualization
|
|
14
|
+
* - **heatmap**: Density visualization
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Delegates all rendering logic to @memberjunction/geo-maps.
|
|
17
|
+
* This component is a thin Angular wrapper managing lifecycle, inputs/outputs,
|
|
18
|
+
* and IntersectionObserver-based deferred initialization.
|
|
17
19
|
*/
|
|
18
20
|
export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
|
|
19
21
|
private cdr;
|
|
@@ -28,6 +30,8 @@ export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestro
|
|
|
28
30
|
LongitudeField: string;
|
|
29
31
|
/** Current render mode. */
|
|
30
32
|
RenderMode: MapRenderMode;
|
|
33
|
+
/** Field name containing GeoJSON boundary data on each record (for boundary mode). */
|
|
34
|
+
BoundaryField: string;
|
|
31
35
|
/** Persisted display state from UserView. */
|
|
32
36
|
DisplayState: Partial<MapDisplayState> | null;
|
|
33
37
|
/**
|
|
@@ -44,8 +48,7 @@ export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestro
|
|
|
44
48
|
DisplayStateChange: EventEmitter<MapDisplayState>;
|
|
45
49
|
/** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */
|
|
46
50
|
RenderModeChange: EventEmitter<MapRenderMode>;
|
|
47
|
-
private
|
|
48
|
-
private markerLayer;
|
|
51
|
+
private engine;
|
|
49
52
|
private visibilityObserver;
|
|
50
53
|
private pendingRender;
|
|
51
54
|
IsLoading: boolean;
|
|
@@ -57,69 +60,15 @@ export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestro
|
|
|
57
60
|
ngOnChanges(changes: SimpleChanges): void;
|
|
58
61
|
ngOnDestroy(): void;
|
|
59
62
|
/**
|
|
60
|
-
* Initialize the
|
|
63
|
+
* Initialize the map engine via MapCore.
|
|
61
64
|
*/
|
|
62
65
|
private InitializeMap;
|
|
63
66
|
/**
|
|
64
|
-
* Render
|
|
67
|
+
* Render records using the current mode, update marker count.
|
|
65
68
|
*/
|
|
66
|
-
private
|
|
69
|
+
private DoRender;
|
|
67
70
|
/**
|
|
68
|
-
*
|
|
69
|
-
* Uses leaflet.markercluster when available, falls back to plain markers.
|
|
70
|
-
*/
|
|
71
|
-
private RenderPointMarkers;
|
|
72
|
-
/**
|
|
73
|
-
* Heatmap mode: density visualization using colored circle markers.
|
|
74
|
-
* Larger/more opaque circles where records cluster together.
|
|
75
|
-
* Click a cluster bubble to see matching records in a popup.
|
|
76
|
-
*/
|
|
77
|
-
private RenderHeatmap;
|
|
78
|
-
/**
|
|
79
|
-
* Regions mode: shaded geographic regions using GeoJSON boundaries.
|
|
80
|
-
* Uses coordinate-based point-in-polygon resolution via GeoDataEngine —
|
|
81
|
-
* no dependency on text field names in the entity. Each record's lat/lng
|
|
82
|
-
* is resolved to its containing country and state/province geometrically.
|
|
83
|
-
*
|
|
84
|
-
* Auto-detects grouping level:
|
|
85
|
-
* - If records span multiple countries → group by Country, render country polygons
|
|
86
|
-
* - If all records are in one country → group by State/Province, render state polygons
|
|
87
|
-
* Falls back to spatial clustering if boundary data isn't available.
|
|
88
|
-
*/
|
|
89
|
-
private RenderChoropleth;
|
|
90
|
-
/**
|
|
91
|
-
* Render state-level choropleth regions from coordinate-resolved groups.
|
|
92
|
-
* Each group is keyed by state ID with its entity and records already resolved.
|
|
93
|
-
*/
|
|
94
|
-
private RenderStateRegions;
|
|
95
|
-
/**
|
|
96
|
-
* Render country-level choropleth regions from coordinate-resolved groups.
|
|
97
|
-
*/
|
|
98
|
-
private RenderCountryRegions;
|
|
99
|
-
/**
|
|
100
|
-
* Render a single GeoJSON boundary region with shading.
|
|
101
|
-
* Returns true if the boundary rendered successfully, false if fallback is needed.
|
|
102
|
-
*/
|
|
103
|
-
private RenderBoundaryRegion;
|
|
104
|
-
/**
|
|
105
|
-
* Render a colored circle marker as a fallback for regions without boundary data.
|
|
106
|
-
*/
|
|
107
|
-
private RenderCircleFallback;
|
|
108
|
-
/**
|
|
109
|
-
* Simple spatial clustering: group nearby records within a lat/lng radius.
|
|
110
|
-
* Returns clusters with center coordinates and member records.
|
|
111
|
-
*/
|
|
112
|
-
private SpatialCluster;
|
|
113
|
-
/**
|
|
114
|
-
* Fit map bounds to encompass all marker coordinates.
|
|
115
|
-
*/
|
|
116
|
-
private FitBoundsToMarkers;
|
|
117
|
-
/**
|
|
118
|
-
* Emit the current display state for persistence.
|
|
119
|
-
*/
|
|
120
|
-
private EmitDisplayState;
|
|
121
|
-
/**
|
|
122
|
-
* Set rendering mode.
|
|
71
|
+
* Set rendering mode (called from template toolbar buttons).
|
|
123
72
|
*/
|
|
124
73
|
SetRenderMode(mode: MapRenderMode): void;
|
|
125
74
|
/**
|
|
@@ -130,22 +79,7 @@ export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestro
|
|
|
130
79
|
* Get a numeric field value from a record, returning null if not a valid number.
|
|
131
80
|
*/
|
|
132
81
|
private GetNumericField;
|
|
133
|
-
/**
|
|
134
|
-
* Build a popup for a cluster of records.
|
|
135
|
-
* Shows first 5 records as clickable links, then "and X more..." if needed.
|
|
136
|
-
* Clicking a record name emits MarkerClick so the parent can open it.
|
|
137
|
-
*/
|
|
138
|
-
private BuildClusterPopup;
|
|
139
|
-
/**
|
|
140
|
-
* Set up popup click handlers after the map initializes.
|
|
141
|
-
* Listens for clicks on record links in popups and emits MarkerClick.
|
|
142
|
-
*/
|
|
143
|
-
private SetupPopupClickHandler;
|
|
144
|
-
/**
|
|
145
|
-
* Escape HTML for popup content.
|
|
146
|
-
*/
|
|
147
|
-
private EscapeHtml;
|
|
148
82
|
static ɵfac: i0.ɵɵFactoryDeclaration<MapViewComponent, never>;
|
|
149
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<MapViewComponent, "mj-map-view", never, { "Entity": { "alias": "Entity"; "required": false; }; "Records": { "alias": "Records"; "required": false; }; "LatitudeField": { "alias": "LatitudeField"; "required": false; }; "LongitudeField": { "alias": "LongitudeField"; "required": false; }; "RenderMode": { "alias": "RenderMode"; "required": false; }; "DisplayState": { "alias": "DisplayState"; "required": false; }; "TotalRecordCount": { "alias": "TotalRecordCount"; "required": false; }; }, { "MarkerClick": "MarkerClick"; "RegionClick": "RegionClick"; "DisplayStateChange": "DisplayStateChange"; "RenderModeChange": "RenderModeChange"; }, never, never, false, never>;
|
|
83
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<MapViewComponent, "mj-map-view", never, { "Entity": { "alias": "Entity"; "required": false; }; "Records": { "alias": "Records"; "required": false; }; "LatitudeField": { "alias": "LatitudeField"; "required": false; }; "LongitudeField": { "alias": "LongitudeField"; "required": false; }; "RenderMode": { "alias": "RenderMode"; "required": false; }; "BoundaryField": { "alias": "BoundaryField"; "required": false; }; "DisplayState": { "alias": "DisplayState"; "required": false; }; "TotalRecordCount": { "alias": "TotalRecordCount"; "required": false; }; }, { "MarkerClick": "MarkerClick"; "RegionClick": "RegionClick"; "DisplayStateChange": "DisplayStateChange"; "RenderModeChange": "RenderModeChange"; }, never, never, false, never>;
|
|
150
84
|
}
|
|
151
85
|
//# sourceMappingURL=map-view.component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-view.component.d.ts","sourceRoot":"","sources":["../../src/lib/map-view.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,YAAY,EACZ,MAAM,EACN,SAAS,EACT,SAAS,EACT,aAAa,EAIb,aAAa,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;;
|
|
1
|
+
{"version":3,"file":"map-view.component.d.ts","sourceRoot":"","sources":["../../src/lib/map-view.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,YAAY,EACZ,MAAM,EACN,SAAS,EACT,SAAS,EACT,aAAa,EAIb,aAAa,EAEhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;;AAK5G;;;;;;;;;;;;;;GAcG;AACH,qBAMa,gBAAiB,YAAW,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS;IAChF,OAAO,CAAC,GAAG,CAA6B;IAGxC,OAAO,CAAC,YAAY,CAA8B;IAElD,iDAAiD;IACxC,MAAM,EAAG,UAAU,CAAC;IAE7B,kGAAkG;IACzF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAM;IAEjD,sDAAsD;IAC7C,aAAa,EAAE,MAAM,CAAmB;IAEjD,wDAAwD;IAC/C,cAAc,EAAE,MAAM,CAAoB;IAEnD,2BAA2B;IAClB,UAAU,EAAE,aAAa,CAAW;IAE7C,sFAAsF;IAC7E,aAAa,EAAE,MAAM,CAAqB;IAGnD,6CAA6C;IACpC,YAAY,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAAQ;IAE9D;;;;OAIG;IACM,gBAAgB,EAAE,MAAM,CAAK;IAEtC,wCAAwC;IAC9B,WAAW,oCAA2C;IAEhE,mDAAmD;IACzC,WAAW,oCAA2C;IAEhE,4DAA4D;IAClD,kBAAkB,gCAAuC;IAEnE,mFAAmF;IACzE,gBAAgB,8BAAqC;IAE/D,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,kBAAkB,CAAqC;IAC/D,OAAO,CAAC,aAAa,CAAS;IAC9B,SAAS,UAAQ;IACjB,WAAW,SAAK;IAEhB,iFAAiF;IACjF,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,QAAQ,IAAI,IAAI;IAIhB,eAAe,IAAI,IAAI;IAwBvB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAgBzC,WAAW,IAAI,IAAI;IAWnB;;OAEG;IACH,OAAO,CAAC,aAAa;IAmFrB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAOhB;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAMxC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAOhB;;OAEG;IACH,OAAO,CAAC,eAAe;yCAvOd,gBAAgB;2CAAhB,gBAAgB;CA6O5B"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Component, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
|
|
2
2
|
import { GeoDataEngine } from '@memberjunction/core-entities';
|
|
3
|
+
import * as MapCore from '@memberjunction/geo-maps';
|
|
3
4
|
import * as i0 from "@angular/core";
|
|
4
5
|
import * as i1 from "@memberjunction/ng-shared-generic";
|
|
5
6
|
import * as i2 from "@angular/common";
|
|
6
7
|
const _c0 = ["mapContainer"];
|
|
7
|
-
function
|
|
8
|
-
i0.ɵɵelementStart(0, "span",
|
|
8
|
+
function MapViewComponent_Conditional_17_Template(rf, ctx) { if (rf & 1) {
|
|
9
|
+
i0.ɵɵelementStart(0, "span", 10);
|
|
9
10
|
i0.ɵɵpipe(1, "number");
|
|
10
11
|
i0.ɵɵpipe(2, "number");
|
|
11
|
-
i0.ɵɵelement(3, "i",
|
|
12
|
+
i0.ɵɵelement(3, "i", 13);
|
|
12
13
|
i0.ɵɵtext(4);
|
|
13
14
|
i0.ɵɵpipe(5, "number");
|
|
14
15
|
i0.ɵɵpipe(6, "number");
|
|
@@ -19,9 +20,9 @@ function MapViewComponent_Conditional_14_Template(rf, ctx) { if (rf & 1) {
|
|
|
19
20
|
i0.ɵɵadvance(4);
|
|
20
21
|
i0.ɵɵtextInterpolate2(" Showing ", i0.ɵɵpipeBind1(5, 9, ctx_r1.Records.length), " of ", i0.ɵɵpipeBind1(6, 11, ctx_r1.TotalRecordCount), " ");
|
|
21
22
|
} }
|
|
22
|
-
function
|
|
23
|
-
i0.ɵɵelementStart(0, "div",
|
|
24
|
-
i0.ɵɵelement(1, "mj-loading",
|
|
23
|
+
function MapViewComponent_Conditional_18_Template(rf, ctx) { if (rf & 1) {
|
|
24
|
+
i0.ɵɵelementStart(0, "div", 11);
|
|
25
|
+
i0.ɵɵelement(1, "mj-loading", 14);
|
|
25
26
|
i0.ɵɵelementEnd();
|
|
26
27
|
} }
|
|
27
28
|
// Leaflet is loaded as a global script at runtime via angular.json scripts array.
|
|
@@ -35,9 +36,11 @@ function MapViewComponent_Conditional_15_Template(rf, ctx) { if (rf & 1) {
|
|
|
35
36
|
* Supports three rendering modes:
|
|
36
37
|
* - **point** (default): Individual markers with optional clustering
|
|
37
38
|
* - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)
|
|
38
|
-
* - **heatmap**: Density visualization
|
|
39
|
+
* - **heatmap**: Density visualization
|
|
39
40
|
*
|
|
40
|
-
*
|
|
41
|
+
* Delegates all rendering logic to @memberjunction/geo-maps.
|
|
42
|
+
* This component is a thin Angular wrapper managing lifecycle, inputs/outputs,
|
|
43
|
+
* and IntersectionObserver-based deferred initialization.
|
|
41
44
|
*/
|
|
42
45
|
export class MapViewComponent {
|
|
43
46
|
cdr = inject(ChangeDetectorRef);
|
|
@@ -52,6 +55,8 @@ export class MapViewComponent {
|
|
|
52
55
|
LongitudeField = '__mj_Longitude';
|
|
53
56
|
/** Current render mode. */
|
|
54
57
|
RenderMode = 'point';
|
|
58
|
+
/** Field name containing GeoJSON boundary data on each record (for boundary mode). */
|
|
59
|
+
BoundaryField = 'BoundaryGeoJSON';
|
|
55
60
|
/** Persisted display state from UserView. */
|
|
56
61
|
DisplayState = null;
|
|
57
62
|
/**
|
|
@@ -68,8 +73,7 @@ export class MapViewComponent {
|
|
|
68
73
|
DisplayStateChange = new EventEmitter();
|
|
69
74
|
/** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */
|
|
70
75
|
RenderModeChange = new EventEmitter();
|
|
71
|
-
|
|
72
|
-
markerLayer = null;
|
|
76
|
+
engine = null;
|
|
73
77
|
visibilityObserver = null;
|
|
74
78
|
pendingRender = false;
|
|
75
79
|
IsLoading = true;
|
|
@@ -87,15 +91,15 @@ export class MapViewComponent {
|
|
|
87
91
|
// Since the map starts hidden (grid view is default), we defer init until visible.
|
|
88
92
|
this.visibilityObserver = new IntersectionObserver((entries) => {
|
|
89
93
|
const entry = entries[0];
|
|
90
|
-
if (entry.isIntersecting && !this.
|
|
94
|
+
if (entry.isIntersecting && !this.engine) {
|
|
91
95
|
// First time visible — initialize the map
|
|
92
96
|
this.InitializeMap();
|
|
93
97
|
}
|
|
94
|
-
else if (entry.isIntersecting && this.
|
|
98
|
+
else if (entry.isIntersecting && this.engine) {
|
|
95
99
|
// Becoming visible again — fix tile rendering and re-fit
|
|
96
|
-
this.
|
|
100
|
+
this.engine.invalidateSize();
|
|
97
101
|
if (this.pendingRender) {
|
|
98
|
-
this.
|
|
102
|
+
this.DoRender();
|
|
99
103
|
this.pendingRender = false;
|
|
100
104
|
}
|
|
101
105
|
}
|
|
@@ -106,11 +110,11 @@ export class MapViewComponent {
|
|
|
106
110
|
}
|
|
107
111
|
ngOnChanges(changes) {
|
|
108
112
|
if (changes['Records'] || changes['RenderMode']) {
|
|
109
|
-
if (this.
|
|
113
|
+
if (this.engine) {
|
|
110
114
|
setTimeout(() => {
|
|
111
|
-
if (this.
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
115
|
+
if (this.engine) {
|
|
116
|
+
this.engine.invalidateSize();
|
|
117
|
+
this.DoRender();
|
|
114
118
|
}
|
|
115
119
|
}, 100);
|
|
116
120
|
}
|
|
@@ -125,393 +129,112 @@ export class MapViewComponent {
|
|
|
125
129
|
this.visibilityObserver.disconnect();
|
|
126
130
|
this.visibilityObserver = null;
|
|
127
131
|
}
|
|
128
|
-
if (this.
|
|
129
|
-
this.
|
|
130
|
-
this.
|
|
132
|
+
if (this.engine) {
|
|
133
|
+
this.engine.destroy();
|
|
134
|
+
this.engine = null;
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
/**
|
|
134
|
-
* Initialize the
|
|
138
|
+
* Initialize the map engine via MapCore.
|
|
135
139
|
*/
|
|
136
140
|
InitializeMap() {
|
|
137
|
-
if (!this.mapContainer?.nativeElement || this.
|
|
141
|
+
if (!this.mapContainer?.nativeElement || this.engine)
|
|
138
142
|
return;
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
this.engine = MapCore.createEngine({
|
|
144
|
+
container: this.mapContainer.nativeElement,
|
|
145
|
+
center: {
|
|
146
|
+
lat: this.DisplayState?.CenterLat ?? 20,
|
|
147
|
+
lng: this.DisplayState?.CenterLng ?? 0
|
|
148
|
+
},
|
|
149
|
+
zoom: this.DisplayState?.ZoomLevel ?? 2,
|
|
150
|
+
latitudeField: this.LatitudeField,
|
|
151
|
+
longitudeField: this.LongitudeField,
|
|
152
|
+
boundaryField: this.BoundaryField,
|
|
153
|
+
geoResolver: GeoDataEngine.Instance,
|
|
154
|
+
getRecordId: (record) => {
|
|
155
|
+
const pkFields = this.Entity?.PrimaryKeys ?? [];
|
|
156
|
+
return pkFields.map(pk => this.GetField(record, pk.Name)).join('||');
|
|
157
|
+
},
|
|
158
|
+
getRecordName: (record) => {
|
|
159
|
+
const nameField = this.Entity?.NameField;
|
|
160
|
+
return nameField ? String(this.GetField(record, nameField.Name) ?? '') : 'Record';
|
|
161
|
+
},
|
|
162
|
+
onMarkerClick: (event) => {
|
|
163
|
+
this.MarkerClick.emit({
|
|
164
|
+
RecordID: event.recordId,
|
|
165
|
+
Latitude: event.lat,
|
|
166
|
+
Longitude: event.lng,
|
|
167
|
+
Record: event.record
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
onRegionClick: (event) => {
|
|
171
|
+
this.RegionClick.emit({
|
|
172
|
+
RegionName: event.regionName,
|
|
173
|
+
GroupBy: event.groupBy,
|
|
174
|
+
RecordCount: event.recordCount,
|
|
175
|
+
Records: event.records
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
onPopupRecordClick: (recordId) => {
|
|
179
|
+
const record = this.Records.find(r => {
|
|
180
|
+
const pkFields = this.Entity?.PrimaryKeys ?? [];
|
|
181
|
+
const id = pkFields.map(pk => this.GetField(r, pk.Name)).join('||');
|
|
182
|
+
return id === recordId;
|
|
183
|
+
});
|
|
184
|
+
if (record) {
|
|
185
|
+
const lat = this.GetNumericField(record, this.LatitudeField) ?? 0;
|
|
186
|
+
const lng = this.GetNumericField(record, this.LongitudeField) ?? 0;
|
|
187
|
+
this.MarkerClick.emit({
|
|
188
|
+
RecordID: recordId,
|
|
189
|
+
Latitude: lat,
|
|
190
|
+
Longitude: lng,
|
|
191
|
+
Record: record
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
onMoveEnd: (state) => {
|
|
196
|
+
this.DisplayStateChange.emit({
|
|
197
|
+
ZoomLevel: state.zoom,
|
|
198
|
+
CenterLat: state.centerLat,
|
|
199
|
+
CenterLng: state.centerLng,
|
|
200
|
+
ClusterMarkers: true
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
maxPopupRecords: 5
|
|
149
204
|
});
|
|
150
|
-
// Compact attribution — required by OSM terms but styled to be unobtrusive
|
|
151
|
-
L.control.attribution({ prefix: false, position: 'bottomright' }).addTo(this.map);
|
|
152
|
-
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
153
|
-
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a>',
|
|
154
|
-
maxZoom: 18
|
|
155
|
-
}).addTo(this.map);
|
|
156
|
-
this.markerLayer = L.layerGroup().addTo(this.map);
|
|
157
|
-
// Persist view state on move/zoom
|
|
158
|
-
this.map.on('moveend', () => this.EmitDisplayState());
|
|
159
|
-
// Set up popup click handlers for record drill-through
|
|
160
|
-
this.SetupPopupClickHandler();
|
|
161
205
|
// invalidateSize after a short delay to ensure tile rendering is correct
|
|
162
206
|
// then render markers and fit bounds
|
|
163
207
|
setTimeout(() => {
|
|
164
|
-
if (this.
|
|
165
|
-
this.
|
|
166
|
-
this.
|
|
208
|
+
if (this.engine) {
|
|
209
|
+
this.engine.invalidateSize();
|
|
210
|
+
this.DoRender();
|
|
167
211
|
this.IsLoading = false;
|
|
168
212
|
this.cdr.detectChanges();
|
|
169
213
|
// Double-invalidate after tiles start loading to fix partial rendering
|
|
170
214
|
setTimeout(() => {
|
|
171
|
-
if (this.
|
|
172
|
-
this.
|
|
215
|
+
if (this.engine)
|
|
216
|
+
this.engine.invalidateSize();
|
|
173
217
|
}, 500);
|
|
174
218
|
}
|
|
175
219
|
}, 100);
|
|
176
220
|
}
|
|
177
221
|
/**
|
|
178
|
-
* Render
|
|
222
|
+
* Render records using the current mode, update marker count.
|
|
179
223
|
*/
|
|
180
|
-
|
|
181
|
-
if (!this.
|
|
224
|
+
DoRender() {
|
|
225
|
+
if (!this.engine)
|
|
182
226
|
return;
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
case 'point':
|
|
186
|
-
this.RenderPointMarkers();
|
|
187
|
-
break;
|
|
188
|
-
case 'heatmap':
|
|
189
|
-
this.RenderHeatmap();
|
|
190
|
-
break;
|
|
191
|
-
case 'choropleth':
|
|
192
|
-
this.RenderChoropleth();
|
|
193
|
-
break;
|
|
194
|
-
default:
|
|
195
|
-
this.RenderPointMarkers();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Point mode: individual markers with marker clustering for dense areas.
|
|
200
|
-
* Uses leaflet.markercluster when available, falls back to plain markers.
|
|
201
|
-
*/
|
|
202
|
-
RenderPointMarkers() {
|
|
203
|
-
const bounds = [];
|
|
204
|
-
// Use marker cluster group if available (loaded via CDN)
|
|
205
|
-
const useCluster = typeof L.markerClusterGroup === 'function';
|
|
206
|
-
const clusterGroup = useCluster ? L.markerClusterGroup({
|
|
207
|
-
maxClusterRadius: 50,
|
|
208
|
-
spiderfyOnMaxZoom: true,
|
|
209
|
-
showCoverageOnHover: false
|
|
210
|
-
}) : null;
|
|
211
|
-
for (const record of this.Records) {
|
|
212
|
-
const lat = this.GetNumericField(record, this.LatitudeField);
|
|
213
|
-
const lng = this.GetNumericField(record, this.LongitudeField);
|
|
214
|
-
if (lat == null || lng == null || isNaN(lat) || isNaN(lng))
|
|
215
|
-
continue;
|
|
216
|
-
const latLng = L.latLng(lat, lng);
|
|
217
|
-
bounds.push(latLng);
|
|
218
|
-
const marker = L.marker(latLng);
|
|
219
|
-
const nameField = this.Entity?.NameField;
|
|
220
|
-
const label = nameField ? String(this.GetField(record, nameField.Name) ?? '') : 'Record';
|
|
221
|
-
marker.bindPopup(`<b>${this.EscapeHtml(label)}</b>`);
|
|
222
|
-
marker.on('click', () => {
|
|
223
|
-
const pkFields = this.Entity?.PrimaryKeys ?? [];
|
|
224
|
-
const recordId = pkFields.map(pk => this.GetField(record, pk.Name)).join('||');
|
|
225
|
-
this.MarkerClick.emit({
|
|
226
|
-
RecordID: recordId,
|
|
227
|
-
Latitude: lat,
|
|
228
|
-
Longitude: lng,
|
|
229
|
-
Record: record
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
if (clusterGroup) {
|
|
233
|
-
clusterGroup.addLayer(marker);
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
this.markerLayer.addLayer(marker);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (clusterGroup) {
|
|
240
|
-
this.markerLayer.addLayer(clusterGroup);
|
|
241
|
-
}
|
|
242
|
-
this.MarkerCount = bounds.length;
|
|
243
|
-
this.FitBoundsToMarkers(bounds);
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Heatmap mode: density visualization using colored circle markers.
|
|
247
|
-
* Larger/more opaque circles where records cluster together.
|
|
248
|
-
* Click a cluster bubble to see matching records in a popup.
|
|
249
|
-
*/
|
|
250
|
-
RenderHeatmap() {
|
|
251
|
-
const bounds = [];
|
|
252
|
-
const recordsWithCoords = [];
|
|
253
|
-
for (const record of this.Records) {
|
|
254
|
-
const lat = this.GetNumericField(record, this.LatitudeField);
|
|
255
|
-
const lng = this.GetNumericField(record, this.LongitudeField);
|
|
256
|
-
if (lat == null || lng == null || isNaN(lat) || isNaN(lng))
|
|
257
|
-
continue;
|
|
258
|
-
bounds.push(L.latLng(lat, lng));
|
|
259
|
-
recordsWithCoords.push({ lat, lng, record });
|
|
260
|
-
}
|
|
261
|
-
// Cluster nearby records (within ~2 degrees / ~200km) into groups
|
|
262
|
-
const clusters = this.SpatialCluster(recordsWithCoords, 2.0);
|
|
263
|
-
for (const cluster of clusters) {
|
|
264
|
-
const radius = Math.min(12 + cluster.records.length * 5, 40);
|
|
265
|
-
const opacity = Math.min(0.3 + cluster.records.length * 0.08, 0.85);
|
|
266
|
-
const circle = L.circleMarker([cluster.centerLat, cluster.centerLng], {
|
|
267
|
-
radius,
|
|
268
|
-
fillColor: '#e74c3c',
|
|
269
|
-
fillOpacity: opacity,
|
|
270
|
-
color: '#c0392b',
|
|
271
|
-
weight: 1,
|
|
272
|
-
opacity: 0.7
|
|
273
|
-
});
|
|
274
|
-
circle.bindPopup(this.BuildClusterPopup(cluster.records, `${cluster.records.length} record${cluster.records.length !== 1 ? 's' : ''}`));
|
|
275
|
-
circle.on('click', () => {
|
|
276
|
-
this.RegionClick.emit({
|
|
277
|
-
RegionName: `Cluster (${cluster.records.length} records)`,
|
|
278
|
-
GroupBy: 'state_province',
|
|
279
|
-
RecordCount: cluster.records.length,
|
|
280
|
-
Records: cluster.records
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
this.markerLayer.addLayer(circle);
|
|
284
|
-
}
|
|
285
|
-
this.MarkerCount = bounds.length;
|
|
286
|
-
this.FitBoundsToMarkers(bounds);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Regions mode: shaded geographic regions using GeoJSON boundaries.
|
|
290
|
-
* Uses coordinate-based point-in-polygon resolution via GeoDataEngine —
|
|
291
|
-
* no dependency on text field names in the entity. Each record's lat/lng
|
|
292
|
-
* is resolved to its containing country and state/province geometrically.
|
|
293
|
-
*
|
|
294
|
-
* Auto-detects grouping level:
|
|
295
|
-
* - If records span multiple countries → group by Country, render country polygons
|
|
296
|
-
* - If all records are in one country → group by State/Province, render state polygons
|
|
297
|
-
* Falls back to spatial clustering if boundary data isn't available.
|
|
298
|
-
*/
|
|
299
|
-
RenderChoropleth() {
|
|
300
|
-
const geo = GeoDataEngine.Instance;
|
|
301
|
-
const bounds = [];
|
|
302
|
-
// Resolve each record's location via point-in-polygon
|
|
303
|
-
const recordsByCountryId = new Map();
|
|
304
|
-
const recordsByStateId = new Map();
|
|
305
|
-
const unmatchedRecords = [];
|
|
306
|
-
for (const record of this.Records) {
|
|
307
|
-
const lat = this.GetNumericField(record, this.LatitudeField);
|
|
308
|
-
const lng = this.GetNumericField(record, this.LongitudeField);
|
|
309
|
-
if (lat == null || lng == null || isNaN(lat) || isNaN(lng))
|
|
310
|
-
continue;
|
|
311
|
-
bounds.push(L.latLng(lat, lng));
|
|
312
|
-
const resolution = geo.ResolvePointToLocation(lat, lng);
|
|
313
|
-
if (resolution.Country) {
|
|
314
|
-
const countryId = resolution.Country.ID.toLowerCase();
|
|
315
|
-
let countryGroup = recordsByCountryId.get(countryId);
|
|
316
|
-
if (!countryGroup) {
|
|
317
|
-
countryGroup = { country: resolution.Country, records: [] };
|
|
318
|
-
recordsByCountryId.set(countryId, countryGroup);
|
|
319
|
-
}
|
|
320
|
-
countryGroup.records.push(record);
|
|
321
|
-
if (resolution.State) {
|
|
322
|
-
const stateId = resolution.State.ID.toLowerCase();
|
|
323
|
-
let stateGroup = recordsByStateId.get(stateId);
|
|
324
|
-
if (!stateGroup) {
|
|
325
|
-
stateGroup = { state: resolution.State, countryId, records: [] };
|
|
326
|
-
recordsByStateId.set(stateId, stateGroup);
|
|
327
|
-
}
|
|
328
|
-
stateGroup.records.push(record);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
else {
|
|
332
|
-
unmatchedRecords.push(record);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
this.MarkerCount = bounds.length;
|
|
336
|
-
// Auto-detect grouping: single country → state-level, multiple → country-level
|
|
337
|
-
if (recordsByCountryId.size <= 1 && recordsByStateId.size > 1) {
|
|
338
|
-
this.RenderStateRegions(recordsByStateId, unmatchedRecords, bounds);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
this.RenderCountryRegions(recordsByCountryId, unmatchedRecords, bounds);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Render state-level choropleth regions from coordinate-resolved groups.
|
|
346
|
-
* Each group is keyed by state ID with its entity and records already resolved.
|
|
347
|
-
*/
|
|
348
|
-
RenderStateRegions(recordsByStateId, unmatchedRecords, bounds) {
|
|
349
|
-
const colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',
|
|
350
|
-
'#1abc9c', '#e67e22', '#2980b9', '#27ae60', '#c0392b',
|
|
351
|
-
'#16a085', '#d35400', '#8e44ad', '#2c3e50', '#f1c40f'];
|
|
352
|
-
let colorIdx = 0;
|
|
353
|
-
for (const [, group] of recordsByStateId) {
|
|
354
|
-
const color = colors[colorIdx % colors.length];
|
|
355
|
-
const rendered = this.RenderBoundaryRegion(group.state.Name, group.state.BoundaryGeoJSON, group.records, color, 'state_province');
|
|
356
|
-
if (!rendered) {
|
|
357
|
-
this.RenderCircleFallback(group.state.Name, group.records, color);
|
|
358
|
-
}
|
|
359
|
-
colorIdx++;
|
|
360
|
-
}
|
|
361
|
-
// Render unmatched records as a gray cluster
|
|
362
|
-
if (unmatchedRecords.length > 0) {
|
|
363
|
-
this.RenderCircleFallback('Unmatched', unmatchedRecords, '#95a5a6');
|
|
364
|
-
}
|
|
365
|
-
this.FitBoundsToMarkers(bounds);
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Render country-level choropleth regions from coordinate-resolved groups.
|
|
369
|
-
*/
|
|
370
|
-
RenderCountryRegions(recordsByCountryId, unmatchedRecords, bounds) {
|
|
371
|
-
const colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',
|
|
372
|
-
'#1abc9c', '#e67e22', '#2980b9', '#27ae60', '#c0392b',
|
|
373
|
-
'#16a085', '#d35400', '#8e44ad', '#2c3e50', '#f1c40f'];
|
|
374
|
-
let colorIdx = 0;
|
|
375
|
-
for (const [, group] of recordsByCountryId) {
|
|
376
|
-
const color = colors[colorIdx % colors.length];
|
|
377
|
-
const rendered = this.RenderBoundaryRegion(group.country.Name, group.country.BoundaryGeoJSON, group.records, color, 'country');
|
|
378
|
-
if (!rendered) {
|
|
379
|
-
this.RenderCircleFallback(group.country.Name, group.records, color);
|
|
380
|
-
}
|
|
381
|
-
colorIdx++;
|
|
382
|
-
}
|
|
383
|
-
if (unmatchedRecords.length > 0) {
|
|
384
|
-
this.RenderCircleFallback('Unmatched', unmatchedRecords, '#95a5a6');
|
|
385
|
-
}
|
|
386
|
-
this.FitBoundsToMarkers(bounds);
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Render a single GeoJSON boundary region with shading.
|
|
390
|
-
* Returns true if the boundary rendered successfully, false if fallback is needed.
|
|
391
|
-
*/
|
|
392
|
-
RenderBoundaryRegion(regionName, boundaryGeoJSON, records, color, groupBy) {
|
|
393
|
-
if (!boundaryGeoJSON)
|
|
394
|
-
return false;
|
|
395
|
-
try {
|
|
396
|
-
const geojson = typeof boundaryGeoJSON === 'string'
|
|
397
|
-
? JSON.parse(boundaryGeoJSON)
|
|
398
|
-
: boundaryGeoJSON;
|
|
399
|
-
const layer = L.geoJSON(geojson, {
|
|
400
|
-
style: {
|
|
401
|
-
fillColor: color,
|
|
402
|
-
fillOpacity: 0.35,
|
|
403
|
-
color: color,
|
|
404
|
-
weight: 2,
|
|
405
|
-
opacity: 0.8
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
layer.bindPopup(this.BuildClusterPopup(records, `${regionName} (${records.length})`));
|
|
409
|
-
layer.on('click', () => {
|
|
410
|
-
this.RegionClick.emit({
|
|
411
|
-
RegionName: regionName,
|
|
412
|
-
GroupBy: groupBy,
|
|
413
|
-
RecordCount: records.length,
|
|
414
|
-
Records: records
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
this.markerLayer.addLayer(layer);
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
catch (e) {
|
|
421
|
-
console.warn(`[MapView] Choropleth: GeoJSON render failed for "${regionName}"`, e);
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Render a colored circle marker as a fallback for regions without boundary data.
|
|
427
|
-
*/
|
|
428
|
-
RenderCircleFallback(regionName, records, color) {
|
|
429
|
-
let sumLat = 0, sumLng = 0, count = 0;
|
|
430
|
-
for (const rec of records) {
|
|
431
|
-
const lat = this.GetNumericField(rec, this.LatitudeField);
|
|
432
|
-
const lng = this.GetNumericField(rec, this.LongitudeField);
|
|
433
|
-
if (lat && lng) {
|
|
434
|
-
sumLat += lat;
|
|
435
|
-
sumLng += lng;
|
|
436
|
-
count++;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (count > 0) {
|
|
440
|
-
const radius = Math.min(15 + records.length * 5, 50);
|
|
441
|
-
const circle = L.circleMarker([sumLat / count, sumLng / count], {
|
|
442
|
-
radius, fillColor: color, fillOpacity: 0.45, color, weight: 2, opacity: 0.85
|
|
443
|
-
});
|
|
444
|
-
circle.bindPopup(this.BuildClusterPopup(records, `${regionName} (${records.length})`));
|
|
445
|
-
this.markerLayer.addLayer(circle);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Simple spatial clustering: group nearby records within a lat/lng radius.
|
|
450
|
-
* Returns clusters with center coordinates and member records.
|
|
451
|
-
*/
|
|
452
|
-
SpatialCluster(items, radiusDegrees) {
|
|
453
|
-
const assigned = new Set();
|
|
454
|
-
const clusters = [];
|
|
455
|
-
for (let i = 0; i < items.length; i++) {
|
|
456
|
-
if (assigned.has(i))
|
|
457
|
-
continue;
|
|
458
|
-
const seed = items[i];
|
|
459
|
-
const members = [seed.record];
|
|
460
|
-
let sumLat = seed.lat;
|
|
461
|
-
let sumLng = seed.lng;
|
|
462
|
-
assigned.add(i);
|
|
463
|
-
// Find all unassigned neighbors within radius
|
|
464
|
-
for (let j = i + 1; j < items.length; j++) {
|
|
465
|
-
if (assigned.has(j))
|
|
466
|
-
continue;
|
|
467
|
-
const candidate = items[j];
|
|
468
|
-
if (Math.abs(candidate.lat - seed.lat) <= radiusDegrees &&
|
|
469
|
-
Math.abs(candidate.lng - seed.lng) <= radiusDegrees) {
|
|
470
|
-
members.push(candidate.record);
|
|
471
|
-
sumLat += candidate.lat;
|
|
472
|
-
sumLng += candidate.lng;
|
|
473
|
-
assigned.add(j);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
clusters.push({
|
|
477
|
-
centerLat: sumLat / members.length,
|
|
478
|
-
centerLng: sumLng / members.length,
|
|
479
|
-
records: members
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
return clusters;
|
|
483
|
-
}
|
|
484
|
-
/**
|
|
485
|
-
* Fit map bounds to encompass all marker coordinates.
|
|
486
|
-
*/
|
|
487
|
-
FitBoundsToMarkers(bounds) {
|
|
488
|
-
if (bounds.length > 0 && this.map) {
|
|
489
|
-
const boundsObj = L.latLngBounds(bounds);
|
|
490
|
-
this.map.fitBounds(boundsObj, { padding: [30, 30], maxZoom: 14 });
|
|
491
|
-
}
|
|
227
|
+
this.engine.render(this.Records, this.RenderMode);
|
|
228
|
+
this.MarkerCount = this.engine.getStats().markerCount;
|
|
492
229
|
this.cdr.detectChanges();
|
|
493
230
|
}
|
|
494
231
|
/**
|
|
495
|
-
*
|
|
496
|
-
*/
|
|
497
|
-
EmitDisplayState() {
|
|
498
|
-
if (!this.map)
|
|
499
|
-
return;
|
|
500
|
-
const center = this.map.getCenter();
|
|
501
|
-
this.DisplayStateChange.emit({
|
|
502
|
-
ZoomLevel: this.map.getZoom(),
|
|
503
|
-
CenterLat: center.lat,
|
|
504
|
-
CenterLng: center.lng,
|
|
505
|
-
ClusterMarkers: true
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Set rendering mode.
|
|
232
|
+
* Set rendering mode (called from template toolbar buttons).
|
|
510
233
|
*/
|
|
511
234
|
SetRenderMode(mode) {
|
|
512
235
|
this.RenderMode = mode;
|
|
513
236
|
this.RenderModeChange.emit(mode);
|
|
514
|
-
this.
|
|
237
|
+
this.DoRender();
|
|
515
238
|
}
|
|
516
239
|
/**
|
|
517
240
|
* Get a field value from a record, supporting both BaseEntity (with .Get()) and plain objects.
|
|
@@ -532,85 +255,13 @@ export class MapViewComponent {
|
|
|
532
255
|
const num = Number(val);
|
|
533
256
|
return isNaN(num) ? null : num;
|
|
534
257
|
}
|
|
535
|
-
/**
|
|
536
|
-
* Build a popup for a cluster of records.
|
|
537
|
-
* Shows first 5 records as clickable links, then "and X more..." if needed.
|
|
538
|
-
* Clicking a record name emits MarkerClick so the parent can open it.
|
|
539
|
-
*/
|
|
540
|
-
BuildClusterPopup(records, title) {
|
|
541
|
-
const nameField = this.Entity?.NameField;
|
|
542
|
-
const maxShow = 5;
|
|
543
|
-
const shown = records.slice(0, maxShow);
|
|
544
|
-
const remaining = records.length - maxShow;
|
|
545
|
-
let html = `<div style="font-size:12px;min-width:160px;">` +
|
|
546
|
-
`<b>${this.EscapeHtml(title)}</b>` +
|
|
547
|
-
`<hr style="margin:4px 0;border-color:#e5e7eb;">`;
|
|
548
|
-
for (const rec of shown) {
|
|
549
|
-
const name = nameField ? String(this.GetField(rec, nameField.Name) ?? '') : 'Record';
|
|
550
|
-
const pkFields = this.Entity?.PrimaryKeys ?? [];
|
|
551
|
-
const recordId = pkFields.map(pk => this.GetField(rec, pk.Name)).join('||');
|
|
552
|
-
// Clickable record name — uses a data attribute that the click handler reads
|
|
553
|
-
html += `<div style="padding:2px 0;cursor:pointer;color:#2563eb;" ` +
|
|
554
|
-
`class="mj-map-popup-record" data-record-id="${this.EscapeHtml(recordId)}">` +
|
|
555
|
-
`${this.EscapeHtml(name)}</div>`;
|
|
556
|
-
}
|
|
557
|
-
if (remaining > 0) {
|
|
558
|
-
html += `<div style="padding:4px 0 0;color:#6b7280;font-style:italic;">` +
|
|
559
|
-
`and ${remaining} more...</div>`;
|
|
560
|
-
}
|
|
561
|
-
html += `</div>`;
|
|
562
|
-
return html;
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Set up popup click handlers after the map initializes.
|
|
566
|
-
* Listens for clicks on record links in popups and emits MarkerClick.
|
|
567
|
-
*/
|
|
568
|
-
SetupPopupClickHandler() {
|
|
569
|
-
if (!this.map)
|
|
570
|
-
return;
|
|
571
|
-
this.map.on('popupopen', () => {
|
|
572
|
-
// Attach click handlers to all popup record links
|
|
573
|
-
setTimeout(() => {
|
|
574
|
-
const links = document.querySelectorAll('.mj-map-popup-record');
|
|
575
|
-
links.forEach(link => {
|
|
576
|
-
link.addEventListener('click', (e) => {
|
|
577
|
-
const recordId = e.currentTarget.getAttribute('data-record-id') ?? '';
|
|
578
|
-
// Find the matching record
|
|
579
|
-
const record = this.Records.find(r => {
|
|
580
|
-
const pkFields = this.Entity?.PrimaryKeys ?? [];
|
|
581
|
-
const id = pkFields.map(pk => this.GetField(r, pk.Name)).join('||');
|
|
582
|
-
return id === recordId;
|
|
583
|
-
});
|
|
584
|
-
if (record) {
|
|
585
|
-
const lat = this.GetNumericField(record, this.LatitudeField) ?? 0;
|
|
586
|
-
const lng = this.GetNumericField(record, this.LongitudeField) ?? 0;
|
|
587
|
-
this.MarkerClick.emit({
|
|
588
|
-
RecordID: recordId,
|
|
589
|
-
Latitude: lat,
|
|
590
|
-
Longitude: lng,
|
|
591
|
-
Record: record
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
}, 50);
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Escape HTML for popup content.
|
|
601
|
-
*/
|
|
602
|
-
EscapeHtml(text) {
|
|
603
|
-
const div = document.createElement('div');
|
|
604
|
-
div.textContent = text;
|
|
605
|
-
return div.innerHTML;
|
|
606
|
-
}
|
|
607
258
|
static ɵfac = function MapViewComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MapViewComponent)(); };
|
|
608
259
|
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MapViewComponent, selectors: [["mj-map-view"]], viewQuery: function MapViewComponent_Query(rf, ctx) { if (rf & 1) {
|
|
609
260
|
i0.ɵɵviewQuery(_c0, 5);
|
|
610
261
|
} if (rf & 2) {
|
|
611
262
|
let _t;
|
|
612
263
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.mapContainer = _t.first);
|
|
613
|
-
} }, inputs: { Entity: "Entity", Records: "Records", LatitudeField: "LatitudeField", LongitudeField: "LongitudeField", RenderMode: "RenderMode", DisplayState: "DisplayState", TotalRecordCount: "TotalRecordCount" }, outputs: { MarkerClick: "MarkerClick", RegionClick: "RegionClick", DisplayStateChange: "DisplayStateChange", RenderModeChange: "RenderModeChange" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls:
|
|
264
|
+
} }, inputs: { Entity: "Entity", Records: "Records", LatitudeField: "LatitudeField", LongitudeField: "LongitudeField", RenderMode: "RenderMode", BoundaryField: "BoundaryField", DisplayState: "DisplayState", TotalRecordCount: "TotalRecordCount" }, outputs: { MarkerClick: "MarkerClick", RegionClick: "RegionClick", DisplayStateChange: "DisplayStateChange", RenderModeChange: "RenderModeChange" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 21, vars: 12, consts: [["mapContainer", ""], [1, "mj-map-view-wrapper"], [1, "mj-map-toolbar"], [1, "mj-map-mode-buttons"], [1, "mj-map-mode-btn", 3, "click"], [1, "fa-solid", "fa-location-dot"], [1, "fa-solid", "fa-draw-polygon"], [1, "fa-solid", "fa-map"], [1, "fa-solid", "fa-fire"], [1, "mj-map-info"], [1, "mj-map-truncated", 3, "title"], [1, "mj-map-loading"], [1, "mj-map-container"], [1, "fa-solid", "fa-triangle-exclamation"], ["text", "Loading map..."]], template: function MapViewComponent_Template(rf, ctx) { if (rf & 1) {
|
|
614
265
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
615
266
|
i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "div", 3)(3, "button", 4);
|
|
616
267
|
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("point")); });
|
|
@@ -618,40 +269,47 @@ export class MapViewComponent {
|
|
|
618
269
|
i0.ɵɵtext(5, " Points ");
|
|
619
270
|
i0.ɵɵelementEnd();
|
|
620
271
|
i0.ɵɵelementStart(6, "button", 4);
|
|
621
|
-
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("
|
|
272
|
+
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("boundary")); });
|
|
622
273
|
i0.ɵɵelement(7, "i", 6);
|
|
623
|
-
i0.ɵɵtext(8, "
|
|
274
|
+
i0.ɵɵtext(8, " Boundary ");
|
|
624
275
|
i0.ɵɵelementEnd();
|
|
625
276
|
i0.ɵɵelementStart(9, "button", 4);
|
|
626
|
-
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("
|
|
277
|
+
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("choropleth")); });
|
|
627
278
|
i0.ɵɵelement(10, "i", 7);
|
|
628
|
-
i0.ɵɵtext(11, "
|
|
279
|
+
i0.ɵɵtext(11, " Regions ");
|
|
280
|
+
i0.ɵɵelementEnd();
|
|
281
|
+
i0.ɵɵelementStart(12, "button", 4);
|
|
282
|
+
i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("heatmap")); });
|
|
283
|
+
i0.ɵɵelement(13, "i", 8);
|
|
284
|
+
i0.ɵɵtext(14, " Heat Map ");
|
|
629
285
|
i0.ɵɵelementEnd()();
|
|
630
|
-
i0.ɵɵelementStart(
|
|
631
|
-
i0.ɵɵtext(
|
|
632
|
-
i0.ɵɵconditionalCreate(
|
|
286
|
+
i0.ɵɵelementStart(15, "div", 9);
|
|
287
|
+
i0.ɵɵtext(16);
|
|
288
|
+
i0.ɵɵconditionalCreate(17, MapViewComponent_Conditional_17_Template, 7, 13, "span", 10);
|
|
633
289
|
i0.ɵɵelementEnd()();
|
|
634
|
-
i0.ɵɵconditionalCreate(
|
|
635
|
-
i0.ɵɵelement(
|
|
290
|
+
i0.ɵɵconditionalCreate(18, MapViewComponent_Conditional_18_Template, 2, 0, "div", 11);
|
|
291
|
+
i0.ɵɵelement(19, "div", 12, 0);
|
|
636
292
|
i0.ɵɵelementEnd();
|
|
637
293
|
} if (rf & 2) {
|
|
638
294
|
i0.ɵɵadvance(3);
|
|
639
295
|
i0.ɵɵclassProp("active", ctx.RenderMode === "point");
|
|
640
296
|
i0.ɵɵadvance(3);
|
|
297
|
+
i0.ɵɵclassProp("active", ctx.RenderMode === "boundary");
|
|
298
|
+
i0.ɵɵadvance(3);
|
|
641
299
|
i0.ɵɵclassProp("active", ctx.RenderMode === "choropleth");
|
|
642
300
|
i0.ɵɵadvance(3);
|
|
643
301
|
i0.ɵɵclassProp("active", ctx.RenderMode === "heatmap");
|
|
644
302
|
i0.ɵɵadvance(4);
|
|
645
303
|
i0.ɵɵtextInterpolate2(" ", ctx.MarkerCount, " location", ctx.MarkerCount !== 1 ? "s" : "", " mapped ");
|
|
646
304
|
i0.ɵɵadvance();
|
|
647
|
-
i0.ɵɵconditional(ctx.IsTruncated ?
|
|
305
|
+
i0.ɵɵconditional(ctx.IsTruncated ? 17 : -1);
|
|
648
306
|
i0.ɵɵadvance();
|
|
649
|
-
i0.ɵɵconditional(ctx.IsLoading ?
|
|
307
|
+
i0.ɵɵconditional(ctx.IsLoading ? 18 : -1);
|
|
650
308
|
} }, dependencies: [i1.LoadingComponent, i2.DecimalPipe], styles: ["[_nghost-%COMP%] {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.mj-map-view-wrapper[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mj-map-toolbar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n z-index: 1000;\n}\n\n.mj-map-mode-buttons[_ngcontent-%COMP%] {\n display: flex;\n gap: 4px;\n}\n\n.mj-map-mode-btn[_ngcontent-%COMP%] {\n padding: 6px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 13px;\n transition: all 0.15s ease;\n}\n\n.mj-map-mode-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.mj-map-mode-btn.active[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-map-info[_ngcontent-%COMP%] {\n font-size: 13px;\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-map-truncated[_ngcontent-%COMP%] {\n color: var(--mj-status-warning-text);\n font-size: 12px;\n}\n\n.mj-map-loading[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 500;\n background: var(--mj-bg-surface);\n}\n\n.mj-map-container[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 400px;\n width: 100%;\n}\n\n.mj-map-container.hidden[_ngcontent-%COMP%] {\n display: none;\n}\n\n\n\n\n\n[_nghost-%COMP%] .leaflet-popup-content-wrapper {\n border-radius: 6px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n[_nghost-%COMP%] .leaflet-popup-content {\n color: #1e293b;\n font-size: 13px;\n margin: 10px 14px;\n}\n\n\n\n[_nghost-%COMP%] .leaflet-container {\n width: 100% !important;\n height: 100% !important;\n}\n\n\n\n[_nghost-%COMP%] .leaflet-control-attribution {\n font-size: 9px;\n padding: 1px 5px;\n background: rgba(255, 255, 255, 0.6);\n color: #666;\n}\n\n[_nghost-%COMP%] .leaflet-control-attribution a {\n color: #666;\n}"] });
|
|
651
309
|
}
|
|
652
310
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MapViewComponent, [{
|
|
653
311
|
type: Component,
|
|
654
|
-
args: [{ standalone: false, selector: 'mj-map-view', template: "<div class=\"mj-map-view-wrapper\">\n <div class=\"mj-map-toolbar\">\n <div class=\"mj-map-mode-buttons\">\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'point'\"\n (click)=\"SetRenderMode('point')\">\n <i class=\"fa-solid fa-location-dot\"></i> Points\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'choropleth'\"\n (click)=\"SetRenderMode('choropleth')\">\n <i class=\"fa-solid fa-map\"></i> Regions\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'heatmap'\"\n (click)=\"SetRenderMode('heatmap')\">\n <i class=\"fa-solid fa-fire\"></i> Heat Map\n </button>\n </div>\n <div class=\"mj-map-info\">\n {{ MarkerCount }} location{{ MarkerCount !== 1 ? 's' : '' }} mapped\n @if (IsTruncated) {\n <span class=\"mj-map-truncated\" title=\"Not all records are displayed. Showing {{ Records.length | number }} of {{ TotalRecordCount | number }} total records.\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Showing {{ Records.length | number }} of {{ TotalRecordCount | number }}\n </span>\n }\n </div>\n </div>\n @if (IsLoading) {\n <div class=\"mj-map-loading\">\n <mj-loading text=\"Loading map...\"></mj-loading>\n </div>\n }\n <div #mapContainer class=\"mj-map-container\"></div>\n</div>\n", styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.mj-map-view-wrapper {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mj-map-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n z-index: 1000;\n}\n\n.mj-map-mode-buttons {\n display: flex;\n gap: 4px;\n}\n\n.mj-map-mode-btn {\n padding: 6px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 13px;\n transition: all 0.15s ease;\n}\n\n.mj-map-mode-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.mj-map-mode-btn.active {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-map-info {\n font-size: 13px;\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-map-truncated {\n color: var(--mj-status-warning-text);\n font-size: 12px;\n}\n\n.mj-map-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 500;\n background: var(--mj-bg-surface);\n}\n\n.mj-map-container {\n flex: 1;\n min-height: 400px;\n width: 100%;\n}\n\n.mj-map-container.hidden {\n display: none;\n}\n\n/* Leaflet popup styling \u2014 use hardcoded colors, NOT design tokens.\n Leaflet has its own light theme and popups are always white background.\n Using --mj-text-primary would make text white in dark mode = invisible. */\n:host ::ng-deep .leaflet-popup-content-wrapper {\n border-radius: 6px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n:host ::ng-deep .leaflet-popup-content {\n color: #1e293b;\n font-size: 13px;\n margin: 10px 14px;\n}\n\n/* Ensure Leaflet container fills its parent */\n:host ::ng-deep .leaflet-container {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Compact attribution \u2014 required by OSM terms but unobtrusive */\n:host ::ng-deep .leaflet-control-attribution {\n font-size: 9px;\n padding: 1px 5px;\n background: rgba(255, 255, 255, 0.6);\n color: #666;\n}\n\n:host ::ng-deep .leaflet-control-attribution a {\n color: #666;\n}\n"] }]
|
|
312
|
+
args: [{ standalone: false, selector: 'mj-map-view', template: "<div class=\"mj-map-view-wrapper\">\n <div class=\"mj-map-toolbar\">\n <div class=\"mj-map-mode-buttons\">\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'point'\"\n (click)=\"SetRenderMode('point')\">\n <i class=\"fa-solid fa-location-dot\"></i> Points\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'boundary'\"\n (click)=\"SetRenderMode('boundary')\">\n <i class=\"fa-solid fa-draw-polygon\"></i> Boundary\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'choropleth'\"\n (click)=\"SetRenderMode('choropleth')\">\n <i class=\"fa-solid fa-map\"></i> Regions\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'heatmap'\"\n (click)=\"SetRenderMode('heatmap')\">\n <i class=\"fa-solid fa-fire\"></i> Heat Map\n </button>\n </div>\n <div class=\"mj-map-info\">\n {{ MarkerCount }} location{{ MarkerCount !== 1 ? 's' : '' }} mapped\n @if (IsTruncated) {\n <span class=\"mj-map-truncated\" title=\"Not all records are displayed. Showing {{ Records.length | number }} of {{ TotalRecordCount | number }} total records.\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Showing {{ Records.length | number }} of {{ TotalRecordCount | number }}\n </span>\n }\n </div>\n </div>\n @if (IsLoading) {\n <div class=\"mj-map-loading\">\n <mj-loading text=\"Loading map...\"></mj-loading>\n </div>\n }\n <div #mapContainer class=\"mj-map-container\"></div>\n</div>\n", styles: [":host {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.mj-map-view-wrapper {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n.mj-map-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n background: var(--mj-bg-surface-card);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n z-index: 1000;\n}\n\n.mj-map-mode-buttons {\n display: flex;\n gap: 4px;\n}\n\n.mj-map-mode-btn {\n padding: 6px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 13px;\n transition: all 0.15s ease;\n}\n\n.mj-map-mode-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.mj-map-mode-btn.active {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-map-info {\n font-size: 13px;\n color: var(--mj-text-muted);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.mj-map-truncated {\n color: var(--mj-status-warning-text);\n font-size: 12px;\n}\n\n.mj-map-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 500;\n background: var(--mj-bg-surface);\n}\n\n.mj-map-container {\n flex: 1;\n min-height: 400px;\n width: 100%;\n}\n\n.mj-map-container.hidden {\n display: none;\n}\n\n/* Leaflet popup styling \u2014 use hardcoded colors, NOT design tokens.\n Leaflet has its own light theme and popups are always white background.\n Using --mj-text-primary would make text white in dark mode = invisible. */\n:host ::ng-deep .leaflet-popup-content-wrapper {\n border-radius: 6px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n}\n\n:host ::ng-deep .leaflet-popup-content {\n color: #1e293b;\n font-size: 13px;\n margin: 10px 14px;\n}\n\n/* Ensure Leaflet container fills its parent */\n:host ::ng-deep .leaflet-container {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Compact attribution \u2014 required by OSM terms but unobtrusive */\n:host ::ng-deep .leaflet-control-attribution {\n font-size: 9px;\n padding: 1px 5px;\n background: rgba(255, 255, 255, 0.6);\n color: #666;\n}\n\n:host ::ng-deep .leaflet-control-attribution a {\n color: #666;\n}\n"] }]
|
|
655
313
|
}], null, { mapContainer: [{
|
|
656
314
|
type: ViewChild,
|
|
657
315
|
args: ['mapContainer', { static: false }]
|
|
@@ -665,6 +323,8 @@ export class MapViewComponent {
|
|
|
665
323
|
type: Input
|
|
666
324
|
}], RenderMode: [{
|
|
667
325
|
type: Input
|
|
326
|
+
}], BoundaryField: [{
|
|
327
|
+
type: Input
|
|
668
328
|
}], DisplayState: [{
|
|
669
329
|
type: Input
|
|
670
330
|
}], TotalRecordCount: [{
|
|
@@ -678,5 +338,5 @@ export class MapViewComponent {
|
|
|
678
338
|
}], RenderModeChange: [{
|
|
679
339
|
type: Output
|
|
680
340
|
}] }); })();
|
|
681
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MapViewComponent, { className: "MapViewComponent", filePath: "src/lib/map-view.component.ts", lineNumber:
|
|
341
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MapViewComponent, { className: "MapViewComponent", filePath: "src/lib/map-view.component.ts", lineNumber: 44 }); })();
|
|
682
342
|
//# sourceMappingURL=map-view.component.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-view.component.js","sourceRoot":"","sources":["../../src/lib/map-view.component.ts","../../src/lib/map-view.component.html"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n OnInit,\n OnDestroy,\n OnChanges,\n SimpleChanges,\n ElementRef,\n ViewChild,\n ChangeDetectorRef,\n AfterViewInit,\n inject\n} from '@angular/core';\nimport { EntityInfo } from '@memberjunction/core';\nimport { GeoDataEngine } from '@memberjunction/core-entities';\nimport { MapRenderMode, MapDisplayState, MapMarkerClickEvent, MapRegionClickEvent } from './map-view.types';\n// Leaflet is loaded as a global script at runtime via angular.json scripts array.\n// The L namespace is available globally — see leaflet.d.ts for type declarations.\n\n/**\n * MapViewComponent — Leaflet-based map visualization for geo-enabled MJ entities.\n *\n * Reads `__mj_Latitude` and `__mj_Longitude` from entity records (or custom field overrides)\n * and renders them as interactive markers on an OpenStreetMap tile layer.\n *\n * Supports three rendering modes:\n * - **point** (default): Individual markers with optional clustering\n * - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)\n * - **heatmap**: Density visualization (future)\n *\n * Integrates into MJExplorer's EntityViewer as the 'map' view mode.\n */\n@Component({\n standalone: false,\n selector: 'mj-map-view',\n templateUrl: './map-view.component.html',\n styleUrls: ['./map-view.component.css']\n})\nexport class MapViewComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {\n private cdr = inject(ChangeDetectorRef);\n\n @ViewChild('mapContainer', { static: false })\n private mapContainer!: ElementRef<HTMLDivElement>;\n\n /** Entity metadata for the displayed records. */\n @Input() Entity!: EntityInfo;\n\n /** Array of entity records to render on the map. Accepts BaseEntity[] or plain Record objects. */\n @Input() Records: Record<string, unknown>[] = [];\n\n /** Latitude field name. Defaults to __mj_Latitude. */\n @Input() LatitudeField: string = '__mj_Latitude';\n\n /** Longitude field name. Defaults to __mj_Longitude. */\n @Input() LongitudeField: string = '__mj_Longitude';\n\n /** Current render mode. */\n @Input() RenderMode: MapRenderMode = 'point';\n\n /** Persisted display state from UserView. */\n @Input() DisplayState: Partial<MapDisplayState> | null = null;\n\n /**\n * Total record count from the server (before any client-side cap).\n * When this exceeds the number of Records passed in, the map shows\n * a truncation indicator so the user knows not all data is displayed.\n */\n @Input() TotalRecordCount: number = 0;\n\n /** Emitted when a marker is clicked. */\n @Output() MarkerClick = new EventEmitter<MapMarkerClickEvent>();\n\n /** Emitted when a choropleth region is clicked. */\n @Output() RegionClick = new EventEmitter<MapRegionClickEvent>();\n\n /** Emitted when display state changes (for persistence). */\n @Output() DisplayStateChange = new EventEmitter<MapDisplayState>();\n\n /** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */\n @Output() RenderModeChange = new EventEmitter<MapRenderMode>();\n\n private map: L.Map | null = null;\n private markerLayer: L.LayerGroup | null = null;\n private visibilityObserver: IntersectionObserver | null = null;\n private pendingRender = false;\n IsLoading = true;\n MarkerCount = 0;\n\n /** Whether the displayed records are truncated (more exist than were loaded). */\n get IsTruncated(): boolean {\n return this.TotalRecordCount > 0 && this.Records.length < this.TotalRecordCount;\n }\n\n ngOnInit(): void {\n // Map initialization happens when the component becomes visible\n }\n\n ngAfterViewInit(): void {\n // Use IntersectionObserver to detect when the map container becomes visible.\n // Leaflet requires the container to have actual dimensions before initialization.\n // Since the map starts hidden (grid view is default), we defer init until visible.\n this.visibilityObserver = new IntersectionObserver((entries) => {\n const entry = entries[0];\n if (entry.isIntersecting && !this.map) {\n // First time visible — initialize the map\n this.InitializeMap();\n } else if (entry.isIntersecting && this.map) {\n // Becoming visible again — fix tile rendering and re-fit\n this.map.invalidateSize();\n if (this.pendingRender) {\n this.RenderMarkers();\n this.pendingRender = false;\n }\n }\n }, { threshold: 0.1 });\n\n if (this.mapContainer?.nativeElement) {\n this.visibilityObserver.observe(this.mapContainer.nativeElement);\n }\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['Records'] || changes['RenderMode']) {\n if (this.map) {\n setTimeout(() => {\n if (this.map) {\n this.map.invalidateSize();\n this.RenderMarkers();\n }\n }, 100);\n } else {\n // Map not initialized yet — mark for render when visible\n this.pendingRender = true;\n }\n }\n }\n\n ngOnDestroy(): void {\n if (this.visibilityObserver) {\n this.visibilityObserver.disconnect();\n this.visibilityObserver = null;\n }\n if (this.map) {\n this.map.remove();\n this.map = null;\n }\n }\n\n /**\n * Initialize the Leaflet map with OSM tiles.\n */\n private InitializeMap(): void {\n if (!this.mapContainer?.nativeElement || this.map) return;\n\n const center: L.LatLngExpression = [\n this.DisplayState?.CenterLat ?? 20,\n this.DisplayState?.CenterLng ?? 0\n ];\n const zoom = this.DisplayState?.ZoomLevel ?? 2;\n\n this.map = L.map(this.mapContainer.nativeElement, {\n center,\n zoom,\n zoomControl: true,\n attributionControl: false\n });\n\n // Compact attribution — required by OSM terms but styled to be unobtrusive\n L.control.attribution({ prefix: false, position: 'bottomright' }).addTo(this.map);\n\n L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© <a href=\"https://www.openstreetmap.org/copyright\">OSM</a>',\n maxZoom: 18\n }).addTo(this.map);\n\n this.markerLayer = L.layerGroup().addTo(this.map);\n\n // Persist view state on move/zoom\n this.map.on('moveend', () => this.EmitDisplayState());\n\n // Set up popup click handlers for record drill-through\n this.SetupPopupClickHandler();\n\n // invalidateSize after a short delay to ensure tile rendering is correct\n // then render markers and fit bounds\n setTimeout(() => {\n if (this.map) {\n this.map.invalidateSize();\n this.RenderMarkers();\n this.IsLoading = false;\n this.cdr.detectChanges();\n\n // Double-invalidate after tiles start loading to fix partial rendering\n setTimeout(() => {\n if (this.map) this.map.invalidateSize();\n }, 500);\n }\n }, 100);\n }\n\n /**\n * Render map content based on the current RenderMode.\n */\n private RenderMarkers(): void {\n if (!this.map || !this.markerLayer) return;\n\n this.markerLayer.clearLayers();\n\n switch (this.RenderMode) {\n case 'point':\n this.RenderPointMarkers();\n break;\n case 'heatmap':\n this.RenderHeatmap();\n break;\n case 'choropleth':\n this.RenderChoropleth();\n break;\n default:\n this.RenderPointMarkers();\n }\n }\n\n /**\n * Point mode: individual markers with marker clustering for dense areas.\n * Uses leaflet.markercluster when available, falls back to plain markers.\n */\n private RenderPointMarkers(): void {\n const bounds: L.LatLng[] = [];\n\n // Use marker cluster group if available (loaded via CDN)\n const useCluster = typeof L.markerClusterGroup === 'function';\n const clusterGroup = useCluster ? L.markerClusterGroup({\n maxClusterRadius: 50,\n spiderfyOnMaxZoom: true,\n showCoverageOnHover: false\n }) : null;\n\n for (const record of this.Records) {\n const lat = this.GetNumericField(record, this.LatitudeField);\n const lng = this.GetNumericField(record, this.LongitudeField);\n\n if (lat == null || lng == null || isNaN(lat) || isNaN(lng)) continue;\n\n const latLng = L.latLng(lat, lng);\n bounds.push(latLng);\n\n const marker = L.marker(latLng);\n\n const nameField = this.Entity?.NameField;\n const label = nameField ? String(this.GetField(record, nameField.Name) ?? '') : 'Record';\n marker.bindPopup(`<b>${this.EscapeHtml(label)}</b>`);\n\n marker.on('click', () => {\n const pkFields = this.Entity?.PrimaryKeys ?? [];\n const recordId = pkFields.map(pk => this.GetField(record, pk.Name)).join('||');\n this.MarkerClick.emit({\n RecordID: recordId,\n Latitude: lat,\n Longitude: lng,\n Record: record\n });\n });\n\n if (clusterGroup) {\n clusterGroup.addLayer(marker);\n } else {\n this.markerLayer!.addLayer(marker);\n }\n }\n\n if (clusterGroup) {\n this.markerLayer!.addLayer(clusterGroup as L.Layer);\n }\n\n this.MarkerCount = bounds.length;\n this.FitBoundsToMarkers(bounds);\n }\n\n /**\n * Heatmap mode: density visualization using colored circle markers.\n * Larger/more opaque circles where records cluster together.\n * Click a cluster bubble to see matching records in a popup.\n */\n private RenderHeatmap(): void {\n const bounds: L.LatLng[] = [];\n const recordsWithCoords: { lat: number; lng: number; record: Record<string, unknown> }[] = [];\n\n for (const record of this.Records) {\n const lat = this.GetNumericField(record, this.LatitudeField);\n const lng = this.GetNumericField(record, this.LongitudeField);\n if (lat == null || lng == null || isNaN(lat) || isNaN(lng)) continue;\n bounds.push(L.latLng(lat, lng));\n recordsWithCoords.push({ lat, lng, record });\n }\n\n // Cluster nearby records (within ~2 degrees / ~200km) into groups\n const clusters = this.SpatialCluster(recordsWithCoords, 2.0);\n\n for (const cluster of clusters) {\n const radius = Math.min(12 + cluster.records.length * 5, 40);\n const opacity = Math.min(0.3 + cluster.records.length * 0.08, 0.85);\n\n const circle = L.circleMarker([cluster.centerLat, cluster.centerLng], {\n radius,\n fillColor: '#e74c3c',\n fillOpacity: opacity,\n color: '#c0392b',\n weight: 1,\n opacity: 0.7\n });\n\n circle.bindPopup(this.BuildClusterPopup(cluster.records, `${cluster.records.length} record${cluster.records.length !== 1 ? 's' : ''}`));\n\n circle.on('click', () => {\n this.RegionClick.emit({\n RegionName: `Cluster (${cluster.records.length} records)`,\n GroupBy: 'state_province',\n RecordCount: cluster.records.length,\n Records: cluster.records\n });\n });\n\n this.markerLayer!.addLayer(circle);\n }\n\n this.MarkerCount = bounds.length;\n this.FitBoundsToMarkers(bounds);\n }\n\n /**\n * Regions mode: shaded geographic regions using GeoJSON boundaries.\n * Uses coordinate-based point-in-polygon resolution via GeoDataEngine —\n * no dependency on text field names in the entity. Each record's lat/lng\n * is resolved to its containing country and state/province geometrically.\n *\n * Auto-detects grouping level:\n * - If records span multiple countries → group by Country, render country polygons\n * - If all records are in one country → group by State/Province, render state polygons\n * Falls back to spatial clustering if boundary data isn't available.\n */\n private RenderChoropleth(): void {\n const geo = GeoDataEngine.Instance;\n const bounds: L.LatLng[] = [];\n\n // Resolve each record's location via point-in-polygon\n const recordsByCountryId = new Map<string, { country: { ID: string; Name: string; BoundaryGeoJSON: string | null }; records: Record<string, unknown>[] }>();\n const recordsByStateId = new Map<string, { state: { ID: string; Name: string; BoundaryGeoJSON: string | null }; countryId: string; records: Record<string, unknown>[] }>();\n const unmatchedRecords: Record<string, unknown>[] = [];\n\n for (const record of this.Records) {\n const lat = this.GetNumericField(record, this.LatitudeField);\n const lng = this.GetNumericField(record, this.LongitudeField);\n if (lat == null || lng == null || isNaN(lat) || isNaN(lng)) continue;\n bounds.push(L.latLng(lat, lng));\n\n const resolution = geo.ResolvePointToLocation(lat, lng);\n\n if (resolution.Country) {\n const countryId = resolution.Country.ID.toLowerCase();\n let countryGroup = recordsByCountryId.get(countryId);\n if (!countryGroup) {\n countryGroup = { country: resolution.Country, records: [] };\n recordsByCountryId.set(countryId, countryGroup);\n }\n countryGroup.records.push(record);\n\n if (resolution.State) {\n const stateId = resolution.State.ID.toLowerCase();\n let stateGroup = recordsByStateId.get(stateId);\n if (!stateGroup) {\n stateGroup = { state: resolution.State, countryId, records: [] };\n recordsByStateId.set(stateId, stateGroup);\n }\n stateGroup.records.push(record);\n }\n } else {\n unmatchedRecords.push(record);\n }\n }\n\n this.MarkerCount = bounds.length;\n\n // Auto-detect grouping: single country → state-level, multiple → country-level\n if (recordsByCountryId.size <= 1 && recordsByStateId.size > 1) {\n this.RenderStateRegions(recordsByStateId, unmatchedRecords, bounds);\n } else {\n this.RenderCountryRegions(recordsByCountryId, unmatchedRecords, bounds);\n }\n }\n\n /**\n * Render state-level choropleth regions from coordinate-resolved groups.\n * Each group is keyed by state ID with its entity and records already resolved.\n */\n private RenderStateRegions(\n recordsByStateId: Map<string, { state: { ID: string; Name: string; BoundaryGeoJSON: string | null }; countryId: string; records: Record<string, unknown>[] }>,\n unmatchedRecords: Record<string, unknown>[],\n bounds: L.LatLng[]\n ): void {\n const colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',\n '#1abc9c', '#e67e22', '#2980b9', '#27ae60', '#c0392b',\n '#16a085', '#d35400', '#8e44ad', '#2c3e50', '#f1c40f'];\n let colorIdx = 0;\n\n for (const [, group] of recordsByStateId) {\n const color = colors[colorIdx % colors.length];\n const rendered = this.RenderBoundaryRegion(\n group.state.Name, group.state.BoundaryGeoJSON, group.records, color, 'state_province'\n );\n\n if (!rendered) {\n this.RenderCircleFallback(group.state.Name, group.records, color);\n }\n colorIdx++;\n }\n\n // Render unmatched records as a gray cluster\n if (unmatchedRecords.length > 0) {\n this.RenderCircleFallback('Unmatched', unmatchedRecords, '#95a5a6');\n }\n\n this.FitBoundsToMarkers(bounds);\n }\n\n /**\n * Render country-level choropleth regions from coordinate-resolved groups.\n */\n private RenderCountryRegions(\n recordsByCountryId: Map<string, { country: { ID: string; Name: string; BoundaryGeoJSON: string | null }; records: Record<string, unknown>[] }>,\n unmatchedRecords: Record<string, unknown>[],\n bounds: L.LatLng[]\n ): void {\n const colors = ['#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9b59b6',\n '#1abc9c', '#e67e22', '#2980b9', '#27ae60', '#c0392b',\n '#16a085', '#d35400', '#8e44ad', '#2c3e50', '#f1c40f'];\n let colorIdx = 0;\n\n for (const [, group] of recordsByCountryId) {\n const color = colors[colorIdx % colors.length];\n const rendered = this.RenderBoundaryRegion(\n group.country.Name, group.country.BoundaryGeoJSON, group.records, color, 'country'\n );\n\n if (!rendered) {\n this.RenderCircleFallback(group.country.Name, group.records, color);\n }\n colorIdx++;\n }\n\n if (unmatchedRecords.length > 0) {\n this.RenderCircleFallback('Unmatched', unmatchedRecords, '#95a5a6');\n }\n\n this.FitBoundsToMarkers(bounds);\n }\n\n /**\n * Render a single GeoJSON boundary region with shading.\n * Returns true if the boundary rendered successfully, false if fallback is needed.\n */\n private RenderBoundaryRegion(\n regionName: string,\n boundaryGeoJSON: string | null,\n records: Record<string, unknown>[],\n color: string,\n groupBy: 'country' | 'state_province'\n ): boolean {\n if (!boundaryGeoJSON) return false;\n\n try {\n const geojson = typeof boundaryGeoJSON === 'string'\n ? JSON.parse(boundaryGeoJSON)\n : boundaryGeoJSON;\n\n const layer = L.geoJSON(geojson, {\n style: {\n fillColor: color,\n fillOpacity: 0.35,\n color: color,\n weight: 2,\n opacity: 0.8\n }\n });\n\n layer.bindPopup(this.BuildClusterPopup(records, `${regionName} (${records.length})`));\n layer.on('click', () => {\n this.RegionClick.emit({\n RegionName: regionName,\n GroupBy: groupBy,\n RecordCount: records.length,\n Records: records\n });\n });\n\n this.markerLayer!.addLayer(layer as L.Layer);\n return true;\n } catch (e) {\n console.warn(`[MapView] Choropleth: GeoJSON render failed for \"${regionName}\"`, e);\n return false;\n }\n }\n\n /**\n * Render a colored circle marker as a fallback for regions without boundary data.\n */\n private RenderCircleFallback(\n regionName: string,\n records: Record<string, unknown>[],\n color: string\n ): void {\n let sumLat = 0, sumLng = 0, count = 0;\n for (const rec of records) {\n const lat = this.GetNumericField(rec, this.LatitudeField);\n const lng = this.GetNumericField(rec, this.LongitudeField);\n if (lat && lng) { sumLat += lat; sumLng += lng; count++; }\n }\n if (count > 0) {\n const radius = Math.min(15 + records.length * 5, 50);\n const circle = L.circleMarker([sumLat / count, sumLng / count], {\n radius, fillColor: color, fillOpacity: 0.45, color, weight: 2, opacity: 0.85\n });\n circle.bindPopup(this.BuildClusterPopup(records, `${regionName} (${records.length})`));\n this.markerLayer!.addLayer(circle);\n }\n }\n\n /**\n * Simple spatial clustering: group nearby records within a lat/lng radius.\n * Returns clusters with center coordinates and member records.\n */\n private SpatialCluster(\n items: { lat: number; lng: number; record: Record<string, unknown> }[],\n radiusDegrees: number\n ): { centerLat: number; centerLng: number; records: Record<string, unknown>[] }[] {\n const assigned = new Set<number>();\n const clusters: { centerLat: number; centerLng: number; records: Record<string, unknown>[] }[] = [];\n\n for (let i = 0; i < items.length; i++) {\n if (assigned.has(i)) continue;\n\n const seed = items[i];\n const members: Record<string, unknown>[] = [seed.record];\n let sumLat = seed.lat;\n let sumLng = seed.lng;\n assigned.add(i);\n\n // Find all unassigned neighbors within radius\n for (let j = i + 1; j < items.length; j++) {\n if (assigned.has(j)) continue;\n const candidate = items[j];\n if (Math.abs(candidate.lat - seed.lat) <= radiusDegrees &&\n Math.abs(candidate.lng - seed.lng) <= radiusDegrees) {\n members.push(candidate.record);\n sumLat += candidate.lat;\n sumLng += candidate.lng;\n assigned.add(j);\n }\n }\n\n clusters.push({\n centerLat: sumLat / members.length,\n centerLng: sumLng / members.length,\n records: members\n });\n }\n\n return clusters;\n }\n\n /**\n * Fit map bounds to encompass all marker coordinates.\n */\n private FitBoundsToMarkers(bounds: L.LatLng[]): void {\n if (bounds.length > 0 && this.map) {\n const boundsObj = L.latLngBounds(bounds);\n this.map.fitBounds(boundsObj, { padding: [30, 30], maxZoom: 14 });\n }\n this.cdr.detectChanges();\n }\n\n /**\n * Emit the current display state for persistence.\n */\n private EmitDisplayState(): void {\n if (!this.map) return;\n const center = this.map.getCenter();\n this.DisplayStateChange.emit({\n ZoomLevel: this.map.getZoom(),\n CenterLat: center.lat,\n CenterLng: center.lng,\n ClusterMarkers: true\n });\n }\n\n /**\n * Set rendering mode.\n */\n SetRenderMode(mode: MapRenderMode): void {\n this.RenderMode = mode;\n this.RenderModeChange.emit(mode);\n this.RenderMarkers();\n }\n\n /**\n * Get a field value from a record, supporting both BaseEntity (with .Get()) and plain objects.\n */\n private GetField(record: Record<string, unknown>, fieldName: string): unknown {\n if ('Get' in record && typeof record['Get'] === 'function') {\n return (record as { Get: (name: string) => unknown }).Get(fieldName);\n }\n return record[fieldName];\n }\n\n /**\n * Get a numeric field value from a record, returning null if not a valid number.\n */\n private GetNumericField(record: Record<string, unknown>, fieldName: string): number | null {\n const val = this.GetField(record, fieldName);\n if (val == null) return null;\n const num = Number(val);\n return isNaN(num) ? null : num;\n }\n\n /**\n * Build a popup for a cluster of records.\n * Shows first 5 records as clickable links, then \"and X more...\" if needed.\n * Clicking a record name emits MarkerClick so the parent can open it.\n */\n private BuildClusterPopup(records: Record<string, unknown>[], title: string): string {\n const nameField = this.Entity?.NameField;\n const maxShow = 5;\n const shown = records.slice(0, maxShow);\n const remaining = records.length - maxShow;\n\n let html = `<div style=\"font-size:12px;min-width:160px;\">` +\n `<b>${this.EscapeHtml(title)}</b>` +\n `<hr style=\"margin:4px 0;border-color:#e5e7eb;\">`;\n\n for (const rec of shown) {\n const name = nameField ? String(this.GetField(rec, nameField.Name) ?? '') : 'Record';\n const pkFields = this.Entity?.PrimaryKeys ?? [];\n const recordId = pkFields.map(pk => this.GetField(rec, pk.Name)).join('||');\n // Clickable record name — uses a data attribute that the click handler reads\n html += `<div style=\"padding:2px 0;cursor:pointer;color:#2563eb;\" ` +\n `class=\"mj-map-popup-record\" data-record-id=\"${this.EscapeHtml(recordId)}\">` +\n `${this.EscapeHtml(name)}</div>`;\n }\n\n if (remaining > 0) {\n html += `<div style=\"padding:4px 0 0;color:#6b7280;font-style:italic;\">` +\n `and ${remaining} more...</div>`;\n }\n\n html += `</div>`;\n return html;\n }\n\n /**\n * Set up popup click handlers after the map initializes.\n * Listens for clicks on record links in popups and emits MarkerClick.\n */\n private SetupPopupClickHandler(): void {\n if (!this.map) return;\n this.map.on('popupopen', () => {\n // Attach click handlers to all popup record links\n setTimeout(() => {\n const links = document.querySelectorAll('.mj-map-popup-record');\n links.forEach(link => {\n link.addEventListener('click', (e) => {\n const recordId = (e.currentTarget as HTMLElement).getAttribute('data-record-id') ?? '';\n // Find the matching record\n const record = this.Records.find(r => {\n const pkFields = this.Entity?.PrimaryKeys ?? [];\n const id = pkFields.map(pk => this.GetField(r, pk.Name)).join('||');\n return id === recordId;\n });\n if (record) {\n const lat = this.GetNumericField(record, this.LatitudeField) ?? 0;\n const lng = this.GetNumericField(record, this.LongitudeField) ?? 0;\n\n this.MarkerClick.emit({\n RecordID: recordId,\n Latitude: lat,\n Longitude: lng,\n Record: record\n });\n }\n });\n });\n }, 50);\n });\n }\n\n /**\n * Escape HTML for popup content.\n */\n private EscapeHtml(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n}\n","<div class=\"mj-map-view-wrapper\">\n <div class=\"mj-map-toolbar\">\n <div class=\"mj-map-mode-buttons\">\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'point'\"\n (click)=\"SetRenderMode('point')\">\n <i class=\"fa-solid fa-location-dot\"></i> Points\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'choropleth'\"\n (click)=\"SetRenderMode('choropleth')\">\n <i class=\"fa-solid fa-map\"></i> Regions\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'heatmap'\"\n (click)=\"SetRenderMode('heatmap')\">\n <i class=\"fa-solid fa-fire\"></i> Heat Map\n </button>\n </div>\n <div class=\"mj-map-info\">\n {{ MarkerCount }} location{{ MarkerCount !== 1 ? 's' : '' }} mapped\n @if (IsTruncated) {\n <span class=\"mj-map-truncated\" title=\"Not all records are displayed. Showing {{ Records.length | number }} of {{ TotalRecordCount | number }} total records.\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Showing {{ Records.length | number }} of {{ TotalRecordCount | number }}\n </span>\n }\n </div>\n </div>\n @if (IsLoading) {\n <div class=\"mj-map-loading\">\n <mj-loading text=\"Loading map...\"></mj-loading>\n </div>\n }\n <div #mapContainer class=\"mj-map-container\"></div>\n</div>\n"]}
|
|
1
|
+
{"version":3,"file":"map-view.component.js","sourceRoot":"","sources":["../../src/lib/map-view.component.ts","../../src/lib/map-view.component.html"],"names":[],"mappings":"AAAA,OAAO,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,YAAY,EAMZ,SAAS,EACT,iBAAiB,EAEjB,MAAM,EACT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAE9D,OAAO,KAAK,OAAO,MAAM,0BAA0B,CAAC;;;;;;ICSpC,gCAA8J;;;IAC1J,wBAAgD;IAAC,YACrD;;;IAAA,iBAAO;;;IAFwB,uBAAA,mLAA8H,CAAA;IACxG,eACrD;IADqD,4IACrD;;;IAKR,+BAA4B;IACxB,iCAA+C;IACnD,iBAAM;;ADjBd,kFAAkF;AAClF,kFAAkF;AAElF;;;;;;;;;;;;;;GAcG;AAOH,MAAM,OAAO,gBAAgB;IACjB,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAGhC,YAAY,CAA8B;IAElD,iDAAiD;IACxC,MAAM,CAAc;IAE7B,kGAAkG;IACzF,OAAO,GAA8B,EAAE,CAAC;IAEjD,sDAAsD;IAC7C,aAAa,GAAW,eAAe,CAAC;IAEjD,wDAAwD;IAC/C,cAAc,GAAW,gBAAgB,CAAC;IAEnD,2BAA2B;IAClB,UAAU,GAAkB,OAAO,CAAC;IAE7C,sFAAsF;IAC7E,aAAa,GAAW,iBAAiB,CAAC;IAGnD,6CAA6C;IACpC,YAAY,GAAoC,IAAI,CAAC;IAE9D;;;;OAIG;IACM,gBAAgB,GAAW,CAAC,CAAC;IAEtC,wCAAwC;IAC9B,WAAW,GAAG,IAAI,YAAY,EAAuB,CAAC;IAEhE,mDAAmD;IACzC,WAAW,GAAG,IAAI,YAAY,EAAuB,CAAC;IAEhE,4DAA4D;IAClD,kBAAkB,GAAG,IAAI,YAAY,EAAmB,CAAC;IAEnE,mFAAmF;IACzE,gBAAgB,GAAG,IAAI,YAAY,EAAiB,CAAC;IAEvD,MAAM,GAA6B,IAAI,CAAC;IACxC,kBAAkB,GAAgC,IAAI,CAAC;IACvD,aAAa,GAAG,KAAK,CAAC;IAC9B,SAAS,GAAG,IAAI,CAAC;IACjB,WAAW,GAAG,CAAC,CAAC;IAEhB,iFAAiF;IACjF,IAAI,WAAW;QACX,OAAO,IAAI,CAAC,gBAAgB,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC;IACpF,CAAC;IAED,QAAQ;QACJ,gEAAgE;IACpE,CAAC;IAED,eAAe;QACX,6EAA6E;QAC7E,kFAAkF;QAClF,mFAAmF;QACnF,IAAI,CAAC,kBAAkB,GAAG,IAAI,oBAAoB,CAAC,CAAC,OAAO,EAAE,EAAE;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YACzB,IAAI,KAAK,CAAC,cAAc,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACvC,0CAA0C;gBAC1C,IAAI,CAAC,aAAa,EAAE,CAAC;YACzB,CAAC;iBAAM,IAAI,KAAK,CAAC,cAAc,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC7C,yDAAyD;gBACzD,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAC7B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;oBACrB,IAAI,CAAC,QAAQ,EAAE,CAAC;oBAChB,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;gBAC/B,CAAC;YACL,CAAC;QACL,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QAEvB,IAAI,IAAI,CAAC,YAAY,EAAE,aAAa,EAAE,CAAC;YACnC,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC;QACrE,CAAC;IACL,CAAC;IAED,WAAW,CAAC,OAAsB;QAC9B,IAAI,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9C,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,UAAU,CAAC,GAAG,EAAE;oBACZ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;wBACd,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;wBAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACpB,CAAC;gBACL,CAAC,EAAE,GAAG,CAAC,CAAC;YACZ,CAAC;iBAAM,CAAC;gBACJ,yDAAyD;gBACzD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;YAC9B,CAAC;QACL,CAAC;IACL,CAAC;IAED,WAAW;QACP,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC1B,IAAI,CAAC,kBAAkB,CAAC,UAAU,EAAE,CAAC;YACrC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACnC,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACvB,CAAC;IACL,CAAC;IAED;;OAEG;IACK,aAAa;QACjB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,aAAa,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QAE7D,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YAC/B,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,aAAa;YAC1C,MAAM,EAAE;gBACJ,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,IAAI,EAAE;gBACvC,GAAG,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,IAAI,CAAC;aACzC;YACD,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,IAAI,CAAC;YACvC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,cAAc,EAAE,IAAI,CAAC,cAAc;YACnC,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,WAAW,EAAE,aAAa,CAAC,QAAQ;YACnC,WAAW,EAAE,CAAC,MAA+B,EAAE,EAAE;gBAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,IAAI,EAAE,CAAC;gBAChD,OAAO,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACzE,CAAC;YACD,aAAa,EAAE,CAAC,MAA+B,EAAE,EAAE;gBAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC;gBACzC,OAAO,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;YACtF,CAAC;YACD,aAAa,EAAE,CAAC,KAA+B,EAAE,EAAE;gBAC/C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;oBAClB,QAAQ,EAAE,KAAK,CAAC,QAAQ;oBACxB,QAAQ,EAAE,KAAK,CAAC,GAAG;oBACnB,SAAS,EAAE,KAAK,CAAC,GAAG;oBACpB,MAAM,EAAE,KAAK,CAAC,MAAM;iBACvB,CAAC,CAAC;YACP,CAAC;YACD,aAAa,EAAE,CAAC,KAA+B,EAAE,EAAE;gBAC/C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;oBAClB,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,OAAO,EAAE,KAAK,CAAC,OAAuC;oBACtD,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;iBACzB,CAAC,CAAC;YACP,CAAC;YACD,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE;gBACrC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;oBACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,IAAI,EAAE,CAAC;oBAChD,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACpE,OAAO,EAAE,KAAK,QAAQ,CAAC;gBAC3B,CAAC,CAAC,CAAC;gBACH,IAAI,MAAM,EAAE,CAAC;oBACT,MAAM,GAAG,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;oBAClE,MAAM,GAAG,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;oBACnE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC;wBAClB,QAAQ,EAAE,QAAQ;wBAClB,QAAQ,EAAE,GAAG;wBACb,SAAS,EAAE,GAAG;wBACd,MAAM,EAAE,MAAM;qBACjB,CAAC,CAAC;gBACP,CAAC;YACL,CAAC;YACD,SAAS,EAAE,CAAC,KAA2B,EAAE,EAAE;gBACvC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;oBACzB,SAAS,EAAE,KAAK,CAAC,IAAI;oBACrB,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,SAAS,EAAE,KAAK,CAAC,SAAS;oBAC1B,cAAc,EAAE,IAAI;iBACvB,CAAC,CAAC;YACP,CAAC;YACD,eAAe,EAAE,CAAC;SACrB,CAAC,CAAC;QAEH,yEAAyE;QACzE,qCAAqC;QACrC,UAAU,CAAC,GAAG,EAAE;YACZ,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACd,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAChB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;gBACvB,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;gBAEzB,uEAAuE;gBACvE,UAAU,CAAC,GAAG,EAAE;oBACZ,IAAI,IAAI,CAAC,MAAM;wBAAE,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAClD,CAAC,EAAE,GAAG,CAAC,CAAC;YACZ,CAAC;QACL,CAAC,EAAE,GAAG,CAAC,CAAC;IACZ,CAAC;IAED;;OAEG;IACK,QAAQ;QACZ,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO;QACzB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC;QACtD,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;IAC7B,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,IAAmB;QAC7B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,QAAQ,EAAE,CAAC;IACpB,CAAC;IAED;;OAEG;IACK,QAAQ,CAAC,MAA+B,EAAE,SAAiB;QAC/D,IAAI,KAAK,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,UAAU,EAAE,CAAC;YACzD,OAAQ,MAA6C,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC;IAED;;OAEG;IACK,eAAe,CAAC,MAA+B,EAAE,SAAiB;QACtE,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,GAAG,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACxB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IACnC,CAAC;0GA5OQ,gBAAgB;6DAAhB,gBAAgB;;;;;;;YCxCjB,AADJ,AADJ,AADJ,8BAAiC,aACD,aACS,gBAGY;YAAjC,mIAAS,kBAAc,OAAO,CAAC,KAAC;YACpC,uBAAwC;YAAC,wBAC7C;YAAA,iBAAS;YACT,iCAE4C;YAApC,mIAAS,kBAAc,UAAU,CAAC,KAAC;YACvC,uBAAwC;YAAC,0BAC7C;YAAA,iBAAS;YACT,iCAE8C;YAAtC,mIAAS,kBAAc,YAAY,CAAC,KAAC;YACzC,wBAA+B;YAAC,0BACpC;YAAA,iBAAS;YACT,kCAE2C;YAAnC,oIAAS,kBAAc,SAAS,CAAC,KAAC;YACtC,wBAAgC;YAAC,2BACrC;YACJ,AADI,iBAAS,EACP;YACN,+BAAyB;YACrB,aACA;YAAA,uFAAmB;YAM3B,AADI,iBAAM,EACJ;YACN,qFAAiB;YAKjB,8BAAkD;YACtD,iBAAM;;YAnCc,eAAuC;YAAvC,oDAAuC;YAKvC,eAA0C;YAA1C,uDAA0C;YAK1C,eAA4C;YAA5C,yDAA4C;YAK5C,eAAyC;YAAzC,sDAAyC;YAMjD,eACA;YADA,sGACA;YAAA,cAIC;YAJD,2CAIC;YAGT,cAIC;YAJD,yCAIC;;;iFDMQ,gBAAgB;cAN5B,SAAS;6BACM,KAAK,YACP,aAAa;;kBAOtB,SAAS;mBAAC,cAAc,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;;kBAI3C,KAAK;;kBAGL,KAAK;;kBAGL,KAAK;;kBAGL,KAAK;;kBAGL,KAAK;;kBAGL,KAAK;;kBAIL,KAAK;;kBAOL,KAAK;;kBAGL,MAAM;;kBAGN,MAAM;;kBAGN,MAAM;;kBAGN,MAAM;;kFA7CE,gBAAgB","sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n OnInit,\n OnDestroy,\n OnChanges,\n SimpleChanges,\n ElementRef,\n ViewChild,\n ChangeDetectorRef,\n AfterViewInit,\n inject\n} from '@angular/core';\nimport { EntityInfo } from '@memberjunction/core';\nimport { GeoDataEngine } from '@memberjunction/core-entities';\nimport { MapRenderMode, MapDisplayState, MapMarkerClickEvent, MapRegionClickEvent } from './map-view.types';\nimport * as MapCore from '@memberjunction/geo-maps';\n// Leaflet is loaded as a global script at runtime via angular.json scripts array.\n// The L namespace is available globally — see leaflet.d.ts for type declarations.\n\n/**\n * MapViewComponent — Leaflet-based map visualization for geo-enabled MJ entities.\n *\n * Reads `__mj_Latitude` and `__mj_Longitude` from entity records (or custom field overrides)\n * and renders them as interactive markers on an OpenStreetMap tile layer.\n *\n * Supports three rendering modes:\n * - **point** (default): Individual markers with optional clustering\n * - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)\n * - **heatmap**: Density visualization\n *\n * Delegates all rendering logic to @memberjunction/geo-maps.\n * This component is a thin Angular wrapper managing lifecycle, inputs/outputs,\n * and IntersectionObserver-based deferred initialization.\n */\n@Component({\n standalone: false,\n selector: 'mj-map-view',\n templateUrl: './map-view.component.html',\n styleUrls: ['./map-view.component.css']\n})\nexport class MapViewComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {\n private cdr = inject(ChangeDetectorRef);\n\n @ViewChild('mapContainer', { static: false })\n private mapContainer!: ElementRef<HTMLDivElement>;\n\n /** Entity metadata for the displayed records. */\n @Input() Entity!: EntityInfo;\n\n /** Array of entity records to render on the map. Accepts BaseEntity[] or plain Record objects. */\n @Input() Records: Record<string, unknown>[] = [];\n\n /** Latitude field name. Defaults to __mj_Latitude. */\n @Input() LatitudeField: string = '__mj_Latitude';\n\n /** Longitude field name. Defaults to __mj_Longitude. */\n @Input() LongitudeField: string = '__mj_Longitude';\n\n /** Current render mode. */\n @Input() RenderMode: MapRenderMode = 'point';\n\n /** Field name containing GeoJSON boundary data on each record (for boundary mode). */\n @Input() BoundaryField: string = 'BoundaryGeoJSON';\n\n\n /** Persisted display state from UserView. */\n @Input() DisplayState: Partial<MapDisplayState> | null = null;\n\n /**\n * Total record count from the server (before any client-side cap).\n * When this exceeds the number of Records passed in, the map shows\n * a truncation indicator so the user knows not all data is displayed.\n */\n @Input() TotalRecordCount: number = 0;\n\n /** Emitted when a marker is clicked. */\n @Output() MarkerClick = new EventEmitter<MapMarkerClickEvent>();\n\n /** Emitted when a choropleth region is clicked. */\n @Output() RegionClick = new EventEmitter<MapRegionClickEvent>();\n\n /** Emitted when display state changes (for persistence). */\n @Output() DisplayStateChange = new EventEmitter<MapDisplayState>();\n\n /** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */\n @Output() RenderModeChange = new EventEmitter<MapRenderMode>();\n\n private engine: MapCore.MapEngine | null = null;\n private visibilityObserver: IntersectionObserver | null = null;\n private pendingRender = false;\n IsLoading = true;\n MarkerCount = 0;\n\n /** Whether the displayed records are truncated (more exist than were loaded). */\n get IsTruncated(): boolean {\n return this.TotalRecordCount > 0 && this.Records.length < this.TotalRecordCount;\n }\n\n ngOnInit(): void {\n // Map initialization happens when the component becomes visible\n }\n\n ngAfterViewInit(): void {\n // Use IntersectionObserver to detect when the map container becomes visible.\n // Leaflet requires the container to have actual dimensions before initialization.\n // Since the map starts hidden (grid view is default), we defer init until visible.\n this.visibilityObserver = new IntersectionObserver((entries) => {\n const entry = entries[0];\n if (entry.isIntersecting && !this.engine) {\n // First time visible — initialize the map\n this.InitializeMap();\n } else if (entry.isIntersecting && this.engine) {\n // Becoming visible again — fix tile rendering and re-fit\n this.engine.invalidateSize();\n if (this.pendingRender) {\n this.DoRender();\n this.pendingRender = false;\n }\n }\n }, { threshold: 0.1 });\n\n if (this.mapContainer?.nativeElement) {\n this.visibilityObserver.observe(this.mapContainer.nativeElement);\n }\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['Records'] || changes['RenderMode']) {\n if (this.engine) {\n setTimeout(() => {\n if (this.engine) {\n this.engine.invalidateSize();\n this.DoRender();\n }\n }, 100);\n } else {\n // Map not initialized yet — mark for render when visible\n this.pendingRender = true;\n }\n }\n }\n\n ngOnDestroy(): void {\n if (this.visibilityObserver) {\n this.visibilityObserver.disconnect();\n this.visibilityObserver = null;\n }\n if (this.engine) {\n this.engine.destroy();\n this.engine = null;\n }\n }\n\n /**\n * Initialize the map engine via MapCore.\n */\n private InitializeMap(): void {\n if (!this.mapContainer?.nativeElement || this.engine) return;\n\n this.engine = MapCore.createEngine({\n container: this.mapContainer.nativeElement,\n center: {\n lat: this.DisplayState?.CenterLat ?? 20,\n lng: this.DisplayState?.CenterLng ?? 0\n },\n zoom: this.DisplayState?.ZoomLevel ?? 2,\n latitudeField: this.LatitudeField,\n longitudeField: this.LongitudeField,\n boundaryField: this.BoundaryField,\n geoResolver: GeoDataEngine.Instance,\n getRecordId: (record: Record<string, unknown>) => {\n const pkFields = this.Entity?.PrimaryKeys ?? [];\n return pkFields.map(pk => this.GetField(record, pk.Name)).join('||');\n },\n getRecordName: (record: Record<string, unknown>) => {\n const nameField = this.Entity?.NameField;\n return nameField ? String(this.GetField(record, nameField.Name) ?? '') : 'Record';\n },\n onMarkerClick: (event: MapCore.MarkerClickEvent) => {\n this.MarkerClick.emit({\n RecordID: event.recordId,\n Latitude: event.lat,\n Longitude: event.lng,\n Record: event.record\n });\n },\n onRegionClick: (event: MapCore.RegionClickEvent) => {\n this.RegionClick.emit({\n RegionName: event.regionName,\n GroupBy: event.groupBy as 'country' | 'state_province',\n RecordCount: event.recordCount,\n Records: event.records\n });\n },\n onPopupRecordClick: (recordId: string) => {\n const record = this.Records.find(r => {\n const pkFields = this.Entity?.PrimaryKeys ?? [];\n const id = pkFields.map(pk => this.GetField(r, pk.Name)).join('||');\n return id === recordId;\n });\n if (record) {\n const lat = this.GetNumericField(record, this.LatitudeField) ?? 0;\n const lng = this.GetNumericField(record, this.LongitudeField) ?? 0;\n this.MarkerClick.emit({\n RecordID: recordId,\n Latitude: lat,\n Longitude: lng,\n Record: record\n });\n }\n },\n onMoveEnd: (state: MapCore.MoveEndEvent) => {\n this.DisplayStateChange.emit({\n ZoomLevel: state.zoom,\n CenterLat: state.centerLat,\n CenterLng: state.centerLng,\n ClusterMarkers: true\n });\n },\n maxPopupRecords: 5\n });\n\n // invalidateSize after a short delay to ensure tile rendering is correct\n // then render markers and fit bounds\n setTimeout(() => {\n if (this.engine) {\n this.engine.invalidateSize();\n this.DoRender();\n this.IsLoading = false;\n this.cdr.detectChanges();\n\n // Double-invalidate after tiles start loading to fix partial rendering\n setTimeout(() => {\n if (this.engine) this.engine.invalidateSize();\n }, 500);\n }\n }, 100);\n }\n\n /**\n * Render records using the current mode, update marker count.\n */\n private DoRender(): void {\n if (!this.engine) return;\n this.engine.render(this.Records, this.RenderMode);\n this.MarkerCount = this.engine.getStats().markerCount;\n this.cdr.detectChanges();\n }\n\n /**\n * Set rendering mode (called from template toolbar buttons).\n */\n SetRenderMode(mode: MapRenderMode): void {\n this.RenderMode = mode;\n this.RenderModeChange.emit(mode);\n this.DoRender();\n }\n\n /**\n * Get a field value from a record, supporting both BaseEntity (with .Get()) and plain objects.\n */\n private GetField(record: Record<string, unknown>, fieldName: string): unknown {\n if ('Get' in record && typeof record['Get'] === 'function') {\n return (record as { Get: (name: string) => unknown }).Get(fieldName);\n }\n return record[fieldName];\n }\n\n /**\n * Get a numeric field value from a record, returning null if not a valid number.\n */\n private GetNumericField(record: Record<string, unknown>, fieldName: string): number | null {\n const val = this.GetField(record, fieldName);\n if (val == null) return null;\n const num = Number(val);\n return isNaN(num) ? null : num;\n }\n}\n","<div class=\"mj-map-view-wrapper\">\n <div class=\"mj-map-toolbar\">\n <div class=\"mj-map-mode-buttons\">\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'point'\"\n (click)=\"SetRenderMode('point')\">\n <i class=\"fa-solid fa-location-dot\"></i> Points\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'boundary'\"\n (click)=\"SetRenderMode('boundary')\">\n <i class=\"fa-solid fa-draw-polygon\"></i> Boundary\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'choropleth'\"\n (click)=\"SetRenderMode('choropleth')\">\n <i class=\"fa-solid fa-map\"></i> Regions\n </button>\n <button class=\"mj-map-mode-btn\"\n [class.active]=\"RenderMode === 'heatmap'\"\n (click)=\"SetRenderMode('heatmap')\">\n <i class=\"fa-solid fa-fire\"></i> Heat Map\n </button>\n </div>\n <div class=\"mj-map-info\">\n {{ MarkerCount }} location{{ MarkerCount !== 1 ? 's' : '' }} mapped\n @if (IsTruncated) {\n <span class=\"mj-map-truncated\" title=\"Not all records are displayed. Showing {{ Records.length | number }} of {{ TotalRecordCount | number }} total records.\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i> Showing {{ Records.length | number }} of {{ TotalRecordCount | number }}\n </span>\n }\n </div>\n </div>\n @if (IsLoading) {\n <div class=\"mj-map-loading\">\n <mj-loading text=\"Loading map...\"></mj-loading>\n </div>\n }\n <div #mapContainer class=\"mj-map-container\"></div>\n</div>\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-view.types.d.ts","sourceRoot":"","sources":["../../src/lib/map-view.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"map-view.types.d.ts","sourceRoot":"","sources":["../../src/lib/map-view.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAE5E;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,gBAAgB,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,gBAAgB,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAAC;CAClD;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CACtC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-view.types.js","sourceRoot":"","sources":["../../src/lib/map-view.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Rendering modes for the map view component.\n */\nexport type MapRenderMode = 'point' | 'choropleth' | 'heatmap';\n\n/**\n * Choropleth grouping level.\n */\nexport type ChoroplethGroupBy = 'country' | 'state_province';\n\n/**\n * Map display state persisted in UserView.DisplayState JSON.\n */\nexport interface MapDisplayState {\n ZoomLevel?: number;\n CenterLat?: number;\n CenterLng?: number;\n ClusterMarkers?: boolean;\n ChoroplethGroupBy?: ChoroplethGroupBy;\n ChoroplethMetric?: 'count' | 'sum' | 'average';\n}\n\n/**\n * Event emitted when a map marker is clicked.\n */\nexport interface MapMarkerClickEvent {\n RecordID: string;\n Latitude: number;\n Longitude: number;\n Record: Record<string, unknown>;\n}\n\n/**\n * Event emitted when a choropleth region is clicked.\n */\nexport interface MapRegionClickEvent {\n RegionName: string;\n GroupBy: ChoroplethGroupBy;\n RecordCount: number;\n Records: Record<string, unknown>[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"map-view.types.js","sourceRoot":"","sources":["../../src/lib/map-view.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Rendering modes for the map view component.\n */\nexport type MapRenderMode = 'point' | 'boundary' | 'choropleth' | 'heatmap';\n\n/**\n * Choropleth grouping level.\n */\nexport type ChoroplethGroupBy = 'country' | 'state_province';\n\n/**\n * Map display state persisted in UserView.DisplayState JSON.\n */\nexport interface MapDisplayState {\n ZoomLevel?: number;\n CenterLat?: number;\n CenterLng?: number;\n ClusterMarkers?: boolean;\n ChoroplethGroupBy?: ChoroplethGroupBy;\n ChoroplethMetric?: 'count' | 'sum' | 'average';\n}\n\n/**\n * Event emitted when a map marker is clicked.\n */\nexport interface MapMarkerClickEvent {\n RecordID: string;\n Latitude: number;\n Longitude: number;\n Record: Record<string, unknown>;\n}\n\n/**\n * Event emitted when a choropleth region is clicked.\n */\nexport interface MapRegionClickEvent {\n RegionName: string;\n GroupBy: ChoroplethGroupBy;\n RecordCount: number;\n Records: Record<string, unknown>[];\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-map-view",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.30.0",
|
|
4
4
|
"description": "MemberJunction: Angular Leaflet-based map component for visualizing geo-enabled entity data",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"@angular/core": "21.1.3"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@memberjunction/core": "5.
|
|
25
|
-
"@memberjunction/core-entities": "5.
|
|
26
|
-
"@memberjunction/global": "5.
|
|
27
|
-
"@memberjunction/
|
|
24
|
+
"@memberjunction/core": "5.30.0",
|
|
25
|
+
"@memberjunction/core-entities": "5.30.0",
|
|
26
|
+
"@memberjunction/global": "5.30.0",
|
|
27
|
+
"@memberjunction/geo-maps": "5.30.0",
|
|
28
|
+
"@memberjunction/ng-shared-generic": "5.30.0",
|
|
28
29
|
"leaflet": "^1.9.4",
|
|
29
30
|
"@types/leaflet": "^1.9.17",
|
|
30
31
|
"tslib": "^2.8.1",
|