kor-mapi 0.1.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/core.d.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { l as MapProvider, f as KorMapConfig, L as LatLng, j as LatLngBounds, e as FitBoundsOptions, r as PanOptions, m as MapTypeId, i as KorMapFeature, h as KorMapEvent, A as AnyEventHandler, E as EventListener, q as MarkerOptions, O as OverlayEvent, I as InfoWindowOptions, u as PolylineOptions, t as PolygonOptions, a as CircleOptions, R as RectangleOptions, d as CustomOverlayOptions, y as TileLayerOptions, k as MapProjection } from './types-BWnSkE6e.js';
2
+ export { C as ClusterOptions, b as ClusterStyle, c as ConfigurationError, F as FillStyle, K as KorMapApiKey, g as KorMapError, M as MapMouseEvent, n as MarkerAnimation, o as MarkerIcon, p as MarkerLabel, P as Padding, s as Point, v as ProviderLoadError, w as ProviderNotSupportedError, S as Size, x as StrokeStyle, T as TileCoord } from './types-BWnSkE6e.js';
3
+
4
+ /**
5
+ * The contract every provider adapter must implement.
6
+ *
7
+ * All zoom values crossing this interface are in the facade's 0–22 scale
8
+ * (Google-style, 0 = world view, 22 = building level). Adapters are
9
+ * responsible for translating to/from their native scale internally.
10
+ *
11
+ * All coordinate values are WGS84 LatLng.
12
+ */
13
+ interface IMapProvider {
14
+ readonly provider: MapProvider;
15
+ /** Initialize the native map inside the given container element. */
16
+ init(container: HTMLElement, config: KorMapConfig): Promise<void>;
17
+ /** Destroy the map and release all resources. */
18
+ destroy(): void;
19
+ setCenter(latlng: LatLng): void;
20
+ getCenter(): LatLng;
21
+ /** @param zoom Facade zoom level 0–22 */
22
+ setZoom(zoom: number): void;
23
+ /** @returns Facade zoom level 0–22 */
24
+ getZoom(): number;
25
+ getBounds(): LatLngBounds;
26
+ fitBounds(bounds: LatLngBounds, options?: FitBoundsOptions): void;
27
+ panTo(latlng: LatLng, options?: PanOptions): void;
28
+ panBy(x: number, y: number): void;
29
+ setMapType(type: MapTypeId): void;
30
+ getMapType(): MapTypeId;
31
+ supports(feature: KorMapFeature): boolean;
32
+ on(event: KorMapEvent, handler: AnyEventHandler): EventListener;
33
+ once(event: KorMapEvent, handler: AnyEventHandler): EventListener;
34
+ off(event: KorMapEvent, listener: EventListener): void;
35
+ addMarker(options: MarkerOptions): NativeMarkerHandle;
36
+ removeMarker(handle: NativeMarkerHandle): void;
37
+ updateMarker(handle: NativeMarkerHandle, options: Partial<MarkerOptions>): void;
38
+ addMarkerEvent(handle: NativeMarkerHandle, event: OverlayEvent, handler: AnyEventHandler): EventListener;
39
+ removeMarkerEvent(handle: NativeMarkerHandle, event: OverlayEvent, listener: EventListener): void;
40
+ openInfoWindow(options: InfoWindowOptions, anchor?: NativeMarkerHandle): NativeInfoWindowHandle;
41
+ closeInfoWindow(handle: NativeInfoWindowHandle): void;
42
+ updateInfoWindow(handle: NativeInfoWindowHandle, options: Partial<InfoWindowOptions>): void;
43
+ isInfoWindowOpen(handle: NativeInfoWindowHandle): boolean;
44
+ addPolyline(options: PolylineOptions): NativeOverlayHandle;
45
+ removePolyline(handle: NativeOverlayHandle): void;
46
+ updatePolyline(handle: NativeOverlayHandle, options: Partial<PolylineOptions>): void;
47
+ addPolygon(options: PolygonOptions): NativeOverlayHandle;
48
+ removePolygon(handle: NativeOverlayHandle): void;
49
+ updatePolygon(handle: NativeOverlayHandle, options: Partial<PolygonOptions>): void;
50
+ addCircle(options: CircleOptions): NativeOverlayHandle;
51
+ removeCircle(handle: NativeOverlayHandle): void;
52
+ updateCircle(handle: NativeOverlayHandle, options: Partial<CircleOptions>): void;
53
+ addRectangle(options: RectangleOptions): NativeOverlayHandle;
54
+ removeRectangle(handle: NativeOverlayHandle): void;
55
+ updateRectangle(handle: NativeOverlayHandle, options: Partial<RectangleOptions>): void;
56
+ addCustomOverlay(options: CustomOverlayOptions): NativeOverlayHandle;
57
+ removeCustomOverlay(handle: NativeOverlayHandle): void;
58
+ addTileLayer(options: TileLayerOptions): NativeOverlayHandle;
59
+ removeTileLayer(handle: NativeOverlayHandle): void;
60
+ getProjection(): MapProjection;
61
+ /** The raw underlying map object. Type depends on provider. */
62
+ readonly nativeMap: unknown;
63
+ }
64
+ /**
65
+ * Opaque handle referencing a native marker instance inside an adapter.
66
+ * The adapter stores the actual native object and looks it up by handle id.
67
+ */
68
+ interface NativeMarkerHandle {
69
+ readonly _handleId: number;
70
+ }
71
+ /**
72
+ * Opaque handle referencing a native overlay (polyline, polygon, circle, etc.)
73
+ * or info window inside an adapter.
74
+ */
75
+ interface NativeOverlayHandle {
76
+ readonly _handleId: number;
77
+ }
78
+ /** Opaque handle referencing a native info window instance. */
79
+ interface NativeInfoWindowHandle {
80
+ readonly _handleId: number;
81
+ }
82
+
83
+ /**
84
+ * The main facade. Create via `KorMap.create(config)`.
85
+ *
86
+ * Type parameter P narrows the type of `map.native`:
87
+ * const map = await KorMap.create<'naver'>({ provider: 'naver', ... });
88
+ * map.native // typed as naver.maps.Map
89
+ */
90
+ declare class KorMap<P extends MapProvider = MapProvider> {
91
+ private readonly adapter;
92
+ private constructor();
93
+ /**
94
+ * Async factory. Loads the provider SDK and initializes the map.
95
+ */
96
+ static create<P extends MapProvider = MapProvider>(config: KorMapConfig<P>): Promise<KorMap<P>>;
97
+ getProvider(): P;
98
+ supports(feature: KorMapFeature): boolean;
99
+ /**
100
+ * The raw underlying map object.
101
+ * Typed via generic when the provider is known at compile time:
102
+ * const map = await KorMap.create<'naver'>({ provider: 'naver', ... });
103
+ * map.native // → naver.maps.Map
104
+ */
105
+ get native(): P extends 'naver' ? typeof window.naver.maps.Map.prototype : P extends 'kakao' ? typeof window.kakao.maps.Map.prototype : P extends 'google' ? typeof window.google.maps.Map.prototype : unknown;
106
+ setCenter(latlng: LatLng): void;
107
+ getCenter(): LatLng;
108
+ setZoom(zoom: number): void;
109
+ getZoom(): number;
110
+ getBounds(): LatLngBounds;
111
+ fitBounds(bounds: LatLngBounds, options?: FitBoundsOptions): void;
112
+ panTo(latlng: LatLng, options?: PanOptions): void;
113
+ panBy(x: number, y: number): void;
114
+ setMapType(type: MapTypeId): void;
115
+ getMapType(): MapTypeId;
116
+ on(event: KorMapEvent, handler: AnyEventHandler): EventListener;
117
+ once(event: KorMapEvent, handler: AnyEventHandler): EventListener;
118
+ off(event: KorMapEvent, listener: EventListener): void;
119
+ destroy(): void;
120
+ /** @internal */
121
+ _adapter(): IMapProvider;
122
+ }
123
+
124
+ declare class Marker {
125
+ private options;
126
+ private handle;
127
+ private currentMap;
128
+ constructor(options: MarkerOptions);
129
+ setMap(map: KorMap | null): void;
130
+ getMap(): KorMap | null;
131
+ setPosition(latlng: LatLng): void;
132
+ getPosition(): LatLng;
133
+ setVisible(visible: boolean): void;
134
+ setOpacity(opacity: number): void;
135
+ setZIndex(zIndex: number): void;
136
+ setDraggable(draggable: boolean): void;
137
+ setIcon(icon: MarkerOptions['icon']): void;
138
+ setOptions(options: Partial<MarkerOptions>): void;
139
+ on(event: OverlayEvent, handler: AnyEventHandler): EventListener | null;
140
+ off(event: OverlayEvent, listener: EventListener): void;
141
+ }
142
+ declare class InfoWindow {
143
+ private options;
144
+ private handle;
145
+ private currentMap;
146
+ constructor(options: InfoWindowOptions);
147
+ open(map: KorMap, anchor?: Marker): void;
148
+ close(): void;
149
+ setContent(content: string | HTMLElement): void;
150
+ getContent(): string | HTMLElement;
151
+ setPosition(latlng: LatLng): void;
152
+ isOpen(): boolean;
153
+ }
154
+ declare class Polyline {
155
+ private options;
156
+ private handle;
157
+ private currentMap;
158
+ constructor(options: PolylineOptions);
159
+ setMap(map: KorMap | null): void;
160
+ setOptions(options: Partial<PolylineOptions>): void;
161
+ }
162
+ declare class Polygon {
163
+ private options;
164
+ private handle;
165
+ private currentMap;
166
+ constructor(options: PolygonOptions);
167
+ setMap(map: KorMap | null): void;
168
+ setOptions(options: Partial<PolygonOptions>): void;
169
+ }
170
+ declare class Circle {
171
+ private options;
172
+ private handle;
173
+ private currentMap;
174
+ constructor(options: CircleOptions);
175
+ setMap(map: KorMap | null): void;
176
+ setOptions(options: Partial<CircleOptions>): void;
177
+ }
178
+ declare class Rectangle {
179
+ private options;
180
+ private handle;
181
+ private currentMap;
182
+ constructor(options: RectangleOptions);
183
+ setMap(map: KorMap | null): void;
184
+ setOptions(options: Partial<RectangleOptions>): void;
185
+ }
186
+ declare abstract class CustomOverlay {
187
+ protected options: CustomOverlayOptions;
188
+ private handle;
189
+ private currentMap;
190
+ constructor(options: CustomOverlayOptions);
191
+ abstract onAdd(): void;
192
+ abstract onRemove(): void;
193
+ abstract draw(): void;
194
+ setMap(map: KorMap | null): void;
195
+ setPosition(latlng: LatLng): void;
196
+ getPosition(): LatLng;
197
+ }
198
+ declare class TileLayer {
199
+ private options;
200
+ private handle;
201
+ private currentMap;
202
+ constructor(options: TileLayerOptions);
203
+ setMap(map: KorMap | null): void;
204
+ }
205
+
206
+ export { AnyEventHandler, Circle, CircleOptions, CustomOverlay, CustomOverlayOptions, EventListener, FitBoundsOptions, InfoWindow, InfoWindowOptions, KorMap, KorMapConfig, KorMapEvent, KorMapFeature, LatLng, LatLngBounds, MapProjection, MapProvider, MapTypeId, Marker, MarkerOptions, OverlayEvent, PanOptions, Polygon, PolygonOptions, Polyline, PolylineOptions, Rectangle, RectangleOptions, TileLayer, TileLayerOptions };
package/dist/core.js ADDED
@@ -0,0 +1,4 @@
1
+ export { Circle, ConfigurationError, CustomOverlay, InfoWindow, KorMap, KorMapError, Marker, Polygon, Polyline, ProviderLoadError, ProviderNotSupportedError, Rectangle, TileLayer } from './chunk-222QLGRB.js';
2
+ export { MapTypeId, MarkerAnimation } from './chunk-HUS54ONW.js';
3
+ //# sourceMappingURL=core.js.map
4
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"core.js"}
@@ -0,0 +1,41 @@
1
+ import { KorMap, Marker } from './core.js';
2
+ export { Circle, CustomOverlay, InfoWindow, Polygon, Polyline, Rectangle, TileLayer } from './core.js';
3
+ import { C as ClusterOptions } from './types-BWnSkE6e.js';
4
+ export { A as AnyEventHandler, a as CircleOptions, b as ClusterStyle, c as ConfigurationError, d as CustomOverlayOptions, E as EventListener, F as FillStyle, e as FitBoundsOptions, I as InfoWindowOptions, K as KorMapApiKey, f as KorMapConfig, g as KorMapError, h as KorMapEvent, i as KorMapFeature, L as LatLng, j as LatLngBounds, M as MapMouseEvent, k as MapProjection, l as MapProvider, m as MapTypeId, n as MarkerAnimation, o as MarkerIcon, p as MarkerLabel, q as MarkerOptions, O as OverlayEvent, P as Padding, r as PanOptions, s as Point, t as PolygonOptions, u as PolylineOptions, v as ProviderLoadError, w as ProviderNotSupportedError, R as RectangleOptions, S as Size, x as StrokeStyle, T as TileCoord, y as TileLayerOptions } from './types-BWnSkE6e.js';
5
+
6
+ /**
7
+ * Grid-based marker clusterer that works across all three providers.
8
+ *
9
+ * Clusters are rendered as `CustomOverlay` div elements so no external
10
+ * dependencies or provider-specific clustering plugins are needed.
11
+ *
12
+ * On each `idle` event the visible viewport is re-gridded using a self-contained
13
+ * Web Mercator projection (provider-agnostic). Markers whose world-pixel
14
+ * coordinates fall in the same `gridSize×gridSize` cell are collapsed into a
15
+ * single cluster badge. Clicking a cluster badge calls `fitBounds()` over its
16
+ * markers so the map zooms in to reveal them.
17
+ */
18
+ declare class MarkerClusterer {
19
+ private currentMap;
20
+ private readonly _markers;
21
+ private readonly _clusters;
22
+ private idleListener;
23
+ private readonly opts;
24
+ constructor(map: KorMap, markers?: Marker[], options?: ClusterOptions);
25
+ addMarker(marker: Marker, redraw?: boolean): void;
26
+ addMarkers(markers: Marker[]): void;
27
+ removeMarker(marker: Marker, redraw?: boolean): boolean;
28
+ clearMarkers(): void;
29
+ setMap(map: KorMap | null): void;
30
+ getMarkers(): Marker[];
31
+ getTotalMarkers(): number;
32
+ /** Force a full re-cluster pass. Call after programmatic map changes if needed. */
33
+ redraw(): void;
34
+ private _register;
35
+ private _bind;
36
+ private _redraw;
37
+ private _addClusterOverlay;
38
+ private _clearClusters;
39
+ }
40
+
41
+ export { ClusterOptions, KorMap, Marker, MarkerClusterer };
package/dist/index.js ADDED
@@ -0,0 +1,232 @@
1
+ export { Circle, ConfigurationError, CustomOverlay, InfoWindow, KorMap, KorMapError, Marker, Polygon, Polyline, ProviderLoadError, ProviderNotSupportedError, Rectangle, TileLayer } from './chunk-222QLGRB.js';
2
+ export { MapTypeId, MarkerAnimation } from './chunk-HUS54ONW.js';
3
+
4
+ // src/modules/clustering/MarkerClusterer.ts
5
+ var DEFAULT_STYLES = [
6
+ { width: 36, height: 36, textColor: "#ffffff", textSize: 12, backgroundColor: "#3b82f6" },
7
+ { width: 44, height: 44, textColor: "#ffffff", textSize: 13, backgroundColor: "#f59e0b" },
8
+ { width: 52, height: 52, textColor: "#ffffff", textSize: 14, backgroundColor: "#ef4444" }
9
+ ];
10
+ var DEFAULTS = {
11
+ minClusterSize: 2,
12
+ maxZoom: 15,
13
+ gridSize: 60,
14
+ averageCenter: false
15
+ };
16
+ function toWorldPixel(lat, lng, zoom) {
17
+ const scale = 256 * Math.pow(2, zoom);
18
+ const x = (lng + 180) / 360 * scale;
19
+ const siny = Math.min(Math.max(Math.sin(lat * Math.PI / 180), -0.9999), 0.9999);
20
+ const y = (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI)) * scale;
21
+ return { x, y };
22
+ }
23
+ function toBounds(positions) {
24
+ let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;
25
+ for (const p of positions) {
26
+ if (p.lat < minLat) minLat = p.lat;
27
+ if (p.lat > maxLat) maxLat = p.lat;
28
+ if (p.lng < minLng) minLng = p.lng;
29
+ if (p.lng > maxLng) maxLng = p.lng;
30
+ }
31
+ return {
32
+ sw: { lat: minLat, lng: minLng },
33
+ ne: { lat: maxLat, lng: maxLng }
34
+ };
35
+ }
36
+ function pickStyle(count, styles) {
37
+ const s0 = styles[0];
38
+ const s1 = styles[1];
39
+ const s2 = styles[2];
40
+ if (count < 10) return s0 ?? DEFAULT_STYLES[0];
41
+ if (count < 100) return s1 ?? s0 ?? DEFAULT_STYLES[1];
42
+ return s2 ?? s1 ?? s0 ?? DEFAULT_STYLES[2];
43
+ }
44
+ function cellKey(px, py, gridSize) {
45
+ return `${Math.floor(px / gridSize)},${Math.floor(py / gridSize)}`;
46
+ }
47
+ function centroid(positions) {
48
+ if (positions.length === 0) return { lat: 0, lng: 0 };
49
+ const lat = positions.reduce((s, p) => s + p.lat, 0) / positions.length;
50
+ const lng = positions.reduce((s, p) => s + p.lng, 0) / positions.length;
51
+ return { lat, lng };
52
+ }
53
+ function createClusterElement(count, style) {
54
+ const el = document.createElement("div");
55
+ const w = style.width;
56
+ const h = style.height;
57
+ const bg = style.backgroundColor ?? DEFAULT_STYLES[0].backgroundColor ?? "#3b82f6";
58
+ const color = style.textColor ?? "#ffffff";
59
+ const fontSize = style.textSize ?? 13;
60
+ el.style.cssText = [
61
+ `width:${w}px`,
62
+ `height:${h}px`,
63
+ `line-height:${h}px`,
64
+ `border-radius:50%`,
65
+ `text-align:center`,
66
+ `cursor:pointer`,
67
+ `font-weight:bold`,
68
+ `box-shadow:0 2px 6px rgba(0,0,0,.3)`,
69
+ `transform:translate(-50%,-50%)`,
70
+ `color:${color}`,
71
+ `font-size:${fontSize}px`,
72
+ style.url ? `background:url(${style.url}) center/cover` : `background-color:${bg}`
73
+ ].join(";");
74
+ el.textContent = String(count);
75
+ return el;
76
+ }
77
+ var MarkerClusterer = class {
78
+ currentMap;
79
+ _markers = [];
80
+ _clusters = [];
81
+ idleListener = null;
82
+ opts;
83
+ constructor(map, markers = [], options = {}) {
84
+ this.currentMap = map;
85
+ this.opts = {
86
+ ...DEFAULTS,
87
+ ...options,
88
+ styles: options.styles ?? DEFAULT_STYLES
89
+ };
90
+ this._bind(map);
91
+ for (const m of markers) {
92
+ this._markers.push(m);
93
+ if (m.getMap() !== map) m.setMap(map);
94
+ }
95
+ this._redraw();
96
+ }
97
+ // -------------------------------------------------------------------------
98
+ // Public API
99
+ // -------------------------------------------------------------------------
100
+ addMarker(marker, redraw = true) {
101
+ if (this._register(marker) && redraw) this._redraw();
102
+ }
103
+ addMarkers(markers) {
104
+ for (const m of markers) this._register(m);
105
+ this._redraw();
106
+ }
107
+ removeMarker(marker, redraw = true) {
108
+ const idx = this._markers.indexOf(marker);
109
+ if (idx === -1) return false;
110
+ this._markers.splice(idx, 1);
111
+ marker.setVisible(true);
112
+ if (redraw) this._redraw();
113
+ return true;
114
+ }
115
+ clearMarkers() {
116
+ this._clearClusters();
117
+ for (const m of this._markers) m.setVisible(true);
118
+ this._markers.length = 0;
119
+ }
120
+ setMap(map) {
121
+ if (this.currentMap && this.idleListener) {
122
+ this.currentMap.off("idle", this.idleListener);
123
+ this.idleListener = null;
124
+ }
125
+ this._clearClusters();
126
+ if (map === null) {
127
+ for (const m of this._markers) m.setVisible(true);
128
+ }
129
+ this.currentMap = map;
130
+ if (map) {
131
+ this._bind(map);
132
+ this._redraw();
133
+ }
134
+ }
135
+ getMarkers() {
136
+ return [...this._markers];
137
+ }
138
+ getTotalMarkers() {
139
+ return this._markers.length;
140
+ }
141
+ /** Force a full re-cluster pass. Call after programmatic map changes if needed. */
142
+ redraw() {
143
+ this._redraw();
144
+ }
145
+ // -------------------------------------------------------------------------
146
+ // Private
147
+ // -------------------------------------------------------------------------
148
+ _register(marker) {
149
+ if (this._markers.includes(marker)) return false;
150
+ this._markers.push(marker);
151
+ if (this.currentMap && marker.getMap() !== this.currentMap) {
152
+ marker.setMap(this.currentMap);
153
+ }
154
+ return true;
155
+ }
156
+ _bind(map) {
157
+ if (this.idleListener) return;
158
+ this.idleListener = map.on("idle", () => {
159
+ this._redraw();
160
+ });
161
+ }
162
+ _redraw() {
163
+ const map = this.currentMap;
164
+ if (!map) return;
165
+ this._clearClusters();
166
+ const zoom = map.getZoom();
167
+ if (zoom > this.opts.maxZoom) {
168
+ for (const m of this._markers) m.setVisible(true);
169
+ return;
170
+ }
171
+ for (const m of this._markers) {
172
+ if (m.getMap()) m.setVisible(false);
173
+ }
174
+ const cells = /* @__PURE__ */ new Map();
175
+ for (const marker of this._markers) {
176
+ if (!marker.getMap()) continue;
177
+ const { lat, lng } = marker.getPosition();
178
+ const pt = toWorldPixel(lat, lng, zoom);
179
+ const key = cellKey(pt.x, pt.y, this.opts.gridSize);
180
+ const existing = cells.get(key);
181
+ if (existing !== void 0) {
182
+ existing.push(marker);
183
+ } else {
184
+ cells.set(key, [marker]);
185
+ }
186
+ }
187
+ for (const group of cells.values()) {
188
+ if (group.length >= this.opts.minClusterSize) {
189
+ this._addClusterOverlay(map, group);
190
+ } else {
191
+ for (const m of group) m.setVisible(true);
192
+ }
193
+ }
194
+ }
195
+ _addClusterOverlay(map, group) {
196
+ const positions = group.map((m) => m.getPosition());
197
+ const firstPos = positions[0];
198
+ const center = this.opts.averageCenter ? centroid(positions) : firstPos ?? { lat: 0, lng: 0 };
199
+ const style = pickStyle(group.length, this.opts.styles);
200
+ const el = createClusterElement(group.length, style);
201
+ el.addEventListener("click", (e) => {
202
+ e.stopPropagation();
203
+ const bounds = toBounds(positions);
204
+ if (bounds.sw.lat === bounds.ne.lat && bounds.sw.lng === bounds.ne.lng) {
205
+ map.setCenter(bounds.sw);
206
+ map.setZoom(this.opts.maxZoom + 1);
207
+ } else {
208
+ map.fitBounds(bounds, { padding: 60 });
209
+ }
210
+ });
211
+ const handle = map._adapter().addCustomOverlay({
212
+ position: center,
213
+ content: el,
214
+ zIndex: 200,
215
+ clickable: true
216
+ });
217
+ this._clusters.push({ handle, markers: group });
218
+ }
219
+ _clearClusters() {
220
+ const map = this.currentMap;
221
+ if (map) {
222
+ for (const c of this._clusters) {
223
+ map._adapter().removeCustomOverlay(c.handle);
224
+ }
225
+ }
226
+ this._clusters.length = 0;
227
+ }
228
+ };
229
+
230
+ export { MarkerClusterer };
231
+ //# sourceMappingURL=index.js.map
232
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/modules/clustering/MarkerClusterer.ts"],"names":[],"mappings":";;;;AASA,IAAM,cAAA,GAA6D;AAAA,EACjE,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,EAAA,EAAI,WAAW,SAAA,EAAW,QAAA,EAAU,EAAA,EAAI,eAAA,EAAiB,SAAA,EAAU;AAAA,EACxF,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,EAAA,EAAI,WAAW,SAAA,EAAW,QAAA,EAAU,EAAA,EAAI,eAAA,EAAiB,SAAA,EAAU;AAAA,EACxF,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,EAAA,EAAI,WAAW,SAAA,EAAW,QAAA,EAAU,EAAA,EAAI,eAAA,EAAiB,SAAA;AAChF,CAAA;AAEA,IAAM,QAAA,GAAW;AAAA,EACf,cAAA,EAAgB,CAAA;AAAA,EAChB,OAAA,EAAS,EAAA;AAAA,EACT,QAAA,EAAU,EAAA;AAAA,EACV,aAAA,EAAe;AACjB,CAAA;AA+BO,SAAS,YAAA,CAAa,GAAA,EAAa,GAAA,EAAa,IAAA,EAAwC;AAC7F,EAAA,MAAM,KAAA,GAAQ,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,GAAG,IAAI,CAAA;AACpC,EAAA,MAAM,CAAA,GAAA,CAAK,GAAA,GAAM,GAAA,IAAO,GAAA,GAAM,KAAA;AAC9B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,IAAI,IAAA,CAAK,GAAA,CAAI,GAAA,GAAM,IAAA,CAAK,EAAA,GAAK,GAAG,CAAA,EAAG,OAAO,GAAG,MAAM,CAAA;AAC9E,EAAA,MAAM,CAAA,GAAA,CAAK,GAAA,GAAM,IAAA,CAAK,GAAA,CAAA,CAAK,CAAA,GAAI,IAAA,KAAS,CAAA,GAAI,IAAA,CAAK,CAAA,IAAK,CAAA,GAAI,IAAA,CAAK,EAAA,CAAA,IAAO,KAAA;AACtE,EAAA,OAAO,EAAE,GAAG,CAAA,EAAE;AAChB;AAGO,SAAS,SAAS,SAAA,EAAmC;AAC1D,EAAA,IAAI,SAAS,QAAA,EAAU,MAAA,GAAS,CAAA,QAAA,EAAW,MAAA,GAAS,UAAU,MAAA,GAAS,CAAA,QAAA;AACvE,EAAA,KAAA,MAAW,KAAK,SAAA,EAAW;AACzB,IAAA,IAAI,CAAA,CAAE,GAAA,GAAM,MAAA,EAAQ,MAAA,GAAS,CAAA,CAAE,GAAA;AAC/B,IAAA,IAAI,CAAA,CAAE,GAAA,GAAM,MAAA,EAAQ,MAAA,GAAS,CAAA,CAAE,GAAA;AAC/B,IAAA,IAAI,CAAA,CAAE,GAAA,GAAM,MAAA,EAAQ,MAAA,GAAS,CAAA,CAAE,GAAA;AAC/B,IAAA,IAAI,CAAA,CAAE,GAAA,GAAM,MAAA,EAAQ,MAAA,GAAS,CAAA,CAAE,GAAA;AAAA,EACjC;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,EAAE,GAAA,EAAK,MAAA,EAAQ,KAAK,MAAA,EAAO;AAAA,IAC/B,EAAA,EAAI,EAAE,GAAA,EAAK,MAAA,EAAQ,KAAK,MAAA;AAAO,GACjC;AACF;AAGO,SAAS,SAAA,CAAU,OAAe,MAAA,EAAsC;AAC7E,EAAA,MAAM,EAAA,GAAK,OAAO,CAAC,CAAA;AACnB,EAAA,MAAM,EAAA,GAAK,OAAO,CAAC,CAAA;AACnB,EAAA,MAAM,EAAA,GAAK,OAAO,CAAC,CAAA;AACnB,EAAA,IAAI,KAAA,GAAQ,EAAA,EAAI,OAAO,EAAA,IAAM,eAAe,CAAC,CAAA;AAC7C,EAAA,IAAI,QAAQ,GAAA,EAAK,OAAO,EAAA,IAAM,EAAA,IAAM,eAAe,CAAC,CAAA;AACpD,EAAA,OAAO,EAAA,IAAM,EAAA,IAAM,EAAA,IAAM,cAAA,CAAe,CAAC,CAAA;AAC3C;AAGO,SAAS,OAAA,CAAQ,EAAA,EAAY,EAAA,EAAY,QAAA,EAA0B;AACxE,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,QAAQ,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,QAAQ,CAAC,CAAA,CAAA;AAClE;AAGO,SAAS,SAAS,SAAA,EAA6B;AACpD,EAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG,OAAO,EAAE,GAAA,EAAK,CAAA,EAAG,KAAK,CAAA,EAAE;AACpD,EAAA,MAAM,GAAA,GAAM,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAA,CAAE,GAAA,EAAK,CAAC,CAAA,GAAI,SAAA,CAAU,MAAA;AACjE,EAAA,MAAM,GAAA,GAAM,SAAA,CAAU,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAA,CAAE,GAAA,EAAK,CAAC,CAAA,GAAI,SAAA,CAAU,MAAA;AACjE,EAAA,OAAO,EAAE,KAAK,GAAA,EAAI;AACpB;AAEA,SAAS,oBAAA,CAAqB,OAAe,KAAA,EAAkC;AAC7E,EAAA,MAAM,EAAA,GAAK,QAAA,CAAS,aAAA,CAAc,KAAK,CAAA;AACvC,EAAA,MAAM,IAAI,KAAA,CAAM,KAAA;AAChB,EAAA,MAAM,IAAI,KAAA,CAAM,MAAA;AAChB,EAAA,MAAM,KAAK,KAAA,CAAM,eAAA,IAAmB,cAAA,CAAe,CAAC,EAAE,eAAA,IAAmB,SAAA;AACzE,EAAA,MAAM,KAAA,GAAQ,MAAM,SAAA,IAAa,SAAA;AACjC,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,EAAA;AAGnC,EAAA,EAAA,CAAG,MAAM,OAAA,GAAU;AAAA,IACjB,SAAS,CAAC,CAAA,EAAA,CAAA;AAAA,IACV,UAAU,CAAC,CAAA,EAAA,CAAA;AAAA,IACX,eAAe,CAAC,CAAA,EAAA,CAAA;AAAA,IAChB,CAAA,iBAAA,CAAA;AAAA,IACA,CAAA,iBAAA,CAAA;AAAA,IACA,CAAA,cAAA,CAAA;AAAA,IACA,CAAA,gBAAA,CAAA;AAAA,IACA,CAAA,mCAAA,CAAA;AAAA,IACA,CAAA,8BAAA,CAAA;AAAA,IACA,SAAS,KAAK,CAAA,CAAA;AAAA,IACd,aAAa,QAAQ,CAAA,EAAA,CAAA;AAAA,IACrB,MAAM,GAAA,GACF,CAAA,eAAA,EAAkB,MAAM,GAAG,CAAA,cAAA,CAAA,GAC3B,oBAAoB,EAAE,CAAA;AAAA,GAC5B,CAAE,KAAK,GAAG,CAAA;AAEV,EAAA,EAAA,CAAG,WAAA,GAAc,OAAO,KAAK,CAAA;AAC7B,EAAA,OAAO,EAAA;AACT;AAkBO,IAAM,kBAAN,MAAsB;AAAA,EACnB,UAAA;AAAA,EACS,WAAqB,EAAC;AAAA,EACtB,YAA2B,EAAC;AAAA,EACrC,YAAA,GAAqC,IAAA;AAAA,EAC5B,IAAA;AAAA,EAEjB,YAAY,GAAA,EAAa,OAAA,GAAoB,EAAC,EAAG,OAAA,GAA0B,EAAC,EAAG;AAC7E,IAAA,IAAA,CAAK,UAAA,GAAa,GAAA;AAClB,IAAA,IAAA,CAAK,IAAA,GAAO;AAAA,MACV,GAAG,QAAA;AAAA,MACH,GAAG,OAAA;AAAA,MACH,MAAA,EAAQ,QAAQ,MAAA,IAAU;AAAA,KAC5B;AAEA,IAAA,IAAA,CAAK,MAAM,GAAG,CAAA;AAEd,IAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,MAAA,IAAA,CAAK,QAAA,CAAS,KAAK,CAAC,CAAA;AACpB,MAAA,IAAI,EAAE,MAAA,EAAO,KAAM,GAAA,EAAK,CAAA,CAAE,OAAO,GAAG,CAAA;AAAA,IACtC;AAEA,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAMA,SAAA,CAAU,MAAA,EAAgB,MAAA,GAAS,IAAA,EAAY;AAC7C,IAAA,IAAI,KAAK,SAAA,CAAU,MAAM,CAAA,IAAK,MAAA,OAAa,OAAA,EAAQ;AAAA,EACrD;AAAA,EAEA,WAAW,OAAA,EAAyB;AAClC,IAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,IAAA,CAAK,SAAA,CAAU,CAAC,CAAA;AACzC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA,EAEA,YAAA,CAAa,MAAA,EAAgB,MAAA,GAAS,IAAA,EAAe;AACnD,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,MAAM,CAAA;AACxC,IAAA,IAAI,GAAA,KAAQ,IAAI,OAAO,KAAA;AACvB,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,GAAA,EAAK,CAAC,CAAA;AAC3B,IAAA,MAAA,CAAO,WAAW,IAAI,CAAA;AACtB,IAAA,IAAI,MAAA,OAAa,OAAA,EAAQ;AACzB,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,YAAA,GAAqB;AACnB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,QAAA,EAAU,CAAA,CAAE,WAAW,IAAI,CAAA;AAChD,IAAA,IAAA,CAAK,SAAS,MAAA,GAAS,CAAA;AAAA,EACzB;AAAA,EAEA,OAAO,GAAA,EAA0B;AAC/B,IAAA,IAAI,IAAA,CAAK,UAAA,IAAc,IAAA,CAAK,YAAA,EAAc;AACxC,MAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,MAAA,EAAQ,IAAA,CAAK,YAAY,CAAA;AAC7C,MAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAAA,IACtB;AACA,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAI,QAAQ,IAAA,EAAM;AAChB,MAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,QAAA,EAAU,CAAA,CAAE,WAAW,IAAI,CAAA;AAAA,IAClD;AACA,IAAA,IAAA,CAAK,UAAA,GAAa,GAAA;AAClB,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,IAAA,CAAK,MAAM,GAAG,CAAA;AACd,MAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,IACf;AAAA,EACF;AAAA,EAEA,UAAA,GAAuB;AACrB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA;AAAA,EAC1B;AAAA,EAEA,eAAA,GAA0B;AACxB,IAAA,OAAO,KAAK,QAAA,CAAS,MAAA;AAAA,EACvB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAMQ,UAAU,MAAA,EAAyB;AACzC,IAAA,IAAI,IAAA,CAAK,QAAA,CAAS,QAAA,CAAS,MAAM,GAAG,OAAO,KAAA;AAC3C,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,MAAM,CAAA;AACzB,IAAA,IAAI,KAAK,UAAA,IAAc,MAAA,CAAO,MAAA,EAAO,KAAM,KAAK,UAAA,EAAY;AAC1D,MAAA,MAAA,CAAO,MAAA,CAAO,KAAK,UAAU,CAAA;AAAA,IAC/B;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEQ,MAAM,GAAA,EAAmB;AAC/B,IAAA,IAAI,KAAK,YAAA,EAAc;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,MAAM;AAAE,MAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,IAAG,CAAC,CAAA;AAAA,EAC9D;AAAA,EAEQ,OAAA,GAAgB;AACtB,IAAA,MAAM,MAAM,IAAA,CAAK,UAAA;AACjB,IAAA,IAAI,CAAC,GAAA,EAAK;AAEV,IAAA,IAAA,CAAK,cAAA,EAAe;AAEpB,IAAA,MAAM,IAAA,GAAO,IAAI,OAAA,EAAQ;AACzB,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,IAAA,CAAK,OAAA,EAAS;AAC5B,MAAA,KAAA,MAAW,CAAA,IAAK,IAAA,CAAK,QAAA,EAAU,CAAA,CAAE,WAAW,IAAI,CAAA;AAChD,MAAA;AAAA,IACF;AAGA,IAAA,KAAA,MAAW,CAAA,IAAK,KAAK,QAAA,EAAU;AAC7B,MAAA,IAAI,CAAA,CAAE,MAAA,EAAO,EAAG,CAAA,CAAE,WAAW,KAAK,CAAA;AAAA,IACpC;AAEA,IAAA,MAAM,KAAA,uBAAY,GAAA,EAAsB;AAExC,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,QAAA,EAAU;AAClC,MAAA,IAAI,CAAC,MAAA,CAAO,MAAA,EAAO,EAAG;AACtB,MAAA,MAAM,EAAE,GAAA,EAAK,GAAA,EAAI,GAAI,OAAO,WAAA,EAAY;AAGxC,MAAA,MAAM,EAAA,GAAK,YAAA,CAAa,GAAA,EAAK,GAAA,EAAK,IAAI,CAAA;AACtC,MAAA,MAAM,GAAA,GAAM,QAAQ,EAAA,CAAG,CAAA,EAAG,GAAG,CAAA,EAAG,IAAA,CAAK,KAAK,QAAQ,CAAA;AAClD,MAAA,MAAM,QAAA,GAAW,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC9B,MAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,QAAA,QAAA,CAAS,KAAK,MAAM,CAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,KAAA,CAAM,GAAA,CAAI,GAAA,EAAK,CAAC,MAAM,CAAC,CAAA;AAAA,MACzB;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,KAAA,IAAS,KAAA,CAAM,MAAA,EAAO,EAAG;AAClC,MAAA,IAAI,KAAA,CAAM,MAAA,IAAU,IAAA,CAAK,IAAA,CAAK,cAAA,EAAgB;AAC5C,QAAA,IAAA,CAAK,kBAAA,CAAmB,KAAK,KAAK,CAAA;AAAA,MACpC,CAAA,MAAO;AACL,QAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,CAAA,CAAE,UAAA,CAAW,IAAI,CAAA;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,kBAAA,CAAmB,KAAa,KAAA,EAAuB;AAC7D,IAAA,MAAM,YAAY,KAAA,CAAM,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,aAAa,CAAA;AAChD,IAAA,MAAM,QAAA,GAAW,UAAU,CAAC,CAAA;AAC5B,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,IAAA,CAAK,aAAA,GACrB,QAAA,CAAS,SAAS,CAAA,GACjB,QAAA,IAAY,EAAE,GAAA,EAAK,CAAA,EAAG,GAAA,EAAK,CAAA,EAAE;AAElC,IAAA,MAAM,QAAQ,SAAA,CAAU,KAAA,CAAM,MAAA,EAAQ,IAAA,CAAK,KAAK,MAAM,CAAA;AACtD,IAAA,MAAM,EAAA,GAAK,oBAAA,CAAqB,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA;AAGnD,IAAA,EAAA,CAAG,gBAAA,CAAiB,OAAA,EAAS,CAAC,CAAA,KAAM;AAClC,MAAA,CAAA,CAAE,eAAA,EAAgB;AAClB,MAAA,MAAM,MAAA,GAAS,SAAS,SAAS,CAAA;AAEjC,MAAA,IAAI,MAAA,CAAO,EAAA,CAAG,GAAA,KAAQ,MAAA,CAAO,EAAA,CAAG,GAAA,IAAO,MAAA,CAAO,EAAA,CAAG,GAAA,KAAQ,MAAA,CAAO,EAAA,CAAG,GAAA,EAAK;AACtE,QAAA,GAAA,CAAI,SAAA,CAAU,OAAO,EAAE,CAAA;AACvB,QAAA,GAAA,CAAI,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,OAAA,GAAU,CAAC,CAAA;AAAA,MACnC,CAAA,MAAO;AACL,QAAA,GAAA,CAAI,SAAA,CAAU,MAAA,EAAQ,EAAE,OAAA,EAAS,IAAI,CAAA;AAAA,MACvC;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAM,MAAA,GAAS,GAAA,CAAI,QAAA,EAAS,CAAE,gBAAA,CAAiB;AAAA,MAC7C,QAAA,EAAU,MAAA;AAAA,MACV,OAAA,EAAS,EAAA;AAAA,MACT,MAAA,EAAQ,GAAA;AAAA,MACR,SAAA,EAAW;AAAA,KACZ,CAAA;AAED,IAAA,IAAA,CAAK,UAAU,IAAA,CAAK,EAAE,MAAA,EAAQ,OAAA,EAAS,OAAO,CAAA;AAAA,EAChD;AAAA,EAEQ,cAAA,GAAuB;AAC7B,IAAA,MAAM,MAAM,IAAA,CAAK,UAAA;AACjB,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,KAAA,MAAW,CAAA,IAAK,KAAK,SAAA,EAAW;AAC9B,QAAA,GAAA,CAAI,QAAA,EAAS,CAAE,mBAAA,CAAoB,CAAA,CAAE,MAAM,CAAA;AAAA,MAC7C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,UAAU,MAAA,GAAS,CAAA;AAAA,EAC1B;AACF","file":"index.js","sourcesContent":["import type { KorMap } from '../../core/KorMap.js';\nimport type { NativeOverlayHandle } from '../../providers/base/IMapProvider.js';\nimport type { ClusterOptions, ClusterStyle, EventListener, LatLng, LatLngBounds } from '../../core/types.js';\nimport { Marker } from '../../core/overlays.js';\n\n// ---------------------------------------------------------------------------\n// Default tier styles (small / medium / large by cluster count)\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_STYLES: [ClusterStyle, ClusterStyle, ClusterStyle] = [\n { width: 36, height: 36, textColor: '#ffffff', textSize: 12, backgroundColor: '#3b82f6' },\n { width: 44, height: 44, textColor: '#ffffff', textSize: 13, backgroundColor: '#f59e0b' },\n { width: 52, height: 52, textColor: '#ffffff', textSize: 14, backgroundColor: '#ef4444' },\n];\n\nconst DEFAULTS = {\n minClusterSize: 2,\n maxZoom: 15,\n gridSize: 60,\n averageCenter: false,\n} as const;\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ninterface ClusterData {\n handle: NativeOverlayHandle;\n markers: Marker[];\n}\n\ninterface ResolvedOptions {\n minClusterSize: number;\n maxZoom: number;\n gridSize: number;\n styles: ClusterStyle[];\n averageCenter: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Pure helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a LatLng to a Mercator world pixel coordinate at the given zoom level.\n *\n * Uses the standard Web Mercator (EPSG:3857) formula: at zoom Z the world is\n * 256×2^Z pixels wide. This is provider-agnostic and gives consistent grid\n * cell assignments regardless of whether the adapter's getProjection() is a\n * stub or a full implementation.\n */\nexport function toWorldPixel(lat: number, lng: number, zoom: number): { x: number; y: number } {\n const scale = 256 * Math.pow(2, zoom);\n const x = (lng + 180) / 360 * scale;\n const siny = Math.min(Math.max(Math.sin(lat * Math.PI / 180), -0.9999), 0.9999);\n const y = (0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI)) * scale;\n return { x, y };\n}\n\n/** Compute the LatLngBounds that tightly contains all given positions. */\nexport function toBounds(positions: LatLng[]): LatLngBounds {\n let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity;\n for (const p of positions) {\n if (p.lat < minLat) minLat = p.lat;\n if (p.lat > maxLat) maxLat = p.lat;\n if (p.lng < minLng) minLng = p.lng;\n if (p.lng > maxLng) maxLng = p.lng;\n }\n return {\n sw: { lat: minLat, lng: minLng },\n ne: { lat: maxLat, lng: maxLng },\n };\n}\n\n/** Pick a style tier based on cluster size. Falls back through tiers gracefully. */\nexport function pickStyle(count: number, styles: ClusterStyle[]): ClusterStyle {\n const s0 = styles[0];\n const s1 = styles[1];\n const s2 = styles[2];\n if (count < 10) return s0 ?? DEFAULT_STYLES[0];\n if (count < 100) return s1 ?? s0 ?? DEFAULT_STYLES[1];\n return s2 ?? s1 ?? s0 ?? DEFAULT_STYLES[2];\n}\n\n/** Assign a pixel coordinate to a grid cell key. */\nexport function cellKey(px: number, py: number, gridSize: number): string {\n return `${Math.floor(px / gridSize)},${Math.floor(py / gridSize)}`;\n}\n\n/** Compute the centroid of an array of LatLng positions. */\nexport function centroid(positions: LatLng[]): LatLng {\n if (positions.length === 0) return { lat: 0, lng: 0 };\n const lat = positions.reduce((s, p) => s + p.lat, 0) / positions.length;\n const lng = positions.reduce((s, p) => s + p.lng, 0) / positions.length;\n return { lat, lng };\n}\n\nfunction createClusterElement(count: number, style: ClusterStyle): HTMLElement {\n const el = document.createElement('div');\n const w = style.width;\n const h = style.height;\n const bg = style.backgroundColor ?? DEFAULT_STYLES[0].backgroundColor ?? '#3b82f6';\n const color = style.textColor ?? '#ffffff';\n const fontSize = style.textSize ?? 13;\n\n // transform centers the element on the anchor point (position LatLng)\n el.style.cssText = [\n `width:${w}px`,\n `height:${h}px`,\n `line-height:${h}px`,\n `border-radius:50%`,\n `text-align:center`,\n `cursor:pointer`,\n `font-weight:bold`,\n `box-shadow:0 2px 6px rgba(0,0,0,.3)`,\n `transform:translate(-50%,-50%)`,\n `color:${color}`,\n `font-size:${fontSize}px`,\n style.url\n ? `background:url(${style.url}) center/cover`\n : `background-color:${bg}`,\n ].join(';');\n\n el.textContent = String(count);\n return el;\n}\n\n// ---------------------------------------------------------------------------\n// MarkerClusterer\n// ---------------------------------------------------------------------------\n\n/**\n * Grid-based marker clusterer that works across all three providers.\n *\n * Clusters are rendered as `CustomOverlay` div elements so no external\n * dependencies or provider-specific clustering plugins are needed.\n *\n * On each `idle` event the visible viewport is re-gridded using a self-contained\n * Web Mercator projection (provider-agnostic). Markers whose world-pixel\n * coordinates fall in the same `gridSize×gridSize` cell are collapsed into a\n * single cluster badge. Clicking a cluster badge calls `fitBounds()` over its\n * markers so the map zooms in to reveal them.\n */\nexport class MarkerClusterer {\n private currentMap: KorMap | null;\n private readonly _markers: Marker[] = [];\n private readonly _clusters: ClusterData[] = [];\n private idleListener: EventListener | null = null;\n private readonly opts: ResolvedOptions;\n\n constructor(map: KorMap, markers: Marker[] = [], options: ClusterOptions = {}) {\n this.currentMap = map;\n this.opts = {\n ...DEFAULTS,\n ...options,\n styles: options.styles ?? DEFAULT_STYLES,\n };\n\n this._bind(map);\n\n for (const m of markers) {\n this._markers.push(m);\n if (m.getMap() !== map) m.setMap(map);\n }\n\n this._redraw();\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n addMarker(marker: Marker, redraw = true): void {\n if (this._register(marker) && redraw) this._redraw();\n }\n\n addMarkers(markers: Marker[]): void {\n for (const m of markers) this._register(m);\n this._redraw();\n }\n\n removeMarker(marker: Marker, redraw = true): boolean {\n const idx = this._markers.indexOf(marker);\n if (idx === -1) return false;\n this._markers.splice(idx, 1);\n marker.setVisible(true); // restore before returning ownership to caller\n if (redraw) this._redraw();\n return true;\n }\n\n clearMarkers(): void {\n this._clearClusters();\n for (const m of this._markers) m.setVisible(true);\n this._markers.length = 0;\n }\n\n setMap(map: KorMap | null): void {\n if (this.currentMap && this.idleListener) {\n this.currentMap.off('idle', this.idleListener);\n this.idleListener = null;\n }\n this._clearClusters();\n if (map === null) {\n for (const m of this._markers) m.setVisible(true);\n }\n this.currentMap = map;\n if (map) {\n this._bind(map);\n this._redraw();\n }\n }\n\n getMarkers(): Marker[] {\n return [...this._markers];\n }\n\n getTotalMarkers(): number {\n return this._markers.length;\n }\n\n /** Force a full re-cluster pass. Call after programmatic map changes if needed. */\n redraw(): void {\n this._redraw();\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n private _register(marker: Marker): boolean {\n if (this._markers.includes(marker)) return false;\n this._markers.push(marker);\n if (this.currentMap && marker.getMap() !== this.currentMap) {\n marker.setMap(this.currentMap);\n }\n return true;\n }\n\n private _bind(map: KorMap): void {\n if (this.idleListener) return; // already bound to this map\n this.idleListener = map.on('idle', () => { this._redraw(); });\n }\n\n private _redraw(): void {\n const map = this.currentMap;\n if (!map) return;\n\n this._clearClusters();\n\n const zoom = map.getZoom();\n if (zoom > this.opts.maxZoom) {\n for (const m of this._markers) m.setVisible(true);\n return;\n }\n\n // Hide all managed markers; we'll selectively re-show unclustered ones below.\n for (const m of this._markers) {\n if (m.getMap()) m.setVisible(false);\n }\n\n const cells = new Map<string, Marker[]>();\n\n for (const marker of this._markers) {\n if (!marker.getMap()) continue;\n const { lat, lng } = marker.getPosition();\n // Use self-contained Mercator world pixels — provider-agnostic and\n // consistent regardless of adapter getProjection() implementation.\n const pt = toWorldPixel(lat, lng, zoom);\n const key = cellKey(pt.x, pt.y, this.opts.gridSize);\n const existing = cells.get(key);\n if (existing !== undefined) {\n existing.push(marker);\n } else {\n cells.set(key, [marker]);\n }\n }\n\n for (const group of cells.values()) {\n if (group.length >= this.opts.minClusterSize) {\n this._addClusterOverlay(map, group);\n } else {\n for (const m of group) m.setVisible(true);\n }\n }\n }\n\n private _addClusterOverlay(map: KorMap, group: Marker[]): void {\n const positions = group.map(m => m.getPosition());\n const firstPos = positions[0];\n const center = this.opts.averageCenter\n ? centroid(positions)\n : (firstPos ?? { lat: 0, lng: 0 });\n\n const style = pickStyle(group.length, this.opts.styles);\n const el = createClusterElement(group.length, style);\n\n // Click expands the cluster by fitting the map to its markers' bounds.\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const bounds = toBounds(positions);\n // If all markers are at the same point, just zoom past maxZoom instead.\n if (bounds.sw.lat === bounds.ne.lat && bounds.sw.lng === bounds.ne.lng) {\n map.setCenter(bounds.sw);\n map.setZoom(this.opts.maxZoom + 1);\n } else {\n map.fitBounds(bounds, { padding: 60 });\n }\n });\n\n const handle = map._adapter().addCustomOverlay({\n position: center,\n content: el,\n zIndex: 200,\n clickable: true,\n });\n\n this._clusters.push({ handle, markers: group });\n }\n\n private _clearClusters(): void {\n const map = this.currentMap;\n if (map) {\n for (const c of this._clusters) {\n map._adapter().removeCustomOverlay(c.handle);\n }\n }\n this._clusters.length = 0;\n }\n}\n"]}