@memberjunction/ng-map-view 0.0.1 → 5.25.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.
@@ -0,0 +1,151 @@
1
+ import { EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, AfterViewInit } from '@angular/core';
2
+ import { EntityInfo } from '@memberjunction/core';
3
+ import { MapRenderMode, MapDisplayState, MapMarkerClickEvent, MapRegionClickEvent } from './map-view.types';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * MapViewComponent — Leaflet-based map visualization for geo-enabled MJ entities.
7
+ *
8
+ * Reads `__mj_Latitude` and `__mj_Longitude` from entity records (or custom field overrides)
9
+ * and renders them as interactive markers on an OpenStreetMap tile layer.
10
+ *
11
+ * Supports three rendering modes:
12
+ * - **point** (default): Individual markers with optional clustering
13
+ * - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)
14
+ * - **heatmap**: Density visualization (future)
15
+ *
16
+ * Integrates into MJExplorer's EntityViewer as the 'map' view mode.
17
+ */
18
+ export declare class MapViewComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
19
+ private cdr;
20
+ private mapContainer;
21
+ /** Entity metadata for the displayed records. */
22
+ Entity: EntityInfo;
23
+ /** Array of entity records to render on the map. Accepts BaseEntity[] or plain Record objects. */
24
+ Records: Record<string, unknown>[];
25
+ /** Latitude field name. Defaults to __mj_Latitude. */
26
+ LatitudeField: string;
27
+ /** Longitude field name. Defaults to __mj_Longitude. */
28
+ LongitudeField: string;
29
+ /** Current render mode. */
30
+ RenderMode: MapRenderMode;
31
+ /** Persisted display state from UserView. */
32
+ DisplayState: Partial<MapDisplayState> | null;
33
+ /**
34
+ * Total record count from the server (before any client-side cap).
35
+ * When this exceeds the number of Records passed in, the map shows
36
+ * a truncation indicator so the user knows not all data is displayed.
37
+ */
38
+ TotalRecordCount: number;
39
+ /** Emitted when a marker is clicked. */
40
+ MarkerClick: EventEmitter<MapMarkerClickEvent>;
41
+ /** Emitted when a choropleth region is clicked. */
42
+ RegionClick: EventEmitter<MapRegionClickEvent>;
43
+ /** Emitted when display state changes (for persistence). */
44
+ DisplayStateChange: EventEmitter<MapDisplayState>;
45
+ /** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */
46
+ RenderModeChange: EventEmitter<MapRenderMode>;
47
+ private map;
48
+ private markerLayer;
49
+ private visibilityObserver;
50
+ private pendingRender;
51
+ IsLoading: boolean;
52
+ MarkerCount: number;
53
+ /** Whether the displayed records are truncated (more exist than were loaded). */
54
+ get IsTruncated(): boolean;
55
+ ngOnInit(): void;
56
+ ngAfterViewInit(): void;
57
+ ngOnChanges(changes: SimpleChanges): void;
58
+ ngOnDestroy(): void;
59
+ /**
60
+ * Initialize the Leaflet map with OSM tiles.
61
+ */
62
+ private InitializeMap;
63
+ /**
64
+ * Render map content based on the current RenderMode.
65
+ */
66
+ private RenderMarkers;
67
+ /**
68
+ * Point mode: individual markers with marker clustering for dense areas.
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.
123
+ */
124
+ SetRenderMode(mode: MapRenderMode): void;
125
+ /**
126
+ * Get a field value from a record, supporting both BaseEntity (with .Get()) and plain objects.
127
+ */
128
+ private GetField;
129
+ /**
130
+ * Get a numeric field value from a record, returning null if not a valid number.
131
+ */
132
+ 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
+ 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>;
150
+ }
151
+ //# sourceMappingURL=map-view.component.d.ts.map
@@ -0,0 +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;;AAI5G;;;;;;;;;;;;GAYG;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,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,GAAG,CAAsB;IACjC,OAAO,CAAC,WAAW,CAA6B;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;IAiDrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAoBrB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAoD1B;;;;OAIG;IACH,OAAO,CAAC,aAAa;IA8CrB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IAkDxB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA6B5B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA0C5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAuCtB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAQ1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAMxC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAOhB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA6BzB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAgC9B;;OAEG;IACH,OAAO,CAAC,UAAU;yCAnpBT,gBAAgB;2CAAhB,gBAAgB;CAwpB5B"}
@@ -0,0 +1,682 @@
1
+ import { Component, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef, inject } from '@angular/core';
2
+ import { GeoDataEngine } from '@memberjunction/core-entities';
3
+ import * as i0 from "@angular/core";
4
+ import * as i1 from "@memberjunction/ng-shared-generic";
5
+ import * as i2 from "@angular/common";
6
+ const _c0 = ["mapContainer"];
7
+ function MapViewComponent_Conditional_14_Template(rf, ctx) { if (rf & 1) {
8
+ i0.ɵɵelementStart(0, "span", 9);
9
+ i0.ɵɵpipe(1, "number");
10
+ i0.ɵɵpipe(2, "number");
11
+ i0.ɵɵelement(3, "i", 12);
12
+ i0.ɵɵtext(4);
13
+ i0.ɵɵpipe(5, "number");
14
+ i0.ɵɵpipe(6, "number");
15
+ i0.ɵɵelementEnd();
16
+ } if (rf & 2) {
17
+ const ctx_r1 = i0.ɵɵnextContext();
18
+ i0.ɵɵproperty("title", i0.ɵɵinterpolate2("Not all records are displayed. Showing ", i0.ɵɵpipeBind1(1, 5, ctx_r1.Records.length), " of ", i0.ɵɵpipeBind1(2, 7, ctx_r1.TotalRecordCount), " total records."));
19
+ i0.ɵɵadvance(4);
20
+ i0.ɵɵtextInterpolate2(" Showing ", i0.ɵɵpipeBind1(5, 9, ctx_r1.Records.length), " of ", i0.ɵɵpipeBind1(6, 11, ctx_r1.TotalRecordCount), " ");
21
+ } }
22
+ function MapViewComponent_Conditional_15_Template(rf, ctx) { if (rf & 1) {
23
+ i0.ɵɵelementStart(0, "div", 10);
24
+ i0.ɵɵelement(1, "mj-loading", 13);
25
+ i0.ɵɵelementEnd();
26
+ } }
27
+ // Leaflet is loaded as a global script at runtime via angular.json scripts array.
28
+ // The L namespace is available globally — see leaflet.d.ts for type declarations.
29
+ /**
30
+ * MapViewComponent — Leaflet-based map visualization for geo-enabled MJ entities.
31
+ *
32
+ * Reads `__mj_Latitude` and `__mj_Longitude` from entity records (or custom field overrides)
33
+ * and renders them as interactive markers on an OpenStreetMap tile layer.
34
+ *
35
+ * Supports three rendering modes:
36
+ * - **point** (default): Individual markers with optional clustering
37
+ * - **choropleth**: Region shading by country/state (requires BoundaryGeoJSON)
38
+ * - **heatmap**: Density visualization (future)
39
+ *
40
+ * Integrates into MJExplorer's EntityViewer as the 'map' view mode.
41
+ */
42
+ export class MapViewComponent {
43
+ cdr = inject(ChangeDetectorRef);
44
+ mapContainer;
45
+ /** Entity metadata for the displayed records. */
46
+ Entity;
47
+ /** Array of entity records to render on the map. Accepts BaseEntity[] or plain Record objects. */
48
+ Records = [];
49
+ /** Latitude field name. Defaults to __mj_Latitude. */
50
+ LatitudeField = '__mj_Latitude';
51
+ /** Longitude field name. Defaults to __mj_Longitude. */
52
+ LongitudeField = '__mj_Longitude';
53
+ /** Current render mode. */
54
+ RenderMode = 'point';
55
+ /** Persisted display state from UserView. */
56
+ DisplayState = null;
57
+ /**
58
+ * Total record count from the server (before any client-side cap).
59
+ * When this exceeds the number of Records passed in, the map shows
60
+ * a truncation indicator so the user knows not all data is displayed.
61
+ */
62
+ TotalRecordCount = 0;
63
+ /** Emitted when a marker is clicked. */
64
+ MarkerClick = new EventEmitter();
65
+ /** Emitted when a choropleth region is clicked. */
66
+ RegionClick = new EventEmitter();
67
+ /** Emitted when display state changes (for persistence). */
68
+ DisplayStateChange = new EventEmitter();
69
+ /** Emitted when render mode changes (supports two-way binding: [(RenderMode)]). */
70
+ RenderModeChange = new EventEmitter();
71
+ map = null;
72
+ markerLayer = null;
73
+ visibilityObserver = null;
74
+ pendingRender = false;
75
+ IsLoading = true;
76
+ MarkerCount = 0;
77
+ /** Whether the displayed records are truncated (more exist than were loaded). */
78
+ get IsTruncated() {
79
+ return this.TotalRecordCount > 0 && this.Records.length < this.TotalRecordCount;
80
+ }
81
+ ngOnInit() {
82
+ // Map initialization happens when the component becomes visible
83
+ }
84
+ ngAfterViewInit() {
85
+ // Use IntersectionObserver to detect when the map container becomes visible.
86
+ // Leaflet requires the container to have actual dimensions before initialization.
87
+ // Since the map starts hidden (grid view is default), we defer init until visible.
88
+ this.visibilityObserver = new IntersectionObserver((entries) => {
89
+ const entry = entries[0];
90
+ if (entry.isIntersecting && !this.map) {
91
+ // First time visible — initialize the map
92
+ this.InitializeMap();
93
+ }
94
+ else if (entry.isIntersecting && this.map) {
95
+ // Becoming visible again — fix tile rendering and re-fit
96
+ this.map.invalidateSize();
97
+ if (this.pendingRender) {
98
+ this.RenderMarkers();
99
+ this.pendingRender = false;
100
+ }
101
+ }
102
+ }, { threshold: 0.1 });
103
+ if (this.mapContainer?.nativeElement) {
104
+ this.visibilityObserver.observe(this.mapContainer.nativeElement);
105
+ }
106
+ }
107
+ ngOnChanges(changes) {
108
+ if (changes['Records'] || changes['RenderMode']) {
109
+ if (this.map) {
110
+ setTimeout(() => {
111
+ if (this.map) {
112
+ this.map.invalidateSize();
113
+ this.RenderMarkers();
114
+ }
115
+ }, 100);
116
+ }
117
+ else {
118
+ // Map not initialized yet — mark for render when visible
119
+ this.pendingRender = true;
120
+ }
121
+ }
122
+ }
123
+ ngOnDestroy() {
124
+ if (this.visibilityObserver) {
125
+ this.visibilityObserver.disconnect();
126
+ this.visibilityObserver = null;
127
+ }
128
+ if (this.map) {
129
+ this.map.remove();
130
+ this.map = null;
131
+ }
132
+ }
133
+ /**
134
+ * Initialize the Leaflet map with OSM tiles.
135
+ */
136
+ InitializeMap() {
137
+ if (!this.mapContainer?.nativeElement || this.map)
138
+ return;
139
+ const center = [
140
+ this.DisplayState?.CenterLat ?? 20,
141
+ this.DisplayState?.CenterLng ?? 0
142
+ ];
143
+ const zoom = this.DisplayState?.ZoomLevel ?? 2;
144
+ this.map = L.map(this.mapContainer.nativeElement, {
145
+ center,
146
+ zoom,
147
+ zoomControl: true,
148
+ attributionControl: false
149
+ });
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: '&copy; <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
+ // invalidateSize after a short delay to ensure tile rendering is correct
162
+ // then render markers and fit bounds
163
+ setTimeout(() => {
164
+ if (this.map) {
165
+ this.map.invalidateSize();
166
+ this.RenderMarkers();
167
+ this.IsLoading = false;
168
+ this.cdr.detectChanges();
169
+ // Double-invalidate after tiles start loading to fix partial rendering
170
+ setTimeout(() => {
171
+ if (this.map)
172
+ this.map.invalidateSize();
173
+ }, 500);
174
+ }
175
+ }, 100);
176
+ }
177
+ /**
178
+ * Render map content based on the current RenderMode.
179
+ */
180
+ RenderMarkers() {
181
+ if (!this.map || !this.markerLayer)
182
+ return;
183
+ this.markerLayer.clearLayers();
184
+ switch (this.RenderMode) {
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
+ }
492
+ this.cdr.detectChanges();
493
+ }
494
+ /**
495
+ * Emit the current display state for persistence.
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.
510
+ */
511
+ SetRenderMode(mode) {
512
+ this.RenderMode = mode;
513
+ this.RenderModeChange.emit(mode);
514
+ this.RenderMarkers();
515
+ }
516
+ /**
517
+ * Get a field value from a record, supporting both BaseEntity (with .Get()) and plain objects.
518
+ */
519
+ GetField(record, fieldName) {
520
+ if ('Get' in record && typeof record['Get'] === 'function') {
521
+ return record.Get(fieldName);
522
+ }
523
+ return record[fieldName];
524
+ }
525
+ /**
526
+ * Get a numeric field value from a record, returning null if not a valid number.
527
+ */
528
+ GetNumericField(record, fieldName) {
529
+ const val = this.GetField(record, fieldName);
530
+ if (val == null)
531
+ return null;
532
+ const num = Number(val);
533
+ return isNaN(num) ? null : num;
534
+ }
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
+ static ɵfac = function MapViewComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MapViewComponent)(); };
608
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MapViewComponent, selectors: [["mj-map-view"]], viewQuery: function MapViewComponent_Query(rf, ctx) { if (rf & 1) {
609
+ i0.ɵɵviewQuery(_c0, 5);
610
+ } if (rf & 2) {
611
+ let _t;
612
+ 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: 18, vars: 10, 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-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
+ const _r1 = i0.ɵɵgetCurrentView();
615
+ i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "div", 3)(3, "button", 4);
616
+ i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("point")); });
617
+ i0.ɵɵelement(4, "i", 5);
618
+ i0.ɵɵtext(5, " Points ");
619
+ i0.ɵɵelementEnd();
620
+ i0.ɵɵelementStart(6, "button", 4);
621
+ i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("choropleth")); });
622
+ i0.ɵɵelement(7, "i", 6);
623
+ i0.ɵɵtext(8, " Regions ");
624
+ i0.ɵɵelementEnd();
625
+ i0.ɵɵelementStart(9, "button", 4);
626
+ i0.ɵɵlistener("click", function MapViewComponent_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.SetRenderMode("heatmap")); });
627
+ i0.ɵɵelement(10, "i", 7);
628
+ i0.ɵɵtext(11, " Heat Map ");
629
+ i0.ɵɵelementEnd()();
630
+ i0.ɵɵelementStart(12, "div", 8);
631
+ i0.ɵɵtext(13);
632
+ i0.ɵɵconditionalCreate(14, MapViewComponent_Conditional_14_Template, 7, 13, "span", 9);
633
+ i0.ɵɵelementEnd()();
634
+ i0.ɵɵconditionalCreate(15, MapViewComponent_Conditional_15_Template, 2, 0, "div", 10);
635
+ i0.ɵɵelement(16, "div", 11, 0);
636
+ i0.ɵɵelementEnd();
637
+ } if (rf & 2) {
638
+ i0.ɵɵadvance(3);
639
+ i0.ɵɵclassProp("active", ctx.RenderMode === "point");
640
+ i0.ɵɵadvance(3);
641
+ i0.ɵɵclassProp("active", ctx.RenderMode === "choropleth");
642
+ i0.ɵɵadvance(3);
643
+ i0.ɵɵclassProp("active", ctx.RenderMode === "heatmap");
644
+ i0.ɵɵadvance(4);
645
+ i0.ɵɵtextInterpolate2(" ", ctx.MarkerCount, " location", ctx.MarkerCount !== 1 ? "s" : "", " mapped ");
646
+ i0.ɵɵadvance();
647
+ i0.ɵɵconditional(ctx.IsTruncated ? 14 : -1);
648
+ i0.ɵɵadvance();
649
+ i0.ɵɵconditional(ctx.IsLoading ? 15 : -1);
650
+ } }, 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
+ }
652
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MapViewComponent, [{
653
+ 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"] }]
655
+ }], null, { mapContainer: [{
656
+ type: ViewChild,
657
+ args: ['mapContainer', { static: false }]
658
+ }], Entity: [{
659
+ type: Input
660
+ }], Records: [{
661
+ type: Input
662
+ }], LatitudeField: [{
663
+ type: Input
664
+ }], LongitudeField: [{
665
+ type: Input
666
+ }], RenderMode: [{
667
+ type: Input
668
+ }], DisplayState: [{
669
+ type: Input
670
+ }], TotalRecordCount: [{
671
+ type: Input
672
+ }], MarkerClick: [{
673
+ type: Output
674
+ }], RegionClick: [{
675
+ type: Output
676
+ }], DisplayStateChange: [{
677
+ type: Output
678
+ }], RenderModeChange: [{
679
+ type: Output
680
+ }] }); })();
681
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MapViewComponent, { className: "MapViewComponent", filePath: "src/lib/map-view.component.ts", lineNumber: 41 }); })();
682
+ //# sourceMappingURL=map-view.component.js.map
@@ -0,0 +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: '&copy; <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"]}
@@ -0,0 +1,10 @@
1
+ import * as i0 from "@angular/core";
2
+ import * as i1 from "./map-view.component";
3
+ import * as i2 from "@angular/common";
4
+ import * as i3 from "@memberjunction/ng-shared-generic";
5
+ export declare class MapViewModule {
6
+ static ɵfac: i0.ɵɵFactoryDeclaration<MapViewModule, never>;
7
+ static ɵmod: i0.ɵɵNgModuleDeclaration<MapViewModule, [typeof i1.MapViewComponent], [typeof i2.CommonModule, typeof i3.SharedGenericModule], [typeof i1.MapViewComponent]>;
8
+ static ɵinj: i0.ɵɵInjectorDeclaration<MapViewModule>;
9
+ }
10
+ //# sourceMappingURL=map-view.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-view.module.d.ts","sourceRoot":"","sources":["../../src/lib/map-view.module.ts"],"names":[],"mappings":";;;;AAKA,qBAKa,aAAa;yCAAb,aAAa;0CAAb,aAAa;0CAAb,aAAa;CAAG"}
@@ -0,0 +1,20 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { SharedGenericModule } from '@memberjunction/ng-shared-generic';
4
+ import { MapViewComponent } from './map-view.component';
5
+ import * as i0 from "@angular/core";
6
+ export class MapViewModule {
7
+ static ɵfac = function MapViewModule_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MapViewModule)(); };
8
+ static ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: MapViewModule });
9
+ static ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [CommonModule, SharedGenericModule] });
10
+ }
11
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MapViewModule, [{
12
+ type: NgModule,
13
+ args: [{
14
+ declarations: [MapViewComponent],
15
+ imports: [CommonModule, SharedGenericModule],
16
+ exports: [MapViewComponent]
17
+ }]
18
+ }], null, null); })();
19
+ (function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(MapViewModule, { declarations: [MapViewComponent], imports: [CommonModule, SharedGenericModule], exports: [MapViewComponent] }); })();
20
+ //# sourceMappingURL=map-view.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map-view.module.js","sourceRoot":"","sources":["../../src/lib/map-view.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;;AAOxD,MAAM,OAAO,aAAa;uGAAb,aAAa;4DAAb,aAAa;gEAHZ,YAAY,EAAE,mBAAmB;;iFAGlC,aAAa;cALzB,QAAQ;eAAC;gBACN,YAAY,EAAE,CAAC,gBAAgB,CAAC;gBAChC,OAAO,EAAE,CAAC,YAAY,EAAE,mBAAmB,CAAC;gBAC5C,OAAO,EAAE,CAAC,gBAAgB,CAAC;aAC9B;;wFACY,aAAa,mBAJP,gBAAgB,aACrB,YAAY,EAAE,mBAAmB,aACjC,gBAAgB","sourcesContent":["import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { SharedGenericModule } from '@memberjunction/ng-shared-generic';\nimport { MapViewComponent } from './map-view.component';\n\n@NgModule({\n declarations: [MapViewComponent],\n imports: [CommonModule, SharedGenericModule],\n exports: [MapViewComponent]\n})\nexport class MapViewModule {}\n"]}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Rendering modes for the map view component.
3
+ */
4
+ export type MapRenderMode = 'point' | 'choropleth' | 'heatmap';
5
+ /**
6
+ * Choropleth grouping level.
7
+ */
8
+ export type ChoroplethGroupBy = 'country' | 'state_province';
9
+ /**
10
+ * Map display state persisted in UserView.DisplayState JSON.
11
+ */
12
+ export interface MapDisplayState {
13
+ ZoomLevel?: number;
14
+ CenterLat?: number;
15
+ CenterLng?: number;
16
+ ClusterMarkers?: boolean;
17
+ ChoroplethGroupBy?: ChoroplethGroupBy;
18
+ ChoroplethMetric?: 'count' | 'sum' | 'average';
19
+ }
20
+ /**
21
+ * Event emitted when a map marker is clicked.
22
+ */
23
+ export interface MapMarkerClickEvent {
24
+ RecordID: string;
25
+ Latitude: number;
26
+ Longitude: number;
27
+ Record: Record<string, unknown>;
28
+ }
29
+ /**
30
+ * Event emitted when a choropleth region is clicked.
31
+ */
32
+ export interface MapRegionClickEvent {
33
+ RegionName: string;
34
+ GroupBy: ChoroplethGroupBy;
35
+ RecordCount: number;
36
+ Records: Record<string, unknown>[];
37
+ }
38
+ //# sourceMappingURL=map-view.types.d.ts.map
@@ -0,0 +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;AAE/D;;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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=map-view.types.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,4 @@
1
+ export * from './lib/map-view.module';
2
+ export * from './lib/map-view.component';
3
+ export * from './lib/map-view.types';
4
+ //# sourceMappingURL=public-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './lib/map-view.module';
2
+ export * from './lib/map-view.component';
3
+ export * from './lib/map-view.types';
4
+ //# sourceMappingURL=public-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC","sourcesContent":["export * from './lib/map-view.module';\nexport * from './lib/map-view.component';\nexport * from './lib/map-view.types';\n"]}
package/package.json CHANGED
@@ -1,10 +1,38 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-map-view",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/ng-map-view",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "version": "5.25.0",
4
+ "description": "MemberJunction: Angular Leaflet-based map component for visualizing geo-enabled entity data",
5
+ "main": "./dist/public-api.js",
6
+ "typings": "./dist/public-api.d.ts",
7
+ "files": [
8
+ "/dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "ngc"
12
+ },
13
+ "author": "MemberJunction.com",
14
+ "license": "ISC",
15
+ "devDependencies": {
16
+ "@angular/compiler": "21.1.3",
17
+ "@angular/compiler-cli": "21.1.3"
18
+ },
19
+ "peerDependencies": {
20
+ "@angular/common": "21.1.3",
21
+ "@angular/core": "21.1.3"
22
+ },
23
+ "dependencies": {
24
+ "@memberjunction/core": "5.25.0",
25
+ "@memberjunction/core-entities": "5.25.0",
26
+ "@memberjunction/global": "5.25.0",
27
+ "@memberjunction/ng-shared-generic": "5.25.0",
28
+ "leaflet": "^1.9.4",
29
+ "@types/leaflet": "^1.9.17",
30
+ "tslib": "^2.8.1",
31
+ "rxjs": "^7.8.2"
32
+ },
33
+ "sideEffects": false,
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/MemberJunction/MJ"
37
+ }
10
38
  }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @memberjunction/ng-map-view
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/ng-map-view`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**