@memberjunction/ng-map-view 0.0.1 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/map-view.component.d.ts +151 -0
- package/dist/lib/map-view.component.d.ts.map +1 -0
- package/dist/lib/map-view.component.js +682 -0
- package/dist/lib/map-view.component.js.map +1 -0
- package/dist/lib/map-view.module.d.ts +10 -0
- package/dist/lib/map-view.module.d.ts.map +1 -0
- package/dist/lib/map-view.module.js +20 -0
- package/dist/lib/map-view.module.js.map +1 -0
- package/dist/lib/map-view.types.d.ts +38 -0
- package/dist/lib/map-view.types.d.ts.map +1 -0
- package/dist/lib/map-view.types.js +2 -0
- package/dist/lib/map-view.types.js.map +1 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +4 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +35 -7
- package/README.md +0 -45
|
@@ -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: '© <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: '© <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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
]
|
|
3
|
+
"version": "5.26.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.26.0",
|
|
25
|
+
"@memberjunction/core-entities": "5.26.0",
|
|
26
|
+
"@memberjunction/global": "5.26.0",
|
|
27
|
+
"@memberjunction/ng-shared-generic": "5.26.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**
|