@memberjunction/ng-map-view 5.29.0 → 5.30.1

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.
@@ -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 (future)
14
+ * - **heatmap**: Density visualization
15
15
  *
16
- * Integrates into MJExplorer's EntityViewer as the 'map' view mode.
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 map;
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 Leaflet map with OSM tiles.
63
+ * Initialize the map engine via MapCore.
61
64
  */
62
65
  private InitializeMap;
63
66
  /**
64
- * Render map content based on the current RenderMode.
67
+ * Render records using the current mode, update marker count.
65
68
  */
66
- private RenderMarkers;
69
+ private DoRender;
67
70
  /**
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.
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;;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"}
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 MapViewComponent_Conditional_14_Template(rf, ctx) { if (rf & 1) {
8
- i0.ɵɵelementStart(0, "span", 9);
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);
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 MapViewComponent_Conditional_15_Template(rf, ctx) { if (rf & 1) {
23
- i0.ɵɵelementStart(0, "div", 10);
24
- i0.ɵɵelement(1, "mj-loading", 13);
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 (future)
39
+ * - **heatmap**: Density visualization
39
40
  *
40
- * Integrates into MJExplorer's EntityViewer as the 'map' view mode.
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
- map = null;
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.map) {
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.map) {
98
+ else if (entry.isIntersecting && this.engine) {
95
99
  // Becoming visible again — fix tile rendering and re-fit
96
- this.map.invalidateSize();
100
+ this.engine.invalidateSize();
97
101
  if (this.pendingRender) {
98
- this.RenderMarkers();
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.map) {
113
+ if (this.engine) {
110
114
  setTimeout(() => {
111
- if (this.map) {
112
- this.map.invalidateSize();
113
- this.RenderMarkers();
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.map) {
129
- this.map.remove();
130
- this.map = null;
132
+ if (this.engine) {
133
+ this.engine.destroy();
134
+ this.engine = null;
131
135
  }
132
136
  }
133
137
  /**
134
- * Initialize the Leaflet map with OSM tiles.
138
+ * Initialize the map engine via MapCore.
135
139
  */
136
140
  InitializeMap() {
137
- if (!this.mapContainer?.nativeElement || this.map)
141
+ if (!this.mapContainer?.nativeElement || this.engine)
138
142
  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
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: '&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
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.map) {
165
- this.map.invalidateSize();
166
- this.RenderMarkers();
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.map)
172
- this.map.invalidateSize();
215
+ if (this.engine)
216
+ this.engine.invalidateSize();
173
217
  }, 500);
174
218
  }
175
219
  }, 100);
176
220
  }
177
221
  /**
178
- * Render map content based on the current RenderMode.
222
+ * Render records using the current mode, update marker count.
179
223
  */
180
- RenderMarkers() {
181
- if (!this.map || !this.markerLayer)
224
+ DoRender() {
225
+ if (!this.engine)
182
226
  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
- }
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
- * 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.
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.RenderMarkers();
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: 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) {
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("choropleth")); });
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, " Regions ");
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("heatmap")); });
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, " Heat Map ");
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(12, "div", 8);
631
- i0.ɵɵtext(13);
632
- i0.ɵɵconditionalCreate(14, MapViewComponent_Conditional_14_Template, 7, 13, "span", 9);
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(15, MapViewComponent_Conditional_15_Template, 2, 0, "div", 10);
635
- i0.ɵɵelement(16, "div", 11, 0);
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 ? 14 : -1);
305
+ i0.ɵɵconditional(ctx.IsTruncated ? 17 : -1);
648
306
  i0.ɵɵadvance();
649
- i0.ɵɵconditional(ctx.IsLoading ? 15 : -1);
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: 41 }); })();
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: '&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"]}
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';\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,7 +1,7 @@
1
1
  /**
2
2
  * Rendering modes for the map view component.
3
3
  */
4
- export type MapRenderMode = 'point' | 'choropleth' | 'heatmap';
4
+ export type MapRenderMode = 'point' | 'boundary' | 'choropleth' | 'heatmap';
5
5
  /**
6
6
  * Choropleth grouping level.
7
7
  */
@@ -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;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"}
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.29.0",
3
+ "version": "5.30.1",
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.29.0",
25
- "@memberjunction/core-entities": "5.29.0",
26
- "@memberjunction/global": "5.29.0",
27
- "@memberjunction/ng-shared-generic": "5.29.0",
24
+ "@memberjunction/core": "5.30.1",
25
+ "@memberjunction/core-entities": "5.30.1",
26
+ "@memberjunction/global": "5.30.1",
27
+ "@memberjunction/geo-maps": "5.30.1",
28
+ "@memberjunction/ng-shared-generic": "5.30.1",
28
29
  "leaflet": "^1.9.4",
29
30
  "@types/leaflet": "^1.9.17",
30
31
  "tslib": "^2.8.1",