react-naver-maps-kit 1.0.0 → 1.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/README.md +10 -5
- package/dist/index.cjs +1380 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1379 -42
- package/dist/index.js.map +1 -1
- package/dist/overlays/marker/Marker.d.ts +4 -0
- package/dist/overlays/marker/Marker.d.ts.map +1 -1
- package/dist/overlays/marker-clusterer/ClustererContext.d.ts +53 -0
- package/dist/overlays/marker-clusterer/ClustererContext.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/MarkerClusterer.d.ts +48 -0
- package/dist/overlays/marker-clusterer/MarkerClusterer.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/algorithms/createAlgorithm.d.ts +4 -0
- package/dist/overlays/marker-clusterer/algorithms/createAlgorithm.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/algorithms/grid.d.ts +17 -0
- package/dist/overlays/marker-clusterer/algorithms/grid.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/algorithms/radius.d.ts +17 -0
- package/dist/overlays/marker-clusterer/algorithms/radius.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/algorithms/supercluster.d.ts +25 -0
- package/dist/overlays/marker-clusterer/algorithms/supercluster.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/types.d.ts +376 -0
- package/dist/overlays/marker-clusterer/types.d.ts.map +1 -0
- package/dist/react/components/NaverMap.d.ts.map +1 -1
- package/package.json +8 -10
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
let react = require("react");
|
|
3
3
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
4
4
|
let react_dom = require("react-dom");
|
|
5
|
+
let react_dom_client = require("react-dom/client");
|
|
5
6
|
|
|
6
7
|
//#region src/core/loader/loadNaverMapsScript.ts
|
|
7
8
|
const NAVER_MAPS_SCRIPT_BASE_URL = "https://oapi.map.naver.com/openapi/v3/maps.js";
|
|
@@ -630,7 +631,10 @@ function getDivProps(props) {
|
|
|
630
631
|
function areNaverMapPropsEqual(previousProps, nextProps) {
|
|
631
632
|
const keys = new Set([...Object.keys(previousProps), ...Object.keys(nextProps)]);
|
|
632
633
|
for (const key of keys) {
|
|
633
|
-
if (key === "children")
|
|
634
|
+
if (key === "children") {
|
|
635
|
+
if (previousProps.children !== nextProps.children) return false;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
634
638
|
const previousValue = previousProps[key];
|
|
635
639
|
const nextValue = nextProps[key];
|
|
636
640
|
if (typeof previousValue === "function" && typeof nextValue === "function") continue;
|
|
@@ -869,6 +873,19 @@ function useNaverMapInstance(options = {}) {
|
|
|
869
873
|
}).map;
|
|
870
874
|
}
|
|
871
875
|
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/overlays/marker-clusterer/ClustererContext.ts
|
|
878
|
+
/**
|
|
879
|
+
* `<MarkerClusterer>`가 제공하는 React Context.
|
|
880
|
+
*
|
|
881
|
+
* `<Marker>`는 이 context를 통해 클러스터러에 자신을 등록합니다.
|
|
882
|
+
* `null`이면 클러스터러 밖에 있는 것이므로 일반 마커로 동작합니다.
|
|
883
|
+
*
|
|
884
|
+
* @internal 라이브러리 내부 전용. 직접 사용하지 마세요.
|
|
885
|
+
*/
|
|
886
|
+
const ClustererContext = (0, react.createContext)(null);
|
|
887
|
+
ClustererContext.displayName = "ClustererContext";
|
|
888
|
+
|
|
872
889
|
//#endregion
|
|
873
890
|
//#region src/overlays/marker/Marker.tsx
|
|
874
891
|
function pickHtmlIconAnchor(icon) {
|
|
@@ -902,87 +919,87 @@ function toMarkerOptions(props, targetMap, icon) {
|
|
|
902
919
|
if (props.zIndex !== void 0) options.zIndex = props.zIndex;
|
|
903
920
|
return options;
|
|
904
921
|
}
|
|
905
|
-
function buildMarkerEventBindings(
|
|
922
|
+
function buildMarkerEventBindings(propsRef) {
|
|
906
923
|
return [
|
|
907
924
|
{
|
|
908
925
|
eventName: "click",
|
|
909
|
-
invoke:
|
|
926
|
+
invoke: (event) => propsRef.current.onClick?.(event)
|
|
910
927
|
},
|
|
911
928
|
{
|
|
912
929
|
eventName: "dblclick",
|
|
913
|
-
invoke:
|
|
930
|
+
invoke: (event) => propsRef.current.onDblClick?.(event)
|
|
914
931
|
},
|
|
915
932
|
{
|
|
916
933
|
eventName: "rightclick",
|
|
917
|
-
invoke:
|
|
934
|
+
invoke: (event) => propsRef.current.onRightClick?.(event)
|
|
918
935
|
},
|
|
919
936
|
{
|
|
920
937
|
eventName: "mousedown",
|
|
921
|
-
invoke:
|
|
938
|
+
invoke: (event) => propsRef.current.onMouseDown?.(event)
|
|
922
939
|
},
|
|
923
940
|
{
|
|
924
941
|
eventName: "mouseup",
|
|
925
|
-
invoke:
|
|
942
|
+
invoke: (event) => propsRef.current.onMouseUp?.(event)
|
|
926
943
|
},
|
|
927
944
|
{
|
|
928
945
|
eventName: "touchstart",
|
|
929
|
-
invoke:
|
|
946
|
+
invoke: (event) => propsRef.current.onTouchStart?.(event)
|
|
930
947
|
},
|
|
931
948
|
{
|
|
932
949
|
eventName: "touchend",
|
|
933
|
-
invoke:
|
|
950
|
+
invoke: (event) => propsRef.current.onTouchEnd?.(event)
|
|
934
951
|
},
|
|
935
952
|
{
|
|
936
953
|
eventName: "dragstart",
|
|
937
|
-
invoke:
|
|
954
|
+
invoke: (event) => propsRef.current.onDragStart?.(event)
|
|
938
955
|
},
|
|
939
956
|
{
|
|
940
957
|
eventName: "drag",
|
|
941
|
-
invoke:
|
|
958
|
+
invoke: (event) => propsRef.current.onDrag?.(event)
|
|
942
959
|
},
|
|
943
960
|
{
|
|
944
961
|
eventName: "dragend",
|
|
945
|
-
invoke:
|
|
962
|
+
invoke: (event) => propsRef.current.onDragEnd?.(event)
|
|
946
963
|
},
|
|
947
964
|
{
|
|
948
965
|
eventName: "clickable_changed",
|
|
949
|
-
invoke:
|
|
966
|
+
invoke: (event) => propsRef.current.onClickableChanged?.(event)
|
|
950
967
|
},
|
|
951
968
|
{
|
|
952
969
|
eventName: "cursor_changed",
|
|
953
|
-
invoke:
|
|
970
|
+
invoke: (event) => propsRef.current.onCursorChanged?.(event)
|
|
954
971
|
},
|
|
955
972
|
{
|
|
956
973
|
eventName: "draggable_changed",
|
|
957
|
-
invoke:
|
|
974
|
+
invoke: (event) => propsRef.current.onDraggableChanged?.(event)
|
|
958
975
|
},
|
|
959
976
|
{
|
|
960
977
|
eventName: "icon_changed",
|
|
961
|
-
invoke:
|
|
978
|
+
invoke: (event) => propsRef.current.onIconChanged?.(event)
|
|
962
979
|
},
|
|
963
980
|
{
|
|
964
981
|
eventName: "icon_loaded",
|
|
965
|
-
invoke:
|
|
982
|
+
invoke: (event) => propsRef.current.onIconLoaded?.(event)
|
|
966
983
|
},
|
|
967
984
|
{
|
|
968
985
|
eventName: "position_changed",
|
|
969
|
-
invoke:
|
|
986
|
+
invoke: (event) => propsRef.current.onPositionChanged?.(event)
|
|
970
987
|
},
|
|
971
988
|
{
|
|
972
989
|
eventName: "shape_changed",
|
|
973
|
-
invoke:
|
|
990
|
+
invoke: (event) => propsRef.current.onShapeChanged?.(event)
|
|
974
991
|
},
|
|
975
992
|
{
|
|
976
993
|
eventName: "title_changed",
|
|
977
|
-
invoke:
|
|
994
|
+
invoke: (event) => propsRef.current.onTitleChanged?.(event)
|
|
978
995
|
},
|
|
979
996
|
{
|
|
980
997
|
eventName: "visible_changed",
|
|
981
|
-
invoke:
|
|
998
|
+
invoke: (event) => propsRef.current.onVisibleChanged?.(event)
|
|
982
999
|
},
|
|
983
1000
|
{
|
|
984
1001
|
eventName: "zIndex_changed",
|
|
985
|
-
invoke:
|
|
1002
|
+
invoke: (event) => propsRef.current.onZIndexChanged?.(event)
|
|
986
1003
|
}
|
|
987
1004
|
];
|
|
988
1005
|
}
|
|
@@ -995,8 +1012,31 @@ function bindMarkerEventListeners(marker, listenersRef, bindings) {
|
|
|
995
1012
|
binding.invoke?.(event);
|
|
996
1013
|
}));
|
|
997
1014
|
}
|
|
1015
|
+
function toLatLngLiteral(position) {
|
|
1016
|
+
if (!position) return null;
|
|
1017
|
+
if (typeof position === "object" && "lat" in position && "lng" in position) {
|
|
1018
|
+
if (typeof position.lat === "number" && typeof position.lng === "number") return {
|
|
1019
|
+
lat: position.lat,
|
|
1020
|
+
lng: position.lng
|
|
1021
|
+
};
|
|
1022
|
+
if (typeof position.lat === "function" && typeof position.lng === "function") return {
|
|
1023
|
+
lat: position.lat(),
|
|
1024
|
+
lng: position.lng()
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof position === "object" && "x" in position && "y" in position) {
|
|
1028
|
+
const p = position;
|
|
1029
|
+
if (typeof p.x === "number" && typeof p.y === "number") return {
|
|
1030
|
+
lat: p.y,
|
|
1031
|
+
lng: p.x
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
998
1036
|
const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
999
1037
|
const { map: contextMap, sdkStatus } = useNaverMap();
|
|
1038
|
+
const clustererRegistry = (0, react.useContext)(ClustererContext);
|
|
1039
|
+
const isInsideClusterer = clustererRegistry !== null && clustererRegistry.enabled;
|
|
1000
1040
|
const markerRef = (0, react.useRef)(null);
|
|
1001
1041
|
const markerEventListenersRef = (0, react.useRef)([]);
|
|
1002
1042
|
const onMarkerDestroyRef = (0, react.useRef)(props.onMarkerDestroy);
|
|
@@ -1004,14 +1044,43 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
|
1004
1044
|
const [portalReady, setPortalReady] = (0, react.useState)(false);
|
|
1005
1045
|
const hasChildren = props.children !== void 0 && props.children !== null;
|
|
1006
1046
|
const targetMap = props.map ?? contextMap;
|
|
1047
|
+
const propsRef = (0, react.useRef)(props);
|
|
1007
1048
|
(0, react.useEffect)(() => {
|
|
1049
|
+
propsRef.current = props;
|
|
1050
|
+
});
|
|
1051
|
+
(0, react.useEffect)(() => {
|
|
1052
|
+
if (!clustererRegistry) return;
|
|
1053
|
+
const id = props.clustererItemId;
|
|
1054
|
+
if (id === void 0 || id === null) return;
|
|
1055
|
+
const latLng = toLatLngLiteral(props.position);
|
|
1056
|
+
if (!latLng) return;
|
|
1057
|
+
clustererRegistry.register({
|
|
1058
|
+
id,
|
|
1059
|
+
position: latLng,
|
|
1060
|
+
data: props.item ?? null,
|
|
1061
|
+
markerOptions: props.icon !== void 0 ? { icon: props.icon } : void 0
|
|
1062
|
+
});
|
|
1063
|
+
return () => {
|
|
1064
|
+
clustererRegistry.unregister(id);
|
|
1065
|
+
};
|
|
1066
|
+
}, [
|
|
1067
|
+
clustererRegistry,
|
|
1068
|
+
props.clustererItemId,
|
|
1069
|
+
props.position,
|
|
1070
|
+
props.item,
|
|
1071
|
+
props.icon
|
|
1072
|
+
]);
|
|
1073
|
+
(0, react.useEffect)(() => {
|
|
1074
|
+
if (isInsideClusterer) return;
|
|
1008
1075
|
onMarkerDestroyRef.current = props.onMarkerDestroy;
|
|
1009
|
-
}, [props.onMarkerDestroy]);
|
|
1076
|
+
}, [isInsideClusterer, props.onMarkerDestroy]);
|
|
1010
1077
|
(0, react.useEffect)(() => {
|
|
1078
|
+
if (isInsideClusterer) return;
|
|
1011
1079
|
if (typeof document === "undefined") return;
|
|
1012
1080
|
setMarkerDiv(document.createElement("div"));
|
|
1013
|
-
}, []);
|
|
1081
|
+
}, [isInsideClusterer]);
|
|
1014
1082
|
(0, react.useEffect)(() => {
|
|
1083
|
+
if (isInsideClusterer) return;
|
|
1015
1084
|
if (!hasChildren || !markerDiv) {
|
|
1016
1085
|
setPortalReady(false);
|
|
1017
1086
|
return;
|
|
@@ -1028,7 +1097,11 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
|
1028
1097
|
return () => {
|
|
1029
1098
|
observer.disconnect();
|
|
1030
1099
|
};
|
|
1031
|
-
}, [
|
|
1100
|
+
}, [
|
|
1101
|
+
isInsideClusterer,
|
|
1102
|
+
hasChildren,
|
|
1103
|
+
markerDiv
|
|
1104
|
+
]);
|
|
1032
1105
|
const invokeMarkerMethod = (0, react.useCallback)((methodName, ...args) => {
|
|
1033
1106
|
const marker = markerRef.current;
|
|
1034
1107
|
if (!marker) return;
|
|
@@ -1084,68 +1157,1332 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
|
1084
1157
|
setZIndex: (...args) => invokeMarkerMethod("setZIndex", ...args)
|
|
1085
1158
|
}), [invokeMarkerMethod]);
|
|
1086
1159
|
(0, react.useEffect)(() => {
|
|
1160
|
+
if (isInsideClusterer) return;
|
|
1087
1161
|
if (sdkStatus !== "ready" || !targetMap || markerRef.current) return;
|
|
1088
1162
|
if (hasChildren && !portalReady) return;
|
|
1089
1163
|
try {
|
|
1090
|
-
const resolvedIcon = resolveMarkerIcon(
|
|
1091
|
-
const marker = new naver.maps.Marker(toMarkerOptions(
|
|
1164
|
+
const resolvedIcon = resolveMarkerIcon(propsRef.current.icon, markerDiv, hasChildren);
|
|
1165
|
+
const marker = new naver.maps.Marker(toMarkerOptions(propsRef.current, targetMap, resolvedIcon));
|
|
1092
1166
|
markerRef.current = marker;
|
|
1093
|
-
if (
|
|
1094
|
-
if (
|
|
1095
|
-
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(
|
|
1096
|
-
|
|
1167
|
+
if (propsRef.current.collisionBehavior !== void 0) marker.setOptions("collisionBehavior", propsRef.current.collisionBehavior);
|
|
1168
|
+
if (propsRef.current.collisionBoxSize !== void 0) marker.setOptions("collisionBoxSize", propsRef.current.collisionBoxSize);
|
|
1169
|
+
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(propsRef));
|
|
1170
|
+
propsRef.current.onMarkerReady?.(marker);
|
|
1097
1171
|
} catch (error) {
|
|
1098
1172
|
const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create naver.maps.Marker instance.");
|
|
1099
|
-
|
|
1173
|
+
propsRef.current.onMarkerError?.(normalizedError);
|
|
1100
1174
|
}
|
|
1101
1175
|
}, [
|
|
1176
|
+
isInsideClusterer,
|
|
1102
1177
|
hasChildren,
|
|
1103
1178
|
markerDiv,
|
|
1104
1179
|
portalReady,
|
|
1105
|
-
props,
|
|
1106
1180
|
sdkStatus,
|
|
1107
1181
|
targetMap
|
|
1108
1182
|
]);
|
|
1109
1183
|
(0, react.useLayoutEffect)(() => {
|
|
1184
|
+
if (isInsideClusterer) return;
|
|
1110
1185
|
const marker = markerRef.current;
|
|
1111
1186
|
if (!marker || !targetMap) return;
|
|
1112
1187
|
const rafId = requestAnimationFrame(() => {
|
|
1113
|
-
const
|
|
1188
|
+
const currentProps = propsRef.current;
|
|
1189
|
+
const nextOptions = toMarkerOptions(currentProps, targetMap, hasChildren && portalReady ? resolveMarkerIcon(currentProps.icon, markerDiv, hasChildren) : currentProps.icon);
|
|
1114
1190
|
marker.setOptions(nextOptions);
|
|
1115
|
-
if (
|
|
1116
|
-
if (
|
|
1191
|
+
if (currentProps.collisionBehavior !== void 0) marker.setOptions("collisionBehavior", currentProps.collisionBehavior);
|
|
1192
|
+
if (currentProps.collisionBoxSize !== void 0) marker.setOptions("collisionBoxSize", currentProps.collisionBoxSize);
|
|
1117
1193
|
});
|
|
1118
1194
|
return () => {
|
|
1119
1195
|
cancelAnimationFrame(rafId);
|
|
1120
1196
|
};
|
|
1121
1197
|
}, [
|
|
1198
|
+
isInsideClusterer,
|
|
1122
1199
|
hasChildren,
|
|
1123
1200
|
markerDiv,
|
|
1124
1201
|
portalReady,
|
|
1125
|
-
|
|
1126
|
-
|
|
1202
|
+
targetMap,
|
|
1203
|
+
props.position,
|
|
1204
|
+
props.icon,
|
|
1205
|
+
props.animation,
|
|
1206
|
+
props.shape,
|
|
1207
|
+
props.title,
|
|
1208
|
+
props.cursor,
|
|
1209
|
+
props.clickable,
|
|
1210
|
+
props.draggable,
|
|
1211
|
+
props.visible,
|
|
1212
|
+
props.zIndex,
|
|
1213
|
+
props.collisionBehavior,
|
|
1214
|
+
props.collisionBoxSize
|
|
1127
1215
|
]);
|
|
1128
1216
|
(0, react.useEffect)(() => {
|
|
1217
|
+
if (isInsideClusterer) return;
|
|
1129
1218
|
const marker = markerRef.current;
|
|
1130
1219
|
if (!marker) return;
|
|
1131
|
-
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(
|
|
1220
|
+
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(propsRef));
|
|
1132
1221
|
return () => {
|
|
1133
1222
|
if (markerEventListenersRef.current.length > 0) {
|
|
1134
1223
|
naver.maps.Event.removeListener(markerEventListenersRef.current);
|
|
1135
1224
|
markerEventListenersRef.current = [];
|
|
1136
1225
|
}
|
|
1137
1226
|
};
|
|
1138
|
-
}, [
|
|
1227
|
+
}, [isInsideClusterer]);
|
|
1139
1228
|
(0, react.useEffect)(() => {
|
|
1229
|
+
if (isInsideClusterer) return;
|
|
1140
1230
|
return () => {
|
|
1141
1231
|
teardownMarker();
|
|
1142
1232
|
};
|
|
1143
|
-
}, [teardownMarker]);
|
|
1233
|
+
}, [isInsideClusterer, teardownMarker]);
|
|
1234
|
+
if (isInsideClusterer) return null;
|
|
1144
1235
|
if (!hasChildren || !markerDiv) return null;
|
|
1145
1236
|
return (0, react_dom.createPortal)(props.children, markerDiv);
|
|
1146
1237
|
});
|
|
1147
1238
|
Marker.displayName = "Marker";
|
|
1148
1239
|
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region src/overlays/marker-clusterer/algorithms/grid.ts
|
|
1242
|
+
const DEFAULT_OPTIONS$2 = {
|
|
1243
|
+
gridSize: 60,
|
|
1244
|
+
minClusterSize: 2,
|
|
1245
|
+
maxZoom: 21
|
|
1246
|
+
};
|
|
1247
|
+
var GridAlgorithm = class {
|
|
1248
|
+
options;
|
|
1249
|
+
constructor(options) {
|
|
1250
|
+
this.options = {
|
|
1251
|
+
...DEFAULT_OPTIONS$2,
|
|
1252
|
+
...options
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
cluster(items, ctx) {
|
|
1256
|
+
if (ctx.zoom >= this.options.maxZoom) return {
|
|
1257
|
+
clusters: [],
|
|
1258
|
+
points: items
|
|
1259
|
+
};
|
|
1260
|
+
const { gridSize } = this.options;
|
|
1261
|
+
const scale = Math.pow(2, ctx.zoom);
|
|
1262
|
+
const cellMap = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const item of items) {
|
|
1264
|
+
const worldX = (item.position.lng + 180) / 360 * scale;
|
|
1265
|
+
const latRad = item.position.lat * Math.PI / 180;
|
|
1266
|
+
const worldY = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * scale;
|
|
1267
|
+
const key = `${Math.floor(worldX * 256 / gridSize)}:${Math.floor(worldY * 256 / gridSize)}`;
|
|
1268
|
+
let cell = cellMap.get(key);
|
|
1269
|
+
if (!cell) {
|
|
1270
|
+
cell = [];
|
|
1271
|
+
cellMap.set(key, cell);
|
|
1272
|
+
}
|
|
1273
|
+
cell.push(item);
|
|
1274
|
+
}
|
|
1275
|
+
const clusters = [];
|
|
1276
|
+
const points = [];
|
|
1277
|
+
for (const [key, cellItems] of cellMap) {
|
|
1278
|
+
if (cellItems.length < this.options.minClusterSize) {
|
|
1279
|
+
points.push(...cellItems);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
let sumLat = 0;
|
|
1283
|
+
let sumLng = 0;
|
|
1284
|
+
let minLat = Infinity;
|
|
1285
|
+
let maxLat = -Infinity;
|
|
1286
|
+
let minLng = Infinity;
|
|
1287
|
+
let maxLng = -Infinity;
|
|
1288
|
+
for (const item of cellItems) {
|
|
1289
|
+
sumLat += item.position.lat;
|
|
1290
|
+
sumLng += item.position.lng;
|
|
1291
|
+
minLat = Math.min(minLat, item.position.lat);
|
|
1292
|
+
maxLat = Math.max(maxLat, item.position.lat);
|
|
1293
|
+
minLng = Math.min(minLng, item.position.lng);
|
|
1294
|
+
maxLng = Math.max(maxLng, item.position.lng);
|
|
1295
|
+
}
|
|
1296
|
+
clusters.push({
|
|
1297
|
+
id: `grid-${key}`,
|
|
1298
|
+
position: {
|
|
1299
|
+
lat: sumLat / cellItems.length,
|
|
1300
|
+
lng: sumLng / cellItems.length
|
|
1301
|
+
},
|
|
1302
|
+
count: cellItems.length,
|
|
1303
|
+
bounds: {
|
|
1304
|
+
south: minLat,
|
|
1305
|
+
north: maxLat,
|
|
1306
|
+
west: minLng,
|
|
1307
|
+
east: maxLng
|
|
1308
|
+
},
|
|
1309
|
+
items: cellItems
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return {
|
|
1313
|
+
clusters,
|
|
1314
|
+
points
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
setOptions(options) {
|
|
1318
|
+
if (typeof options === "object" && options !== null) this.options = {
|
|
1319
|
+
...this.options,
|
|
1320
|
+
...options
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/overlays/marker-clusterer/algorithms/radius.ts
|
|
1327
|
+
const DEFAULT_OPTIONS$1 = {
|
|
1328
|
+
radius: 60,
|
|
1329
|
+
minClusterSize: 2,
|
|
1330
|
+
maxZoom: 21
|
|
1331
|
+
};
|
|
1332
|
+
function toWorldPixel(lat, lng, zoom) {
|
|
1333
|
+
const scale = Math.pow(2, zoom) * 256;
|
|
1334
|
+
const x = (lng + 180) / 360 * scale;
|
|
1335
|
+
const latRad = lat * Math.PI / 180;
|
|
1336
|
+
return {
|
|
1337
|
+
x,
|
|
1338
|
+
y: (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * scale
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function pixelDistance(a, b) {
|
|
1342
|
+
const dx = a.x - b.x;
|
|
1343
|
+
const dy = a.y - b.y;
|
|
1344
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
1345
|
+
}
|
|
1346
|
+
var RadiusAlgorithm = class {
|
|
1347
|
+
options;
|
|
1348
|
+
constructor(options) {
|
|
1349
|
+
this.options = {
|
|
1350
|
+
...DEFAULT_OPTIONS$1,
|
|
1351
|
+
...options
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
cluster(items, ctx) {
|
|
1355
|
+
if (ctx.zoom >= this.options.maxZoom) return {
|
|
1356
|
+
clusters: [],
|
|
1357
|
+
points: items
|
|
1358
|
+
};
|
|
1359
|
+
const { radius } = this.options;
|
|
1360
|
+
const used = /* @__PURE__ */ new Set();
|
|
1361
|
+
const clusters = [];
|
|
1362
|
+
const points = [];
|
|
1363
|
+
const pixelPositions = items.map((item) => toWorldPixel(item.position.lat, item.position.lng, ctx.zoom));
|
|
1364
|
+
for (let i = 0; i < items.length; i++) {
|
|
1365
|
+
if (used.has(items[i].id)) continue;
|
|
1366
|
+
const center = pixelPositions[i];
|
|
1367
|
+
const group = [items[i]];
|
|
1368
|
+
used.add(items[i].id);
|
|
1369
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
1370
|
+
if (used.has(items[j].id)) continue;
|
|
1371
|
+
if (pixelDistance(center, pixelPositions[j]) <= radius) {
|
|
1372
|
+
group.push(items[j]);
|
|
1373
|
+
used.add(items[j].id);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (group.length < this.options.minClusterSize) {
|
|
1377
|
+
points.push(...group);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
let sumLat = 0;
|
|
1381
|
+
let sumLng = 0;
|
|
1382
|
+
let minLat = Infinity;
|
|
1383
|
+
let maxLat = -Infinity;
|
|
1384
|
+
let minLng = Infinity;
|
|
1385
|
+
let maxLng = -Infinity;
|
|
1386
|
+
for (const item of group) {
|
|
1387
|
+
sumLat += item.position.lat;
|
|
1388
|
+
sumLng += item.position.lng;
|
|
1389
|
+
minLat = Math.min(minLat, item.position.lat);
|
|
1390
|
+
maxLat = Math.max(maxLat, item.position.lat);
|
|
1391
|
+
minLng = Math.min(minLng, item.position.lng);
|
|
1392
|
+
maxLng = Math.max(maxLng, item.position.lng);
|
|
1393
|
+
}
|
|
1394
|
+
const avgLat = sumLat / group.length;
|
|
1395
|
+
const avgLng = sumLng / group.length;
|
|
1396
|
+
clusters.push({
|
|
1397
|
+
id: `radius-${avgLat.toFixed(6)}-${avgLng.toFixed(6)}-${group.length}`,
|
|
1398
|
+
position: {
|
|
1399
|
+
lat: avgLat,
|
|
1400
|
+
lng: avgLng
|
|
1401
|
+
},
|
|
1402
|
+
count: group.length,
|
|
1403
|
+
bounds: {
|
|
1404
|
+
south: minLat,
|
|
1405
|
+
north: maxLat,
|
|
1406
|
+
west: minLng,
|
|
1407
|
+
east: maxLng
|
|
1408
|
+
},
|
|
1409
|
+
items: group
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
clusters,
|
|
1414
|
+
points
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
setOptions(options) {
|
|
1418
|
+
if (typeof options === "object" && options !== null) this.options = {
|
|
1419
|
+
...this.options,
|
|
1420
|
+
...options
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
//#endregion
|
|
1426
|
+
//#region ../../node_modules/.pnpm/kdbush@4.0.2/node_modules/kdbush/index.js
|
|
1427
|
+
const ARRAY_TYPES = [
|
|
1428
|
+
Int8Array,
|
|
1429
|
+
Uint8Array,
|
|
1430
|
+
Uint8ClampedArray,
|
|
1431
|
+
Int16Array,
|
|
1432
|
+
Uint16Array,
|
|
1433
|
+
Int32Array,
|
|
1434
|
+
Uint32Array,
|
|
1435
|
+
Float32Array,
|
|
1436
|
+
Float64Array
|
|
1437
|
+
];
|
|
1438
|
+
/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
|
|
1439
|
+
const VERSION = 1;
|
|
1440
|
+
const HEADER_SIZE = 8;
|
|
1441
|
+
var KDBush = class KDBush {
|
|
1442
|
+
/**
|
|
1443
|
+
* Creates an index from raw `ArrayBuffer` data.
|
|
1444
|
+
* @param {ArrayBuffer} data
|
|
1445
|
+
*/
|
|
1446
|
+
static from(data) {
|
|
1447
|
+
if (!(data instanceof ArrayBuffer)) throw new Error("Data must be an instance of ArrayBuffer.");
|
|
1448
|
+
const [magic, versionAndType] = new Uint8Array(data, 0, 2);
|
|
1449
|
+
if (magic !== 219) throw new Error("Data does not appear to be in a KDBush format.");
|
|
1450
|
+
const version = versionAndType >> 4;
|
|
1451
|
+
if (version !== VERSION) throw new Error(`Got v${version} data when expected v${VERSION}.`);
|
|
1452
|
+
const ArrayType = ARRAY_TYPES[versionAndType & 15];
|
|
1453
|
+
if (!ArrayType) throw new Error("Unrecognized array type.");
|
|
1454
|
+
const [nodeSize] = new Uint16Array(data, 2, 1);
|
|
1455
|
+
const [numItems] = new Uint32Array(data, 4, 1);
|
|
1456
|
+
return new KDBush(numItems, nodeSize, ArrayType, data);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Creates an index that will hold a given number of items.
|
|
1460
|
+
* @param {number} numItems
|
|
1461
|
+
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
|
|
1462
|
+
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
|
|
1463
|
+
* @param {ArrayBuffer} [data] (For internal use only)
|
|
1464
|
+
*/
|
|
1465
|
+
constructor(numItems, nodeSize = 64, ArrayType = Float64Array, data) {
|
|
1466
|
+
if (isNaN(numItems) || numItems < 0) throw new Error(`Unpexpected numItems value: ${numItems}.`);
|
|
1467
|
+
this.numItems = +numItems;
|
|
1468
|
+
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
|
|
1469
|
+
this.ArrayType = ArrayType;
|
|
1470
|
+
this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
|
|
1471
|
+
const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
|
|
1472
|
+
const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
|
|
1473
|
+
const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
|
|
1474
|
+
const padCoords = (8 - idsByteSize % 8) % 8;
|
|
1475
|
+
if (arrayTypeIndex < 0) throw new Error(`Unexpected typed array class: ${ArrayType}.`);
|
|
1476
|
+
if (data && data instanceof ArrayBuffer) {
|
|
1477
|
+
this.data = data;
|
|
1478
|
+
this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
|
|
1479
|
+
this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
|
|
1480
|
+
this._pos = numItems * 2;
|
|
1481
|
+
this._finished = true;
|
|
1482
|
+
} else {
|
|
1483
|
+
this.data = new ArrayBuffer(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
|
|
1484
|
+
this.ids = new this.IndexArrayType(this.data, HEADER_SIZE, numItems);
|
|
1485
|
+
this.coords = new this.ArrayType(this.data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
|
|
1486
|
+
this._pos = 0;
|
|
1487
|
+
this._finished = false;
|
|
1488
|
+
new Uint8Array(this.data, 0, 2).set([219, (VERSION << 4) + arrayTypeIndex]);
|
|
1489
|
+
new Uint16Array(this.data, 2, 1)[0] = nodeSize;
|
|
1490
|
+
new Uint32Array(this.data, 4, 1)[0] = numItems;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Add a point to the index.
|
|
1495
|
+
* @param {number} x
|
|
1496
|
+
* @param {number} y
|
|
1497
|
+
* @returns {number} An incremental index associated with the added item (starting from `0`).
|
|
1498
|
+
*/
|
|
1499
|
+
add(x, y) {
|
|
1500
|
+
const index = this._pos >> 1;
|
|
1501
|
+
this.ids[index] = index;
|
|
1502
|
+
this.coords[this._pos++] = x;
|
|
1503
|
+
this.coords[this._pos++] = y;
|
|
1504
|
+
return index;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Perform indexing of the added points.
|
|
1508
|
+
*/
|
|
1509
|
+
finish() {
|
|
1510
|
+
const numAdded = this._pos >> 1;
|
|
1511
|
+
if (numAdded !== this.numItems) throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
|
|
1512
|
+
sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);
|
|
1513
|
+
this._finished = true;
|
|
1514
|
+
return this;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Search the index for items within a given bounding box.
|
|
1518
|
+
* @param {number} minX
|
|
1519
|
+
* @param {number} minY
|
|
1520
|
+
* @param {number} maxX
|
|
1521
|
+
* @param {number} maxY
|
|
1522
|
+
* @returns {number[]} An array of indices correponding to the found items.
|
|
1523
|
+
*/
|
|
1524
|
+
range(minX, minY, maxX, maxY) {
|
|
1525
|
+
if (!this._finished) throw new Error("Data not yet indexed - call index.finish().");
|
|
1526
|
+
const { ids, coords, nodeSize } = this;
|
|
1527
|
+
const stack = [
|
|
1528
|
+
0,
|
|
1529
|
+
ids.length - 1,
|
|
1530
|
+
0
|
|
1531
|
+
];
|
|
1532
|
+
const result = [];
|
|
1533
|
+
while (stack.length) {
|
|
1534
|
+
const axis = stack.pop() || 0;
|
|
1535
|
+
const right = stack.pop() || 0;
|
|
1536
|
+
const left = stack.pop() || 0;
|
|
1537
|
+
if (right - left <= nodeSize) {
|
|
1538
|
+
for (let i = left; i <= right; i++) {
|
|
1539
|
+
const x = coords[2 * i];
|
|
1540
|
+
const y = coords[2 * i + 1];
|
|
1541
|
+
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]);
|
|
1542
|
+
}
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const m = left + right >> 1;
|
|
1546
|
+
const x = coords[2 * m];
|
|
1547
|
+
const y = coords[2 * m + 1];
|
|
1548
|
+
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);
|
|
1549
|
+
if (axis === 0 ? minX <= x : minY <= y) {
|
|
1550
|
+
stack.push(left);
|
|
1551
|
+
stack.push(m - 1);
|
|
1552
|
+
stack.push(1 - axis);
|
|
1553
|
+
}
|
|
1554
|
+
if (axis === 0 ? maxX >= x : maxY >= y) {
|
|
1555
|
+
stack.push(m + 1);
|
|
1556
|
+
stack.push(right);
|
|
1557
|
+
stack.push(1 - axis);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return result;
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Search the index for items within a given radius.
|
|
1564
|
+
* @param {number} qx
|
|
1565
|
+
* @param {number} qy
|
|
1566
|
+
* @param {number} r Query radius.
|
|
1567
|
+
* @returns {number[]} An array of indices correponding to the found items.
|
|
1568
|
+
*/
|
|
1569
|
+
within(qx, qy, r) {
|
|
1570
|
+
if (!this._finished) throw new Error("Data not yet indexed - call index.finish().");
|
|
1571
|
+
const { ids, coords, nodeSize } = this;
|
|
1572
|
+
const stack = [
|
|
1573
|
+
0,
|
|
1574
|
+
ids.length - 1,
|
|
1575
|
+
0
|
|
1576
|
+
];
|
|
1577
|
+
const result = [];
|
|
1578
|
+
const r2 = r * r;
|
|
1579
|
+
while (stack.length) {
|
|
1580
|
+
const axis = stack.pop() || 0;
|
|
1581
|
+
const right = stack.pop() || 0;
|
|
1582
|
+
const left = stack.pop() || 0;
|
|
1583
|
+
if (right - left <= nodeSize) {
|
|
1584
|
+
for (let i = left; i <= right; i++) if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) result.push(ids[i]);
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const m = left + right >> 1;
|
|
1588
|
+
const x = coords[2 * m];
|
|
1589
|
+
const y = coords[2 * m + 1];
|
|
1590
|
+
if (sqDist(x, y, qx, qy) <= r2) result.push(ids[m]);
|
|
1591
|
+
if (axis === 0 ? qx - r <= x : qy - r <= y) {
|
|
1592
|
+
stack.push(left);
|
|
1593
|
+
stack.push(m - 1);
|
|
1594
|
+
stack.push(1 - axis);
|
|
1595
|
+
}
|
|
1596
|
+
if (axis === 0 ? qx + r >= x : qy + r >= y) {
|
|
1597
|
+
stack.push(m + 1);
|
|
1598
|
+
stack.push(right);
|
|
1599
|
+
stack.push(1 - axis);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return result;
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
/**
|
|
1606
|
+
* @param {Uint16Array | Uint32Array} ids
|
|
1607
|
+
* @param {InstanceType<TypedArrayConstructor>} coords
|
|
1608
|
+
* @param {number} nodeSize
|
|
1609
|
+
* @param {number} left
|
|
1610
|
+
* @param {number} right
|
|
1611
|
+
* @param {number} axis
|
|
1612
|
+
*/
|
|
1613
|
+
function sort(ids, coords, nodeSize, left, right, axis) {
|
|
1614
|
+
if (right - left <= nodeSize) return;
|
|
1615
|
+
const m = left + right >> 1;
|
|
1616
|
+
select(ids, coords, m, left, right, axis);
|
|
1617
|
+
sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
|
|
1618
|
+
sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Custom Floyd-Rivest selection algorithm: sort ids and coords so that
|
|
1622
|
+
* [left..k-1] items are smaller than k-th item (on either x or y axis)
|
|
1623
|
+
* @param {Uint16Array | Uint32Array} ids
|
|
1624
|
+
* @param {InstanceType<TypedArrayConstructor>} coords
|
|
1625
|
+
* @param {number} k
|
|
1626
|
+
* @param {number} left
|
|
1627
|
+
* @param {number} right
|
|
1628
|
+
* @param {number} axis
|
|
1629
|
+
*/
|
|
1630
|
+
function select(ids, coords, k, left, right, axis) {
|
|
1631
|
+
while (right > left) {
|
|
1632
|
+
if (right - left > 600) {
|
|
1633
|
+
const n = right - left + 1;
|
|
1634
|
+
const m = k - left + 1;
|
|
1635
|
+
const z = Math.log(n);
|
|
1636
|
+
const s = .5 * Math.exp(2 * z / 3);
|
|
1637
|
+
const sd = .5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
|
|
1638
|
+
select(ids, coords, k, Math.max(left, Math.floor(k - m * s / n + sd)), Math.min(right, Math.floor(k + (n - m) * s / n + sd)), axis);
|
|
1639
|
+
}
|
|
1640
|
+
const t = coords[2 * k + axis];
|
|
1641
|
+
let i = left;
|
|
1642
|
+
let j = right;
|
|
1643
|
+
swapItem(ids, coords, left, k);
|
|
1644
|
+
if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);
|
|
1645
|
+
while (i < j) {
|
|
1646
|
+
swapItem(ids, coords, i, j);
|
|
1647
|
+
i++;
|
|
1648
|
+
j--;
|
|
1649
|
+
while (coords[2 * i + axis] < t) i++;
|
|
1650
|
+
while (coords[2 * j + axis] > t) j--;
|
|
1651
|
+
}
|
|
1652
|
+
if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
|
|
1653
|
+
else {
|
|
1654
|
+
j++;
|
|
1655
|
+
swapItem(ids, coords, j, right);
|
|
1656
|
+
}
|
|
1657
|
+
if (j <= k) left = j + 1;
|
|
1658
|
+
if (k <= j) right = j - 1;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* @param {Uint16Array | Uint32Array} ids
|
|
1663
|
+
* @param {InstanceType<TypedArrayConstructor>} coords
|
|
1664
|
+
* @param {number} i
|
|
1665
|
+
* @param {number} j
|
|
1666
|
+
*/
|
|
1667
|
+
function swapItem(ids, coords, i, j) {
|
|
1668
|
+
swap(ids, i, j);
|
|
1669
|
+
swap(coords, 2 * i, 2 * j);
|
|
1670
|
+
swap(coords, 2 * i + 1, 2 * j + 1);
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* @param {InstanceType<TypedArrayConstructor>} arr
|
|
1674
|
+
* @param {number} i
|
|
1675
|
+
* @param {number} j
|
|
1676
|
+
*/
|
|
1677
|
+
function swap(arr, i, j) {
|
|
1678
|
+
const tmp = arr[i];
|
|
1679
|
+
arr[i] = arr[j];
|
|
1680
|
+
arr[j] = tmp;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* @param {number} ax
|
|
1684
|
+
* @param {number} ay
|
|
1685
|
+
* @param {number} bx
|
|
1686
|
+
* @param {number} by
|
|
1687
|
+
*/
|
|
1688
|
+
function sqDist(ax, ay, bx, by) {
|
|
1689
|
+
const dx = ax - bx;
|
|
1690
|
+
const dy = ay - by;
|
|
1691
|
+
return dx * dx + dy * dy;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
//#endregion
|
|
1695
|
+
//#region ../../node_modules/.pnpm/supercluster@8.0.1/node_modules/supercluster/index.js
|
|
1696
|
+
const defaultOptions = {
|
|
1697
|
+
minZoom: 0,
|
|
1698
|
+
maxZoom: 16,
|
|
1699
|
+
minPoints: 2,
|
|
1700
|
+
radius: 40,
|
|
1701
|
+
extent: 512,
|
|
1702
|
+
nodeSize: 64,
|
|
1703
|
+
log: false,
|
|
1704
|
+
generateId: false,
|
|
1705
|
+
reduce: null,
|
|
1706
|
+
map: (props) => props
|
|
1707
|
+
};
|
|
1708
|
+
const fround = Math.fround || ((tmp) => ((x) => {
|
|
1709
|
+
tmp[0] = +x;
|
|
1710
|
+
return tmp[0];
|
|
1711
|
+
}))(new Float32Array(1));
|
|
1712
|
+
const OFFSET_ZOOM = 2;
|
|
1713
|
+
const OFFSET_ID = 3;
|
|
1714
|
+
const OFFSET_PARENT = 4;
|
|
1715
|
+
const OFFSET_NUM = 5;
|
|
1716
|
+
const OFFSET_PROP = 6;
|
|
1717
|
+
var Supercluster = class {
|
|
1718
|
+
constructor(options) {
|
|
1719
|
+
this.options = Object.assign(Object.create(defaultOptions), options);
|
|
1720
|
+
this.trees = new Array(this.options.maxZoom + 1);
|
|
1721
|
+
this.stride = this.options.reduce ? 7 : 6;
|
|
1722
|
+
this.clusterProps = [];
|
|
1723
|
+
}
|
|
1724
|
+
load(points) {
|
|
1725
|
+
const { log, minZoom, maxZoom } = this.options;
|
|
1726
|
+
if (log) console.time("total time");
|
|
1727
|
+
const timerId = `prepare ${points.length} points`;
|
|
1728
|
+
if (log) console.time(timerId);
|
|
1729
|
+
this.points = points;
|
|
1730
|
+
const data = [];
|
|
1731
|
+
for (let i = 0; i < points.length; i++) {
|
|
1732
|
+
const p = points[i];
|
|
1733
|
+
if (!p.geometry) continue;
|
|
1734
|
+
const [lng, lat] = p.geometry.coordinates;
|
|
1735
|
+
const x = fround(lngX(lng));
|
|
1736
|
+
const y = fround(latY(lat));
|
|
1737
|
+
data.push(x, y, Infinity, i, -1, 1);
|
|
1738
|
+
if (this.options.reduce) data.push(0);
|
|
1739
|
+
}
|
|
1740
|
+
let tree = this.trees[maxZoom + 1] = this._createTree(data);
|
|
1741
|
+
if (log) console.timeEnd(timerId);
|
|
1742
|
+
for (let z = maxZoom; z >= minZoom; z--) {
|
|
1743
|
+
const now = +Date.now();
|
|
1744
|
+
tree = this.trees[z] = this._createTree(this._cluster(tree, z));
|
|
1745
|
+
if (log) console.log("z%d: %d clusters in %dms", z, tree.numItems, +Date.now() - now);
|
|
1746
|
+
}
|
|
1747
|
+
if (log) console.timeEnd("total time");
|
|
1748
|
+
return this;
|
|
1749
|
+
}
|
|
1750
|
+
getClusters(bbox, zoom) {
|
|
1751
|
+
let minLng = ((bbox[0] + 180) % 360 + 360) % 360 - 180;
|
|
1752
|
+
const minLat = Math.max(-90, Math.min(90, bbox[1]));
|
|
1753
|
+
let maxLng = bbox[2] === 180 ? 180 : ((bbox[2] + 180) % 360 + 360) % 360 - 180;
|
|
1754
|
+
const maxLat = Math.max(-90, Math.min(90, bbox[3]));
|
|
1755
|
+
if (bbox[2] - bbox[0] >= 360) {
|
|
1756
|
+
minLng = -180;
|
|
1757
|
+
maxLng = 180;
|
|
1758
|
+
} else if (minLng > maxLng) {
|
|
1759
|
+
const easternHem = this.getClusters([
|
|
1760
|
+
minLng,
|
|
1761
|
+
minLat,
|
|
1762
|
+
180,
|
|
1763
|
+
maxLat
|
|
1764
|
+
], zoom);
|
|
1765
|
+
const westernHem = this.getClusters([
|
|
1766
|
+
-180,
|
|
1767
|
+
minLat,
|
|
1768
|
+
maxLng,
|
|
1769
|
+
maxLat
|
|
1770
|
+
], zoom);
|
|
1771
|
+
return easternHem.concat(westernHem);
|
|
1772
|
+
}
|
|
1773
|
+
const tree = this.trees[this._limitZoom(zoom)];
|
|
1774
|
+
const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
|
|
1775
|
+
const data = tree.data;
|
|
1776
|
+
const clusters = [];
|
|
1777
|
+
for (const id of ids) {
|
|
1778
|
+
const k = this.stride * id;
|
|
1779
|
+
clusters.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
1780
|
+
}
|
|
1781
|
+
return clusters;
|
|
1782
|
+
}
|
|
1783
|
+
getChildren(clusterId) {
|
|
1784
|
+
const originId = this._getOriginId(clusterId);
|
|
1785
|
+
const originZoom = this._getOriginZoom(clusterId);
|
|
1786
|
+
const errorMsg = "No cluster with the specified id.";
|
|
1787
|
+
const tree = this.trees[originZoom];
|
|
1788
|
+
if (!tree) throw new Error(errorMsg);
|
|
1789
|
+
const data = tree.data;
|
|
1790
|
+
if (originId * this.stride >= data.length) throw new Error(errorMsg);
|
|
1791
|
+
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
|
|
1792
|
+
const x = data[originId * this.stride];
|
|
1793
|
+
const y = data[originId * this.stride + 1];
|
|
1794
|
+
const ids = tree.within(x, y, r);
|
|
1795
|
+
const children = [];
|
|
1796
|
+
for (const id of ids) {
|
|
1797
|
+
const k = id * this.stride;
|
|
1798
|
+
if (data[k + OFFSET_PARENT] === clusterId) children.push(data[k + OFFSET_NUM] > 1 ? getClusterJSON(data, k, this.clusterProps) : this.points[data[k + OFFSET_ID]]);
|
|
1799
|
+
}
|
|
1800
|
+
if (children.length === 0) throw new Error(errorMsg);
|
|
1801
|
+
return children;
|
|
1802
|
+
}
|
|
1803
|
+
getLeaves(clusterId, limit, offset) {
|
|
1804
|
+
limit = limit || 10;
|
|
1805
|
+
offset = offset || 0;
|
|
1806
|
+
const leaves = [];
|
|
1807
|
+
this._appendLeaves(leaves, clusterId, limit, offset, 0);
|
|
1808
|
+
return leaves;
|
|
1809
|
+
}
|
|
1810
|
+
getTile(z, x, y) {
|
|
1811
|
+
const tree = this.trees[this._limitZoom(z)];
|
|
1812
|
+
const z2 = Math.pow(2, z);
|
|
1813
|
+
const { extent, radius } = this.options;
|
|
1814
|
+
const p = radius / extent;
|
|
1815
|
+
const top = (y - p) / z2;
|
|
1816
|
+
const bottom = (y + 1 + p) / z2;
|
|
1817
|
+
const tile = { features: [] };
|
|
1818
|
+
this._addTileFeatures(tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom), tree.data, x, y, z2, tile);
|
|
1819
|
+
if (x === 0) this._addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.data, z2, y, z2, tile);
|
|
1820
|
+
if (x === z2 - 1) this._addTileFeatures(tree.range(0, top, p / z2, bottom), tree.data, -1, y, z2, tile);
|
|
1821
|
+
return tile.features.length ? tile : null;
|
|
1822
|
+
}
|
|
1823
|
+
getClusterExpansionZoom(clusterId) {
|
|
1824
|
+
let expansionZoom = this._getOriginZoom(clusterId) - 1;
|
|
1825
|
+
while (expansionZoom <= this.options.maxZoom) {
|
|
1826
|
+
const children = this.getChildren(clusterId);
|
|
1827
|
+
expansionZoom++;
|
|
1828
|
+
if (children.length !== 1) break;
|
|
1829
|
+
clusterId = children[0].properties.cluster_id;
|
|
1830
|
+
}
|
|
1831
|
+
return expansionZoom;
|
|
1832
|
+
}
|
|
1833
|
+
_appendLeaves(result, clusterId, limit, offset, skipped) {
|
|
1834
|
+
const children = this.getChildren(clusterId);
|
|
1835
|
+
for (const child of children) {
|
|
1836
|
+
const props = child.properties;
|
|
1837
|
+
if (props && props.cluster) if (skipped + props.point_count <= offset) skipped += props.point_count;
|
|
1838
|
+
else skipped = this._appendLeaves(result, props.cluster_id, limit, offset, skipped);
|
|
1839
|
+
else if (skipped < offset) skipped++;
|
|
1840
|
+
else result.push(child);
|
|
1841
|
+
if (result.length === limit) break;
|
|
1842
|
+
}
|
|
1843
|
+
return skipped;
|
|
1844
|
+
}
|
|
1845
|
+
_createTree(data) {
|
|
1846
|
+
const tree = new KDBush(data.length / this.stride | 0, this.options.nodeSize, Float32Array);
|
|
1847
|
+
for (let i = 0; i < data.length; i += this.stride) tree.add(data[i], data[i + 1]);
|
|
1848
|
+
tree.finish();
|
|
1849
|
+
tree.data = data;
|
|
1850
|
+
return tree;
|
|
1851
|
+
}
|
|
1852
|
+
_addTileFeatures(ids, data, x, y, z2, tile) {
|
|
1853
|
+
for (const i of ids) {
|
|
1854
|
+
const k = i * this.stride;
|
|
1855
|
+
const isCluster = data[k + OFFSET_NUM] > 1;
|
|
1856
|
+
let tags, px, py;
|
|
1857
|
+
if (isCluster) {
|
|
1858
|
+
tags = getClusterProperties(data, k, this.clusterProps);
|
|
1859
|
+
px = data[k];
|
|
1860
|
+
py = data[k + 1];
|
|
1861
|
+
} else {
|
|
1862
|
+
const p = this.points[data[k + OFFSET_ID]];
|
|
1863
|
+
tags = p.properties;
|
|
1864
|
+
const [lng, lat] = p.geometry.coordinates;
|
|
1865
|
+
px = lngX(lng);
|
|
1866
|
+
py = latY(lat);
|
|
1867
|
+
}
|
|
1868
|
+
const f = {
|
|
1869
|
+
type: 1,
|
|
1870
|
+
geometry: [[Math.round(this.options.extent * (px * z2 - x)), Math.round(this.options.extent * (py * z2 - y))]],
|
|
1871
|
+
tags
|
|
1872
|
+
};
|
|
1873
|
+
let id;
|
|
1874
|
+
if (isCluster || this.options.generateId) id = data[k + OFFSET_ID];
|
|
1875
|
+
else id = this.points[data[k + OFFSET_ID]].id;
|
|
1876
|
+
if (id !== void 0) f.id = id;
|
|
1877
|
+
tile.features.push(f);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
_limitZoom(z) {
|
|
1881
|
+
return Math.max(this.options.minZoom, Math.min(Math.floor(+z), this.options.maxZoom + 1));
|
|
1882
|
+
}
|
|
1883
|
+
_cluster(tree, zoom) {
|
|
1884
|
+
const { radius, extent, reduce, minPoints } = this.options;
|
|
1885
|
+
const r = radius / (extent * Math.pow(2, zoom));
|
|
1886
|
+
const data = tree.data;
|
|
1887
|
+
const nextData = [];
|
|
1888
|
+
const stride = this.stride;
|
|
1889
|
+
for (let i = 0; i < data.length; i += stride) {
|
|
1890
|
+
if (data[i + OFFSET_ZOOM] <= zoom) continue;
|
|
1891
|
+
data[i + OFFSET_ZOOM] = zoom;
|
|
1892
|
+
const x = data[i];
|
|
1893
|
+
const y = data[i + 1];
|
|
1894
|
+
const neighborIds = tree.within(data[i], data[i + 1], r);
|
|
1895
|
+
const numPointsOrigin = data[i + OFFSET_NUM];
|
|
1896
|
+
let numPoints = numPointsOrigin;
|
|
1897
|
+
for (const neighborId of neighborIds) {
|
|
1898
|
+
const k = neighborId * stride;
|
|
1899
|
+
if (data[k + OFFSET_ZOOM] > zoom) numPoints += data[k + OFFSET_NUM];
|
|
1900
|
+
}
|
|
1901
|
+
if (numPoints > numPointsOrigin && numPoints >= minPoints) {
|
|
1902
|
+
let wx = x * numPointsOrigin;
|
|
1903
|
+
let wy = y * numPointsOrigin;
|
|
1904
|
+
let clusterProperties;
|
|
1905
|
+
let clusterPropIndex = -1;
|
|
1906
|
+
const id = ((i / stride | 0) << 5) + (zoom + 1) + this.points.length;
|
|
1907
|
+
for (const neighborId of neighborIds) {
|
|
1908
|
+
const k = neighborId * stride;
|
|
1909
|
+
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1910
|
+
data[k + OFFSET_ZOOM] = zoom;
|
|
1911
|
+
const numPoints2 = data[k + OFFSET_NUM];
|
|
1912
|
+
wx += data[k] * numPoints2;
|
|
1913
|
+
wy += data[k + 1] * numPoints2;
|
|
1914
|
+
data[k + OFFSET_PARENT] = id;
|
|
1915
|
+
if (reduce) {
|
|
1916
|
+
if (!clusterProperties) {
|
|
1917
|
+
clusterProperties = this._map(data, i, true);
|
|
1918
|
+
clusterPropIndex = this.clusterProps.length;
|
|
1919
|
+
this.clusterProps.push(clusterProperties);
|
|
1920
|
+
}
|
|
1921
|
+
reduce(clusterProperties, this._map(data, k));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
data[i + OFFSET_PARENT] = id;
|
|
1925
|
+
nextData.push(wx / numPoints, wy / numPoints, Infinity, id, -1, numPoints);
|
|
1926
|
+
if (reduce) nextData.push(clusterPropIndex);
|
|
1927
|
+
} else {
|
|
1928
|
+
for (let j = 0; j < stride; j++) nextData.push(data[i + j]);
|
|
1929
|
+
if (numPoints > 1) for (const neighborId of neighborIds) {
|
|
1930
|
+
const k = neighborId * stride;
|
|
1931
|
+
if (data[k + OFFSET_ZOOM] <= zoom) continue;
|
|
1932
|
+
data[k + OFFSET_ZOOM] = zoom;
|
|
1933
|
+
for (let j = 0; j < stride; j++) nextData.push(data[k + j]);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return nextData;
|
|
1938
|
+
}
|
|
1939
|
+
_getOriginId(clusterId) {
|
|
1940
|
+
return clusterId - this.points.length >> 5;
|
|
1941
|
+
}
|
|
1942
|
+
_getOriginZoom(clusterId) {
|
|
1943
|
+
return (clusterId - this.points.length) % 32;
|
|
1944
|
+
}
|
|
1945
|
+
_map(data, i, clone) {
|
|
1946
|
+
if (data[i + OFFSET_NUM] > 1) {
|
|
1947
|
+
const props = this.clusterProps[data[i + OFFSET_PROP]];
|
|
1948
|
+
return clone ? Object.assign({}, props) : props;
|
|
1949
|
+
}
|
|
1950
|
+
const original = this.points[data[i + OFFSET_ID]].properties;
|
|
1951
|
+
const result = this.options.map(original);
|
|
1952
|
+
return clone && result === original ? Object.assign({}, result) : result;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
function getClusterJSON(data, i, clusterProps) {
|
|
1956
|
+
return {
|
|
1957
|
+
type: "Feature",
|
|
1958
|
+
id: data[i + OFFSET_ID],
|
|
1959
|
+
properties: getClusterProperties(data, i, clusterProps),
|
|
1960
|
+
geometry: {
|
|
1961
|
+
type: "Point",
|
|
1962
|
+
coordinates: [xLng(data[i]), yLat(data[i + 1])]
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
function getClusterProperties(data, i, clusterProps) {
|
|
1967
|
+
const count = data[i + OFFSET_NUM];
|
|
1968
|
+
const abbrev = count >= 1e4 ? `${Math.round(count / 1e3)}k` : count >= 1e3 ? `${Math.round(count / 100) / 10}k` : count;
|
|
1969
|
+
const propIndex = data[i + OFFSET_PROP];
|
|
1970
|
+
const properties = propIndex === -1 ? {} : Object.assign({}, clusterProps[propIndex]);
|
|
1971
|
+
return Object.assign(properties, {
|
|
1972
|
+
cluster: true,
|
|
1973
|
+
cluster_id: data[i + OFFSET_ID],
|
|
1974
|
+
point_count: count,
|
|
1975
|
+
point_count_abbreviated: abbrev
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function lngX(lng) {
|
|
1979
|
+
return lng / 360 + .5;
|
|
1980
|
+
}
|
|
1981
|
+
function latY(lat) {
|
|
1982
|
+
const sin = Math.sin(lat * Math.PI / 180);
|
|
1983
|
+
const y = .5 - .25 * Math.log((1 + sin) / (1 - sin)) / Math.PI;
|
|
1984
|
+
return y < 0 ? 0 : y > 1 ? 1 : y;
|
|
1985
|
+
}
|
|
1986
|
+
function xLng(x) {
|
|
1987
|
+
return (x - .5) * 360;
|
|
1988
|
+
}
|
|
1989
|
+
function yLat(y) {
|
|
1990
|
+
const y2 = (180 - y * 360) * Math.PI / 180;
|
|
1991
|
+
return 360 * Math.atan(Math.exp(y2)) / Math.PI - 90;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
//#endregion
|
|
1995
|
+
//#region src/overlays/marker-clusterer/algorithms/supercluster.ts
|
|
1996
|
+
const DEFAULT_OPTIONS = {
|
|
1997
|
+
radius: 60,
|
|
1998
|
+
minZoom: 0,
|
|
1999
|
+
maxZoom: 16,
|
|
2000
|
+
extent: 512,
|
|
2001
|
+
nodeSize: 64
|
|
2002
|
+
};
|
|
2003
|
+
var SuperclusterAlgorithm = class {
|
|
2004
|
+
options;
|
|
2005
|
+
index;
|
|
2006
|
+
itemMap = /* @__PURE__ */ new Map();
|
|
2007
|
+
dirty = true;
|
|
2008
|
+
constructor(options) {
|
|
2009
|
+
this.options = {
|
|
2010
|
+
...DEFAULT_OPTIONS,
|
|
2011
|
+
...options
|
|
2012
|
+
};
|
|
2013
|
+
this.index = new Supercluster({
|
|
2014
|
+
radius: this.options.radius,
|
|
2015
|
+
minZoom: this.options.minZoom,
|
|
2016
|
+
maxZoom: this.options.maxZoom,
|
|
2017
|
+
extent: this.options.extent,
|
|
2018
|
+
nodeSize: this.options.nodeSize
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
cluster(items, ctx) {
|
|
2022
|
+
if (this.dirty || this.itemMap.size !== items.length) this.load(items);
|
|
2023
|
+
const { south, west, north, east } = ctx.bounds;
|
|
2024
|
+
const rawClusters = this.index.getClusters([
|
|
2025
|
+
west,
|
|
2026
|
+
south,
|
|
2027
|
+
east,
|
|
2028
|
+
north
|
|
2029
|
+
], Math.floor(ctx.zoom));
|
|
2030
|
+
const clusters = [];
|
|
2031
|
+
const singlePoints = [];
|
|
2032
|
+
for (const feature of rawClusters) {
|
|
2033
|
+
const [lng, lat] = feature.geometry.coordinates;
|
|
2034
|
+
const props = feature.properties;
|
|
2035
|
+
if ("cluster" in props && props.cluster === true) {
|
|
2036
|
+
const clusterProps = props;
|
|
2037
|
+
const clusterId = feature.id;
|
|
2038
|
+
const leaves = this.getLeaves(clusterId);
|
|
2039
|
+
let minLat = Infinity;
|
|
2040
|
+
let maxLat = -Infinity;
|
|
2041
|
+
let minLng = Infinity;
|
|
2042
|
+
let maxLng = -Infinity;
|
|
2043
|
+
for (const leaf of leaves) {
|
|
2044
|
+
minLat = Math.min(minLat, leaf.position.lat);
|
|
2045
|
+
maxLat = Math.max(maxLat, leaf.position.lat);
|
|
2046
|
+
minLng = Math.min(minLng, leaf.position.lng);
|
|
2047
|
+
maxLng = Math.max(maxLng, leaf.position.lng);
|
|
2048
|
+
}
|
|
2049
|
+
clusters.push({
|
|
2050
|
+
id: `sc-${String(clusterId)}`,
|
|
2051
|
+
position: {
|
|
2052
|
+
lat,
|
|
2053
|
+
lng
|
|
2054
|
+
},
|
|
2055
|
+
count: clusterProps.point_count,
|
|
2056
|
+
bounds: {
|
|
2057
|
+
south: minLat,
|
|
2058
|
+
north: maxLat,
|
|
2059
|
+
west: minLng,
|
|
2060
|
+
east: maxLng
|
|
2061
|
+
},
|
|
2062
|
+
items: leaves
|
|
2063
|
+
});
|
|
2064
|
+
} else {
|
|
2065
|
+
const pointProps = props;
|
|
2066
|
+
const item = this.itemMap.get(pointProps.itemId);
|
|
2067
|
+
if (item) singlePoints.push(item);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
clusters,
|
|
2072
|
+
points: singlePoints
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
setOptions(options) {
|
|
2076
|
+
if (typeof options === "object" && options !== null) {
|
|
2077
|
+
const next = {
|
|
2078
|
+
...this.options,
|
|
2079
|
+
...options
|
|
2080
|
+
};
|
|
2081
|
+
this.options = next;
|
|
2082
|
+
this.index = new Supercluster({
|
|
2083
|
+
radius: next.radius,
|
|
2084
|
+
minZoom: next.minZoom,
|
|
2085
|
+
maxZoom: next.maxZoom,
|
|
2086
|
+
extent: next.extent,
|
|
2087
|
+
nodeSize: next.nodeSize
|
|
2088
|
+
});
|
|
2089
|
+
this.dirty = true;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
destroy() {
|
|
2093
|
+
this.itemMap.clear();
|
|
2094
|
+
}
|
|
2095
|
+
load(items) {
|
|
2096
|
+
this.itemMap.clear();
|
|
2097
|
+
const features = [];
|
|
2098
|
+
for (const item of items) {
|
|
2099
|
+
this.itemMap.set(item.id, item);
|
|
2100
|
+
features.push({
|
|
2101
|
+
type: "Feature",
|
|
2102
|
+
geometry: {
|
|
2103
|
+
type: "Point",
|
|
2104
|
+
coordinates: [item.position.lng, item.position.lat]
|
|
2105
|
+
},
|
|
2106
|
+
properties: {
|
|
2107
|
+
itemId: item.id,
|
|
2108
|
+
data: item.data,
|
|
2109
|
+
markerOptions: item.markerOptions
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
this.index.load(features);
|
|
2114
|
+
this.dirty = false;
|
|
2115
|
+
}
|
|
2116
|
+
getLeaves(clusterId) {
|
|
2117
|
+
if (clusterId === void 0) return [];
|
|
2118
|
+
const numericId = typeof clusterId === "string" ? parseInt(clusterId, 10) : clusterId;
|
|
2119
|
+
if (typeof numericId !== "number" || isNaN(numericId)) return [];
|
|
2120
|
+
const leaves = this.index.getLeaves(numericId, Infinity);
|
|
2121
|
+
const result = [];
|
|
2122
|
+
for (const leaf of leaves) {
|
|
2123
|
+
const props = leaf.properties;
|
|
2124
|
+
const item = this.itemMap.get(props.itemId);
|
|
2125
|
+
if (item) result.push(item);
|
|
2126
|
+
}
|
|
2127
|
+
return result;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
//#endregion
|
|
2132
|
+
//#region src/overlays/marker-clusterer/algorithms/createAlgorithm.ts
|
|
2133
|
+
function isBuiltInConfig(value) {
|
|
2134
|
+
return "type" in value && typeof value.type === "string";
|
|
2135
|
+
}
|
|
2136
|
+
function omitUndefined(obj) {
|
|
2137
|
+
const result = {};
|
|
2138
|
+
for (const key of Object.keys(obj)) if (obj[key] !== void 0) result[key] = obj[key];
|
|
2139
|
+
return result;
|
|
2140
|
+
}
|
|
2141
|
+
function createAlgorithm(config) {
|
|
2142
|
+
switch (config.type) {
|
|
2143
|
+
case "grid": return new GridAlgorithm(omitUndefined({
|
|
2144
|
+
gridSize: config.gridSize,
|
|
2145
|
+
minClusterSize: config.minClusterSize,
|
|
2146
|
+
maxZoom: config.maxZoom
|
|
2147
|
+
}));
|
|
2148
|
+
case "radius": return new RadiusAlgorithm(omitUndefined({
|
|
2149
|
+
radius: config.radius,
|
|
2150
|
+
minClusterSize: config.minClusterSize,
|
|
2151
|
+
maxZoom: config.maxZoom
|
|
2152
|
+
}));
|
|
2153
|
+
case "supercluster": return new SuperclusterAlgorithm(omitUndefined({
|
|
2154
|
+
radius: config.radius,
|
|
2155
|
+
minZoom: config.minZoom,
|
|
2156
|
+
maxZoom: config.maxZoom,
|
|
2157
|
+
extent: config.extent,
|
|
2158
|
+
nodeSize: config.nodeSize
|
|
2159
|
+
}));
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
//#endregion
|
|
2164
|
+
//#region src/overlays/marker-clusterer/MarkerClusterer.tsx
|
|
2165
|
+
function DefaultClusterIcon({ count }) {
|
|
2166
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2167
|
+
style: {
|
|
2168
|
+
display: "flex",
|
|
2169
|
+
alignItems: "center",
|
|
2170
|
+
justifyContent: "center",
|
|
2171
|
+
width: 40,
|
|
2172
|
+
height: 40,
|
|
2173
|
+
borderRadius: "50%",
|
|
2174
|
+
backgroundColor: "rgba(0, 100, 255, 0.7)",
|
|
2175
|
+
color: "#fff",
|
|
2176
|
+
fontWeight: "bold",
|
|
2177
|
+
fontSize: 14,
|
|
2178
|
+
border: "2px solid rgba(0, 100, 255, 0.9)"
|
|
2179
|
+
},
|
|
2180
|
+
children: count
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
function getMapBounds(map) {
|
|
2184
|
+
const bounds = map.getBounds();
|
|
2185
|
+
if (bounds instanceof naver.maps.LatLngBounds) {
|
|
2186
|
+
const sw = bounds.getSW();
|
|
2187
|
+
const ne = bounds.getNE();
|
|
2188
|
+
return {
|
|
2189
|
+
south: sw.lat(),
|
|
2190
|
+
west: sw.lng(),
|
|
2191
|
+
north: ne.lat(),
|
|
2192
|
+
east: ne.lng()
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
const b = bounds;
|
|
2196
|
+
if ("south" in b && "north" in b && "west" in b && "east" in b) return {
|
|
2197
|
+
south: b.south,
|
|
2198
|
+
north: b.north,
|
|
2199
|
+
west: b.west,
|
|
2200
|
+
east: b.east
|
|
2201
|
+
};
|
|
2202
|
+
return {
|
|
2203
|
+
south: -90,
|
|
2204
|
+
north: 90,
|
|
2205
|
+
west: -180,
|
|
2206
|
+
east: 180
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
function padBounds(bounds, padding) {
|
|
2210
|
+
const latPad = (bounds.north - bounds.south) * padding * .01;
|
|
2211
|
+
const lngPad = (bounds.east - bounds.west) * padding * .01;
|
|
2212
|
+
return {
|
|
2213
|
+
south: bounds.south - latPad,
|
|
2214
|
+
north: bounds.north + latPad,
|
|
2215
|
+
west: bounds.west - lngPad,
|
|
2216
|
+
east: bounds.east + lngPad
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* 다수의 `<Marker>`를 자동으로 클러스터링해주는 컴포넌트.
|
|
2221
|
+
*
|
|
2222
|
+
* ## 동작 방식
|
|
2223
|
+
*
|
|
2224
|
+
* 1. `<Marker>`는 `<MarkerClusterer>` 내부에서 직접 마커를 생성하지 않고
|
|
2225
|
+
* 내부 registry에 위치·데이터를 등록합니다.
|
|
2226
|
+
* 2. `MarkerClusterer`는 지도 이벤트(`idle`, `move`, `zoom`) 발생 시
|
|
2227
|
+
* 현재 줌·뷰포트 기준으로 클러스터를 재계산합니다.
|
|
2228
|
+
* 3. 클러스터 마커는 `createRoot`를 이용해 React 컴포넌트를 HTML 아이콘으로 렌더링합니다.
|
|
2229
|
+
*
|
|
2230
|
+
* ## 기본 사용법
|
|
2231
|
+
*
|
|
2232
|
+
* ```tsx
|
|
2233
|
+
* <NaverMap ...>
|
|
2234
|
+
* <MarkerClusterer
|
|
2235
|
+
* algorithm={{ type: "supercluster", radius: 60 }}
|
|
2236
|
+
* clusterIcon={({ count }) => <ClusterBadge count={count} />}
|
|
2237
|
+
* onClusterClick={({ cluster, helpers }) =>
|
|
2238
|
+
* helpers.zoomToCluster(cluster, { maxZoom: 16 })
|
|
2239
|
+
* }
|
|
2240
|
+
* >
|
|
2241
|
+
* {points.map(p => (
|
|
2242
|
+
* <Marker
|
|
2243
|
+
* key={p.id}
|
|
2244
|
+
* clustererItemId={p.id}
|
|
2245
|
+
* position={p.position}
|
|
2246
|
+
* item={p}
|
|
2247
|
+
* />
|
|
2248
|
+
* ))}
|
|
2249
|
+
* </MarkerClusterer>
|
|
2250
|
+
* </NaverMap>
|
|
2251
|
+
* ```
|
|
2252
|
+
*
|
|
2253
|
+
* ## 주의 사항
|
|
2254
|
+
*
|
|
2255
|
+
* - `<Marker>`에 반드시 `clustererItemId` prop을 지정해야 합니다.
|
|
2256
|
+
* - `<MarkerClusterer>`는 반드시 `<NaverMap>` 내부에 위치해야 합니다.
|
|
2257
|
+
* - `enabled={false}`로 설정하면 클러스터링이 해제되고 각 `<Marker>`가 개별 마커로 렌더링됩니다.
|
|
2258
|
+
*
|
|
2259
|
+
* @typeParam TData - children `<Marker>`의 `item` prop 타입
|
|
2260
|
+
*/
|
|
2261
|
+
function MarkerClusterer(props) {
|
|
2262
|
+
const { algorithm: algorithmProp, clusterIcon, onClusterClick, behavior, clusterData, enabled = true, children } = props;
|
|
2263
|
+
const { map, sdkStatus } = useNaverMap();
|
|
2264
|
+
const registryRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2265
|
+
const [registryVersion, setRegistryVersion] = (0, react.useState)(0);
|
|
2266
|
+
const registry = (0, react.useMemo)(() => ({
|
|
2267
|
+
enabled,
|
|
2268
|
+
register(item) {
|
|
2269
|
+
registryRef.current.set(item.id, item);
|
|
2270
|
+
setRegistryVersion((v) => v + 1);
|
|
2271
|
+
},
|
|
2272
|
+
unregister(id) {
|
|
2273
|
+
registryRef.current.delete(id);
|
|
2274
|
+
setRegistryVersion((v) => v + 1);
|
|
2275
|
+
}
|
|
2276
|
+
}), [enabled]);
|
|
2277
|
+
const algorithmRef = (0, react.useRef)(null);
|
|
2278
|
+
const algorithm = (0, react.useMemo)(() => {
|
|
2279
|
+
if (algorithmProp && !isBuiltInConfig(algorithmProp)) return algorithmProp;
|
|
2280
|
+
const config = algorithmProp ?? {
|
|
2281
|
+
type: "supercluster",
|
|
2282
|
+
radius: 60
|
|
2283
|
+
};
|
|
2284
|
+
if (!isBuiltInConfig(config)) return config;
|
|
2285
|
+
return createAlgorithm(config);
|
|
2286
|
+
}, [algorithmProp]);
|
|
2287
|
+
(0, react.useEffect)(() => {
|
|
2288
|
+
const prev = algorithmRef.current;
|
|
2289
|
+
algorithmRef.current = algorithm;
|
|
2290
|
+
return () => {
|
|
2291
|
+
if (prev && prev !== algorithm) prev.destroy?.();
|
|
2292
|
+
};
|
|
2293
|
+
}, [algorithm]);
|
|
2294
|
+
const pointMarkersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2295
|
+
const clusterMarkersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2296
|
+
const clusterIconRootsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2297
|
+
const clusterIconContainersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2298
|
+
const helpersRef = (0, react.useRef)({
|
|
2299
|
+
zoomToCluster: () => {},
|
|
2300
|
+
fitBounds: () => {}
|
|
2301
|
+
});
|
|
2302
|
+
(0, react.useEffect)(() => {
|
|
2303
|
+
if (!map) return;
|
|
2304
|
+
helpersRef.current = {
|
|
2305
|
+
zoomToCluster(cluster, options) {
|
|
2306
|
+
if (cluster.bounds) {
|
|
2307
|
+
const bounds = options?.padding ? padBounds(cluster.bounds, options.padding) : cluster.bounds;
|
|
2308
|
+
const latLngBounds = new naver.maps.LatLngBounds(new naver.maps.LatLng(bounds.south, bounds.west), new naver.maps.LatLng(bounds.north, bounds.east));
|
|
2309
|
+
map.fitBounds(latLngBounds);
|
|
2310
|
+
if (options?.maxZoom !== void 0) {
|
|
2311
|
+
if (map.getZoom() > options.maxZoom) map.setZoom(options.maxZoom);
|
|
2312
|
+
}
|
|
2313
|
+
} else {
|
|
2314
|
+
const nextZoom = Math.min(map.getZoom() + 1, options?.maxZoom ?? 21);
|
|
2315
|
+
map.setCenter(new naver.maps.LatLng(cluster.position.lat, cluster.position.lng));
|
|
2316
|
+
map.setZoom(nextZoom);
|
|
2317
|
+
}
|
|
2318
|
+
},
|
|
2319
|
+
fitBounds(bounds, options) {
|
|
2320
|
+
const paddedBounds = options?.padding ? padBounds(bounds, options.padding) : bounds;
|
|
2321
|
+
const latLngBounds = new naver.maps.LatLngBounds(new naver.maps.LatLng(paddedBounds.south, paddedBounds.west), new naver.maps.LatLng(paddedBounds.north, paddedBounds.east));
|
|
2322
|
+
map.fitBounds(latLngBounds);
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
}, [map]);
|
|
2326
|
+
const onClusterClickRef = (0, react.useRef)(onClusterClick);
|
|
2327
|
+
(0, react.useEffect)(() => {
|
|
2328
|
+
onClusterClickRef.current = onClusterClick;
|
|
2329
|
+
}, [onClusterClick]);
|
|
2330
|
+
const clusterIconRef = (0, react.useRef)(clusterIcon);
|
|
2331
|
+
(0, react.useEffect)(() => {
|
|
2332
|
+
clusterIconRef.current = clusterIcon;
|
|
2333
|
+
}, [clusterIcon]);
|
|
2334
|
+
const recompute = (0, react.useCallback)(() => {
|
|
2335
|
+
if (!map || sdkStatus !== "ready" || !enabled) return;
|
|
2336
|
+
const zoom = map.getZoom();
|
|
2337
|
+
const bounds = getMapBounds(map);
|
|
2338
|
+
const items = Array.from(registryRef.current.values());
|
|
2339
|
+
const { clusters, points } = algorithm.cluster(items, {
|
|
2340
|
+
zoom,
|
|
2341
|
+
bounds
|
|
2342
|
+
});
|
|
2343
|
+
const maxItems = clusterData?.maxItemsInCluster;
|
|
2344
|
+
const includeItems = clusterData?.includeItems ?? true;
|
|
2345
|
+
const processedClusters = clusters.map((c) => ({
|
|
2346
|
+
...c,
|
|
2347
|
+
items: includeItems ? maxItems !== void 0 ? c.items?.slice(0, maxItems) : c.items : void 0
|
|
2348
|
+
}));
|
|
2349
|
+
const nextPointIds = new Set(points.map((p) => p.id));
|
|
2350
|
+
const prevPointMarkers = pointMarkersRef.current;
|
|
2351
|
+
for (const [id, marker] of prevPointMarkers) if (!nextPointIds.has(id)) {
|
|
2352
|
+
marker.setMap(null);
|
|
2353
|
+
prevPointMarkers.delete(id);
|
|
2354
|
+
}
|
|
2355
|
+
for (const point of points) {
|
|
2356
|
+
const existing = prevPointMarkers.get(point.id);
|
|
2357
|
+
if (existing) {
|
|
2358
|
+
const pos = new naver.maps.LatLng(point.position.lat, point.position.lng);
|
|
2359
|
+
existing.setPosition(pos);
|
|
2360
|
+
if (point.markerOptions) existing.setOptions({ ...point.markerOptions });
|
|
2361
|
+
} else {
|
|
2362
|
+
const opts = {
|
|
2363
|
+
position: new naver.maps.LatLng(point.position.lat, point.position.lng),
|
|
2364
|
+
map
|
|
2365
|
+
};
|
|
2366
|
+
if (point.markerOptions) Object.assign(opts, point.markerOptions);
|
|
2367
|
+
const marker = new naver.maps.Marker(opts);
|
|
2368
|
+
prevPointMarkers.set(point.id, marker);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const nextClusterIds = new Set(processedClusters.map((c) => c.id));
|
|
2372
|
+
const prevClusterMarkers = clusterMarkersRef.current;
|
|
2373
|
+
const prevRoots = clusterIconRootsRef.current;
|
|
2374
|
+
const prevContainers = clusterIconContainersRef.current;
|
|
2375
|
+
for (const [id, marker] of prevClusterMarkers) if (!nextClusterIds.has(id)) {
|
|
2376
|
+
marker.setMap(null);
|
|
2377
|
+
prevClusterMarkers.delete(id);
|
|
2378
|
+
const root = prevRoots.get(id);
|
|
2379
|
+
if (root) {
|
|
2380
|
+
root.unmount();
|
|
2381
|
+
prevRoots.delete(id);
|
|
2382
|
+
}
|
|
2383
|
+
prevContainers.delete(id);
|
|
2384
|
+
}
|
|
2385
|
+
for (const cluster of processedClusters) {
|
|
2386
|
+
const existingMarker = prevClusterMarkers.get(cluster.id);
|
|
2387
|
+
const renderer = clusterIconRef.current;
|
|
2388
|
+
const iconNode = renderer ? renderer({
|
|
2389
|
+
cluster,
|
|
2390
|
+
count: cluster.count
|
|
2391
|
+
}) : DefaultClusterIcon({ count: cluster.count });
|
|
2392
|
+
if (existingMarker) {
|
|
2393
|
+
existingMarker.setPosition(new naver.maps.LatLng(cluster.position.lat, cluster.position.lng));
|
|
2394
|
+
const root = prevRoots.get(cluster.id);
|
|
2395
|
+
if (root) root.render(iconNode);
|
|
2396
|
+
} else {
|
|
2397
|
+
const container = document.createElement("div");
|
|
2398
|
+
container.style.cursor = "pointer";
|
|
2399
|
+
const root = (0, react_dom_client.createRoot)(container);
|
|
2400
|
+
root.render(iconNode);
|
|
2401
|
+
prevContainers.set(cluster.id, container);
|
|
2402
|
+
prevRoots.set(cluster.id, root);
|
|
2403
|
+
const marker = new naver.maps.Marker({
|
|
2404
|
+
position: new naver.maps.LatLng(cluster.position.lat, cluster.position.lng),
|
|
2405
|
+
map,
|
|
2406
|
+
icon: { content: container },
|
|
2407
|
+
clickable: true
|
|
2408
|
+
});
|
|
2409
|
+
naver.maps.Event.addListener(marker, "click", () => {
|
|
2410
|
+
onClusterClickRef.current?.({
|
|
2411
|
+
cluster,
|
|
2412
|
+
helpers: helpersRef.current
|
|
2413
|
+
});
|
|
2414
|
+
});
|
|
2415
|
+
prevClusterMarkers.set(cluster.id, marker);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}, [
|
|
2419
|
+
map,
|
|
2420
|
+
sdkStatus,
|
|
2421
|
+
enabled,
|
|
2422
|
+
algorithm,
|
|
2423
|
+
clusterData?.includeItems,
|
|
2424
|
+
clusterData?.maxItemsInCluster
|
|
2425
|
+
]);
|
|
2426
|
+
(0, react.useEffect)(() => {
|
|
2427
|
+
recompute();
|
|
2428
|
+
}, [recompute, registryVersion]);
|
|
2429
|
+
(0, react.useEffect)(() => {
|
|
2430
|
+
if (!map || sdkStatus !== "ready" || !enabled) return;
|
|
2431
|
+
const recomputeOn = behavior?.recomputeOn ?? "idle";
|
|
2432
|
+
const debounceMs = behavior?.debounceMs ?? 200;
|
|
2433
|
+
let timerId = null;
|
|
2434
|
+
const listeners = [];
|
|
2435
|
+
const debouncedRecompute = () => {
|
|
2436
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
2437
|
+
timerId = setTimeout(() => {
|
|
2438
|
+
recompute();
|
|
2439
|
+
timerId = null;
|
|
2440
|
+
}, debounceMs);
|
|
2441
|
+
};
|
|
2442
|
+
if (recomputeOn === "idle") listeners.push(naver.maps.Event.addListener(map, "idle", debouncedRecompute));
|
|
2443
|
+
else if (recomputeOn === "move") listeners.push(naver.maps.Event.addListener(map, "bounds_changed", debouncedRecompute));
|
|
2444
|
+
else if (recomputeOn === "zoom") listeners.push(naver.maps.Event.addListener(map, "zoom_changed", debouncedRecompute));
|
|
2445
|
+
recompute();
|
|
2446
|
+
return () => {
|
|
2447
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
2448
|
+
if (listeners.length > 0) naver.maps.Event.removeListener(listeners);
|
|
2449
|
+
};
|
|
2450
|
+
}, [
|
|
2451
|
+
map,
|
|
2452
|
+
sdkStatus,
|
|
2453
|
+
enabled,
|
|
2454
|
+
behavior?.recomputeOn,
|
|
2455
|
+
behavior?.debounceMs,
|
|
2456
|
+
recompute
|
|
2457
|
+
]);
|
|
2458
|
+
(0, react.useEffect)(() => {
|
|
2459
|
+
return () => {
|
|
2460
|
+
for (const marker of pointMarkersRef.current.values()) marker.setMap(null);
|
|
2461
|
+
pointMarkersRef.current.clear();
|
|
2462
|
+
for (const marker of clusterMarkersRef.current.values()) marker.setMap(null);
|
|
2463
|
+
clusterMarkersRef.current.clear();
|
|
2464
|
+
for (const root of clusterIconRootsRef.current.values()) root.unmount();
|
|
2465
|
+
clusterIconRootsRef.current.clear();
|
|
2466
|
+
clusterIconContainersRef.current.clear();
|
|
2467
|
+
};
|
|
2468
|
+
}, []);
|
|
2469
|
+
(0, react.useEffect)(() => {
|
|
2470
|
+
if (enabled) return;
|
|
2471
|
+
for (const marker of pointMarkersRef.current.values()) marker.setMap(null);
|
|
2472
|
+
pointMarkersRef.current.clear();
|
|
2473
|
+
for (const marker of clusterMarkersRef.current.values()) marker.setMap(null);
|
|
2474
|
+
clusterMarkersRef.current.clear();
|
|
2475
|
+
for (const root of clusterIconRootsRef.current.values()) root.unmount();
|
|
2476
|
+
clusterIconRootsRef.current.clear();
|
|
2477
|
+
clusterIconContainersRef.current.clear();
|
|
2478
|
+
}, [enabled]);
|
|
2479
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererContext.Provider, {
|
|
2480
|
+
value: registry,
|
|
2481
|
+
children
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
MarkerClusterer.displayName = "MarkerClusterer";
|
|
2485
|
+
|
|
1149
2486
|
//#endregion
|
|
1150
2487
|
//#region src/overlays/infowindow/InfoWindow.tsx
|
|
1151
2488
|
function toInfoWindowOptions(props) {
|
|
@@ -2629,10 +3966,12 @@ const version = "0.0.1";
|
|
|
2629
3966
|
|
|
2630
3967
|
//#endregion
|
|
2631
3968
|
exports.Circle = Circle;
|
|
3969
|
+
exports.ClustererContext = ClustererContext;
|
|
2632
3970
|
exports.Ellipse = Ellipse;
|
|
2633
3971
|
exports.GroundOverlay = GroundOverlay;
|
|
2634
3972
|
exports.InfoWindow = InfoWindow;
|
|
2635
3973
|
exports.Marker = Marker;
|
|
3974
|
+
exports.MarkerClusterer = MarkerClusterer;
|
|
2636
3975
|
exports.NaverMap = NaverMap;
|
|
2637
3976
|
exports.NaverMapContext = NaverMapContext;
|
|
2638
3977
|
exports.NaverMapProvider = NaverMapProvider;
|