react-naver-maps-kit 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1306 -43
- 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 +1307 -46
- 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 +46 -0
- package/dist/overlays/marker-clusterer/ClustererContext.d.ts.map +1 -0
- package/dist/overlays/marker-clusterer/MarkerClusterer.d.ts +49 -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 +1 -0
- package/dist/react/components/NaverMap.d.ts.map +1 -1
- package/package.json +19 -17
package/dist/index.cjs
CHANGED
|
@@ -872,6 +872,27 @@ function useNaverMapInstance(options = {}) {
|
|
|
872
872
|
}).map;
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/overlays/marker-clusterer/ClustererContext.ts
|
|
877
|
+
/**
|
|
878
|
+
* `<MarkerClusterer>`가 제공하는 registry Context.
|
|
879
|
+
* register/unregister/enabled만 포함하며, enabled가 바뀔 때만 갱신됩니다.
|
|
880
|
+
*
|
|
881
|
+
* @internal
|
|
882
|
+
*/
|
|
883
|
+
const ClustererContext = (0, react.createContext)(null);
|
|
884
|
+
ClustererContext.displayName = "ClustererContext";
|
|
885
|
+
/**
|
|
886
|
+
* `<MarkerClusterer>`가 제공하는 visibility Context.
|
|
887
|
+
* 재계산 시마다 갱신되며, 클러스터링 안 된 마커 ID 집합을 담습니다.
|
|
888
|
+
*
|
|
889
|
+
* `<Marker>`는 이 context를 읽어 자신이 보여야 할지 판단합니다.
|
|
890
|
+
*
|
|
891
|
+
* @internal
|
|
892
|
+
*/
|
|
893
|
+
const ClustererVisibilityContext = (0, react.createContext)(/* @__PURE__ */ new Set());
|
|
894
|
+
ClustererVisibilityContext.displayName = "ClustererVisibilityContext";
|
|
895
|
+
|
|
875
896
|
//#endregion
|
|
876
897
|
//#region src/overlays/marker/Marker.tsx
|
|
877
898
|
function pickHtmlIconAnchor(icon) {
|
|
@@ -905,87 +926,87 @@ function toMarkerOptions(props, targetMap, icon) {
|
|
|
905
926
|
if (props.zIndex !== void 0) options.zIndex = props.zIndex;
|
|
906
927
|
return options;
|
|
907
928
|
}
|
|
908
|
-
function buildMarkerEventBindings(
|
|
929
|
+
function buildMarkerEventBindings(propsRef) {
|
|
909
930
|
return [
|
|
910
931
|
{
|
|
911
932
|
eventName: "click",
|
|
912
|
-
invoke:
|
|
933
|
+
invoke: (event) => propsRef.current.onClick?.(event)
|
|
913
934
|
},
|
|
914
935
|
{
|
|
915
936
|
eventName: "dblclick",
|
|
916
|
-
invoke:
|
|
937
|
+
invoke: (event) => propsRef.current.onDblClick?.(event)
|
|
917
938
|
},
|
|
918
939
|
{
|
|
919
940
|
eventName: "rightclick",
|
|
920
|
-
invoke:
|
|
941
|
+
invoke: (event) => propsRef.current.onRightClick?.(event)
|
|
921
942
|
},
|
|
922
943
|
{
|
|
923
944
|
eventName: "mousedown",
|
|
924
|
-
invoke:
|
|
945
|
+
invoke: (event) => propsRef.current.onMouseDown?.(event)
|
|
925
946
|
},
|
|
926
947
|
{
|
|
927
948
|
eventName: "mouseup",
|
|
928
|
-
invoke:
|
|
949
|
+
invoke: (event) => propsRef.current.onMouseUp?.(event)
|
|
929
950
|
},
|
|
930
951
|
{
|
|
931
952
|
eventName: "touchstart",
|
|
932
|
-
invoke:
|
|
953
|
+
invoke: (event) => propsRef.current.onTouchStart?.(event)
|
|
933
954
|
},
|
|
934
955
|
{
|
|
935
956
|
eventName: "touchend",
|
|
936
|
-
invoke:
|
|
957
|
+
invoke: (event) => propsRef.current.onTouchEnd?.(event)
|
|
937
958
|
},
|
|
938
959
|
{
|
|
939
960
|
eventName: "dragstart",
|
|
940
|
-
invoke:
|
|
961
|
+
invoke: (event) => propsRef.current.onDragStart?.(event)
|
|
941
962
|
},
|
|
942
963
|
{
|
|
943
964
|
eventName: "drag",
|
|
944
|
-
invoke:
|
|
965
|
+
invoke: (event) => propsRef.current.onDrag?.(event)
|
|
945
966
|
},
|
|
946
967
|
{
|
|
947
968
|
eventName: "dragend",
|
|
948
|
-
invoke:
|
|
969
|
+
invoke: (event) => propsRef.current.onDragEnd?.(event)
|
|
949
970
|
},
|
|
950
971
|
{
|
|
951
972
|
eventName: "clickable_changed",
|
|
952
|
-
invoke:
|
|
973
|
+
invoke: (event) => propsRef.current.onClickableChanged?.(event)
|
|
953
974
|
},
|
|
954
975
|
{
|
|
955
976
|
eventName: "cursor_changed",
|
|
956
|
-
invoke:
|
|
977
|
+
invoke: (event) => propsRef.current.onCursorChanged?.(event)
|
|
957
978
|
},
|
|
958
979
|
{
|
|
959
980
|
eventName: "draggable_changed",
|
|
960
|
-
invoke:
|
|
981
|
+
invoke: (event) => propsRef.current.onDraggableChanged?.(event)
|
|
961
982
|
},
|
|
962
983
|
{
|
|
963
984
|
eventName: "icon_changed",
|
|
964
|
-
invoke:
|
|
985
|
+
invoke: (event) => propsRef.current.onIconChanged?.(event)
|
|
965
986
|
},
|
|
966
987
|
{
|
|
967
988
|
eventName: "icon_loaded",
|
|
968
|
-
invoke:
|
|
989
|
+
invoke: (event) => propsRef.current.onIconLoaded?.(event)
|
|
969
990
|
},
|
|
970
991
|
{
|
|
971
992
|
eventName: "position_changed",
|
|
972
|
-
invoke:
|
|
993
|
+
invoke: (event) => propsRef.current.onPositionChanged?.(event)
|
|
973
994
|
},
|
|
974
995
|
{
|
|
975
996
|
eventName: "shape_changed",
|
|
976
|
-
invoke:
|
|
997
|
+
invoke: (event) => propsRef.current.onShapeChanged?.(event)
|
|
977
998
|
},
|
|
978
999
|
{
|
|
979
1000
|
eventName: "title_changed",
|
|
980
|
-
invoke:
|
|
1001
|
+
invoke: (event) => propsRef.current.onTitleChanged?.(event)
|
|
981
1002
|
},
|
|
982
1003
|
{
|
|
983
1004
|
eventName: "visible_changed",
|
|
984
|
-
invoke:
|
|
1005
|
+
invoke: (event) => propsRef.current.onVisibleChanged?.(event)
|
|
985
1006
|
},
|
|
986
1007
|
{
|
|
987
1008
|
eventName: "zIndex_changed",
|
|
988
|
-
invoke:
|
|
1009
|
+
invoke: (event) => propsRef.current.onZIndexChanged?.(event)
|
|
989
1010
|
}
|
|
990
1011
|
];
|
|
991
1012
|
}
|
|
@@ -998,24 +1019,73 @@ function bindMarkerEventListeners(marker, listenersRef, bindings) {
|
|
|
998
1019
|
binding.invoke?.(event);
|
|
999
1020
|
}));
|
|
1000
1021
|
}
|
|
1022
|
+
function toLatLngLiteral(position) {
|
|
1023
|
+
if (!position) return null;
|
|
1024
|
+
if (typeof position === "object" && "lat" in position && "lng" in position) {
|
|
1025
|
+
if (typeof position.lat === "number" && typeof position.lng === "number") return {
|
|
1026
|
+
lat: position.lat,
|
|
1027
|
+
lng: position.lng
|
|
1028
|
+
};
|
|
1029
|
+
if (typeof position.lat === "function" && typeof position.lng === "function") return {
|
|
1030
|
+
lat: position.lat(),
|
|
1031
|
+
lng: position.lng()
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof position === "object" && "x" in position && "y" in position) {
|
|
1035
|
+
const p = position;
|
|
1036
|
+
if (typeof p.x === "number" && typeof p.y === "number") return {
|
|
1037
|
+
lat: p.y,
|
|
1038
|
+
lng: p.x
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1001
1043
|
const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
1002
1044
|
const { map: contextMap, sdkStatus } = useNaverMap();
|
|
1045
|
+
const clustererRegistry = (0, react.useContext)(ClustererContext);
|
|
1046
|
+
const visibleIds = (0, react.useContext)(ClustererVisibilityContext);
|
|
1047
|
+
const isInsideClusterer = clustererRegistry !== null && clustererRegistry.enabled;
|
|
1048
|
+
const autoId = (0, react.useId)();
|
|
1049
|
+
const effectiveId = props.clustererItemId ?? autoId;
|
|
1050
|
+
const isHiddenByClusterer = isInsideClusterer && !visibleIds.has(effectiveId);
|
|
1003
1051
|
const markerRef = (0, react.useRef)(null);
|
|
1004
1052
|
const markerEventListenersRef = (0, react.useRef)([]);
|
|
1005
1053
|
const onMarkerDestroyRef = (0, react.useRef)(props.onMarkerDestroy);
|
|
1006
|
-
const [markerDiv
|
|
1054
|
+
const [markerDiv] = (0, react.useState)(() => document.createElement("div"));
|
|
1007
1055
|
const [portalReady, setPortalReady] = (0, react.useState)(false);
|
|
1008
1056
|
const hasChildren = props.children !== void 0 && props.children !== null;
|
|
1009
1057
|
const targetMap = props.map ?? contextMap;
|
|
1058
|
+
const propsRef = (0, react.useRef)(props);
|
|
1059
|
+
(0, react.useEffect)(() => {
|
|
1060
|
+
propsRef.current = props;
|
|
1061
|
+
});
|
|
1062
|
+
(0, react.useEffect)(() => {
|
|
1063
|
+
if (!clustererRegistry) return;
|
|
1064
|
+
const id = props.clustererItemId ?? autoId;
|
|
1065
|
+
const latLng = toLatLngLiteral(props.position);
|
|
1066
|
+
if (!latLng) return;
|
|
1067
|
+
clustererRegistry.register({
|
|
1068
|
+
id,
|
|
1069
|
+
position: latLng,
|
|
1070
|
+
data: props.item ?? null,
|
|
1071
|
+
markerOptions: props.icon !== void 0 ? { icon: props.icon } : void 0
|
|
1072
|
+
});
|
|
1073
|
+
return () => {
|
|
1074
|
+
clustererRegistry.unregister(id);
|
|
1075
|
+
};
|
|
1076
|
+
}, [
|
|
1077
|
+
autoId,
|
|
1078
|
+
clustererRegistry,
|
|
1079
|
+
props.clustererItemId,
|
|
1080
|
+
props.position,
|
|
1081
|
+
props.item,
|
|
1082
|
+
props.icon
|
|
1083
|
+
]);
|
|
1010
1084
|
(0, react.useEffect)(() => {
|
|
1011
1085
|
onMarkerDestroyRef.current = props.onMarkerDestroy;
|
|
1012
1086
|
}, [props.onMarkerDestroy]);
|
|
1013
1087
|
(0, react.useEffect)(() => {
|
|
1014
|
-
if (
|
|
1015
|
-
setMarkerDiv(document.createElement("div"));
|
|
1016
|
-
}, []);
|
|
1017
|
-
(0, react.useEffect)(() => {
|
|
1018
|
-
if (!hasChildren || !markerDiv) {
|
|
1088
|
+
if (!hasChildren) {
|
|
1019
1089
|
setPortalReady(false);
|
|
1020
1090
|
return;
|
|
1021
1091
|
}
|
|
@@ -1087,68 +1157,1259 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
|
|
|
1087
1157
|
setZIndex: (...args) => invokeMarkerMethod("setZIndex", ...args)
|
|
1088
1158
|
}), [invokeMarkerMethod]);
|
|
1089
1159
|
(0, react.useEffect)(() => {
|
|
1160
|
+
if (isHiddenByClusterer) return;
|
|
1090
1161
|
if (sdkStatus !== "ready" || !targetMap || markerRef.current) return;
|
|
1091
1162
|
if (hasChildren && !portalReady) return;
|
|
1092
1163
|
try {
|
|
1093
|
-
const resolvedIcon = resolveMarkerIcon(
|
|
1094
|
-
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));
|
|
1095
1166
|
markerRef.current = marker;
|
|
1096
|
-
if (
|
|
1097
|
-
if (
|
|
1098
|
-
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(
|
|
1099
|
-
|
|
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);
|
|
1100
1171
|
} catch (error) {
|
|
1101
1172
|
const normalizedError = error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create naver.maps.Marker instance.");
|
|
1102
|
-
|
|
1173
|
+
propsRef.current.onMarkerError?.(normalizedError);
|
|
1103
1174
|
}
|
|
1104
1175
|
}, [
|
|
1176
|
+
isHiddenByClusterer,
|
|
1105
1177
|
hasChildren,
|
|
1106
1178
|
markerDiv,
|
|
1107
1179
|
portalReady,
|
|
1108
|
-
props,
|
|
1109
1180
|
sdkStatus,
|
|
1110
1181
|
targetMap
|
|
1111
1182
|
]);
|
|
1112
1183
|
(0, react.useLayoutEffect)(() => {
|
|
1184
|
+
if (isHiddenByClusterer) return;
|
|
1113
1185
|
const marker = markerRef.current;
|
|
1114
1186
|
if (!marker || !targetMap) return;
|
|
1115
1187
|
const rafId = requestAnimationFrame(() => {
|
|
1116
|
-
const
|
|
1188
|
+
const currentProps = propsRef.current;
|
|
1189
|
+
const nextOptions = toMarkerOptions(currentProps, targetMap, hasChildren && portalReady ? resolveMarkerIcon(currentProps.icon, markerDiv, hasChildren) : currentProps.icon);
|
|
1117
1190
|
marker.setOptions(nextOptions);
|
|
1118
|
-
if (
|
|
1119
|
-
if (
|
|
1191
|
+
if (currentProps.collisionBehavior !== void 0) marker.setOptions("collisionBehavior", currentProps.collisionBehavior);
|
|
1192
|
+
if (currentProps.collisionBoxSize !== void 0) marker.setOptions("collisionBoxSize", currentProps.collisionBoxSize);
|
|
1120
1193
|
});
|
|
1121
1194
|
return () => {
|
|
1122
1195
|
cancelAnimationFrame(rafId);
|
|
1123
1196
|
};
|
|
1124
1197
|
}, [
|
|
1198
|
+
isHiddenByClusterer,
|
|
1125
1199
|
hasChildren,
|
|
1126
1200
|
markerDiv,
|
|
1127
1201
|
portalReady,
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
|
1130
1215
|
]);
|
|
1131
1216
|
(0, react.useEffect)(() => {
|
|
1217
|
+
if (isHiddenByClusterer) return;
|
|
1132
1218
|
const marker = markerRef.current;
|
|
1133
1219
|
if (!marker) return;
|
|
1134
|
-
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(
|
|
1220
|
+
bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(propsRef));
|
|
1135
1221
|
return () => {
|
|
1136
1222
|
if (markerEventListenersRef.current.length > 0) {
|
|
1137
1223
|
naver.maps.Event.removeListener(markerEventListenersRef.current);
|
|
1138
1224
|
markerEventListenersRef.current = [];
|
|
1139
1225
|
}
|
|
1140
1226
|
};
|
|
1141
|
-
}, [
|
|
1227
|
+
}, [isHiddenByClusterer]);
|
|
1142
1228
|
(0, react.useEffect)(() => {
|
|
1229
|
+
if (isHiddenByClusterer) return;
|
|
1143
1230
|
return () => {
|
|
1144
1231
|
teardownMarker();
|
|
1145
1232
|
};
|
|
1146
|
-
}, [teardownMarker]);
|
|
1233
|
+
}, [isHiddenByClusterer, teardownMarker]);
|
|
1234
|
+
if (isHiddenByClusterer) return null;
|
|
1147
1235
|
if (!hasChildren || !markerDiv) return null;
|
|
1148
1236
|
return (0, react_dom.createPortal)(props.children, markerDiv);
|
|
1149
1237
|
});
|
|
1150
1238
|
Marker.displayName = "Marker";
|
|
1151
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. 클러스터 마커는 `<Marker>` JSX로 렌더링되며, children으로 클러스터 아이콘이 전달됩니다.
|
|
2229
|
+
* 4. 클러스터에 포함되지 않은 단독 포인트는 원래 `<Marker>` children을 그대로 표시합니다.
|
|
2230
|
+
*
|
|
2231
|
+
* ## 기본 사용법
|
|
2232
|
+
*
|
|
2233
|
+
* ```tsx
|
|
2234
|
+
* <NaverMap ...>
|
|
2235
|
+
* <MarkerClusterer
|
|
2236
|
+
* algorithm={{ type: "supercluster", radius: 60 }}
|
|
2237
|
+
* clusterIcon={({ count }) => <ClusterBadge count={count} />}
|
|
2238
|
+
* onClusterClick={({ cluster, helpers }) =>
|
|
2239
|
+
* helpers.zoomToCluster(cluster, { maxZoom: 16 })
|
|
2240
|
+
* }
|
|
2241
|
+
* >
|
|
2242
|
+
* {points.map(p => (
|
|
2243
|
+
* <Marker
|
|
2244
|
+
* key={p.id}
|
|
2245
|
+
* position={p.position}
|
|
2246
|
+
* item={p}
|
|
2247
|
+
* >
|
|
2248
|
+
* <CustomPin />
|
|
2249
|
+
* </Marker>
|
|
2250
|
+
* ))}
|
|
2251
|
+
* </MarkerClusterer>
|
|
2252
|
+
* </NaverMap>
|
|
2253
|
+
* ```
|
|
2254
|
+
*
|
|
2255
|
+
* ## 주의 사항
|
|
2256
|
+
*
|
|
2257
|
+
* - `<MarkerClusterer>`는 반드시 `<NaverMap>` 내부에 위치해야 합니다.
|
|
2258
|
+
* - `enabled={false}`로 설정하면 클러스터링이 해제되고 각 `<Marker>`가 개별 마커로 렌더링됩니다.
|
|
2259
|
+
*
|
|
2260
|
+
* @typeParam TData - children `<Marker>`의 `item` prop 타입
|
|
2261
|
+
*/
|
|
2262
|
+
function MarkerClusterer(props) {
|
|
2263
|
+
const { algorithm: algorithmProp, clusterIcon, onClusterClick, behavior, clusterData, enabled = true, children } = props;
|
|
2264
|
+
const { map, sdkStatus } = useNaverMap();
|
|
2265
|
+
const registryRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2266
|
+
const [registryVersion, setRegistryVersion] = (0, react.useState)(0);
|
|
2267
|
+
const registry = (0, react.useMemo)(() => ({
|
|
2268
|
+
enabled,
|
|
2269
|
+
register(item) {
|
|
2270
|
+
registryRef.current.set(item.id, item);
|
|
2271
|
+
setRegistryVersion((v) => v + 1);
|
|
2272
|
+
},
|
|
2273
|
+
unregister(id) {
|
|
2274
|
+
registryRef.current.delete(id);
|
|
2275
|
+
setRegistryVersion((v) => v + 1);
|
|
2276
|
+
}
|
|
2277
|
+
}), [enabled]);
|
|
2278
|
+
const algorithmRef = (0, react.useRef)(null);
|
|
2279
|
+
const algorithm = (0, react.useMemo)(() => {
|
|
2280
|
+
if (algorithmProp && !isBuiltInConfig(algorithmProp)) return algorithmProp;
|
|
2281
|
+
const config = algorithmProp ?? {
|
|
2282
|
+
type: "supercluster",
|
|
2283
|
+
radius: 60
|
|
2284
|
+
};
|
|
2285
|
+
if (!isBuiltInConfig(config)) return config;
|
|
2286
|
+
return createAlgorithm(config);
|
|
2287
|
+
}, [algorithmProp]);
|
|
2288
|
+
(0, react.useEffect)(() => {
|
|
2289
|
+
const prev = algorithmRef.current;
|
|
2290
|
+
algorithmRef.current = algorithm;
|
|
2291
|
+
return () => {
|
|
2292
|
+
if (prev && prev !== algorithm) prev.destroy?.();
|
|
2293
|
+
};
|
|
2294
|
+
}, [algorithm]);
|
|
2295
|
+
const [clusters, setClusters] = (0, react.useState)([]);
|
|
2296
|
+
const [visibleIds, setVisibleIds] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
2297
|
+
const helpersRef = (0, react.useRef)({
|
|
2298
|
+
zoomToCluster: () => {},
|
|
2299
|
+
fitBounds: () => {}
|
|
2300
|
+
});
|
|
2301
|
+
(0, react.useEffect)(() => {
|
|
2302
|
+
if (!map) return;
|
|
2303
|
+
helpersRef.current = {
|
|
2304
|
+
zoomToCluster(cluster, options) {
|
|
2305
|
+
if (cluster.bounds) {
|
|
2306
|
+
const bounds = options?.padding ? padBounds(cluster.bounds, options.padding) : cluster.bounds;
|
|
2307
|
+
const latLngBounds = new naver.maps.LatLngBounds(new naver.maps.LatLng(bounds.south, bounds.west), new naver.maps.LatLng(bounds.north, bounds.east));
|
|
2308
|
+
map.fitBounds(latLngBounds);
|
|
2309
|
+
if (options?.maxZoom !== void 0) {
|
|
2310
|
+
if (map.getZoom() > options.maxZoom) map.setZoom(options.maxZoom);
|
|
2311
|
+
}
|
|
2312
|
+
} else {
|
|
2313
|
+
const nextZoom = Math.min(map.getZoom() + 1, options?.maxZoom ?? 21);
|
|
2314
|
+
map.setCenter(new naver.maps.LatLng(cluster.position.lat, cluster.position.lng));
|
|
2315
|
+
map.setZoom(nextZoom);
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
fitBounds(bounds, options) {
|
|
2319
|
+
const paddedBounds = options?.padding ? padBounds(bounds, options.padding) : bounds;
|
|
2320
|
+
const latLngBounds = new naver.maps.LatLngBounds(new naver.maps.LatLng(paddedBounds.south, paddedBounds.west), new naver.maps.LatLng(paddedBounds.north, paddedBounds.east));
|
|
2321
|
+
map.fitBounds(latLngBounds);
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
}, [map]);
|
|
2325
|
+
const onClusterClickRef = (0, react.useRef)(onClusterClick);
|
|
2326
|
+
(0, react.useEffect)(() => {
|
|
2327
|
+
onClusterClickRef.current = onClusterClick;
|
|
2328
|
+
}, [onClusterClick]);
|
|
2329
|
+
const recompute = (0, react.useCallback)(() => {
|
|
2330
|
+
if (!map || sdkStatus !== "ready" || !enabled) return;
|
|
2331
|
+
const zoom = map.getZoom();
|
|
2332
|
+
const bounds = getMapBounds(map);
|
|
2333
|
+
const items = Array.from(registryRef.current.values());
|
|
2334
|
+
const { clusters: rawClusters, points } = algorithm.cluster(items, {
|
|
2335
|
+
zoom,
|
|
2336
|
+
bounds
|
|
2337
|
+
});
|
|
2338
|
+
const maxItems = clusterData?.maxItemsInCluster;
|
|
2339
|
+
const includeItems = clusterData?.includeItems ?? true;
|
|
2340
|
+
setClusters(rawClusters.map((c) => ({
|
|
2341
|
+
...c,
|
|
2342
|
+
items: includeItems ? maxItems !== void 0 ? c.items?.slice(0, maxItems) : c.items : void 0
|
|
2343
|
+
})));
|
|
2344
|
+
setVisibleIds(new Set(points.map((p) => p.id)));
|
|
2345
|
+
}, [
|
|
2346
|
+
map,
|
|
2347
|
+
sdkStatus,
|
|
2348
|
+
enabled,
|
|
2349
|
+
algorithm,
|
|
2350
|
+
clusterData?.includeItems,
|
|
2351
|
+
clusterData?.maxItemsInCluster
|
|
2352
|
+
]);
|
|
2353
|
+
(0, react.useEffect)(() => {
|
|
2354
|
+
recompute();
|
|
2355
|
+
}, [recompute, registryVersion]);
|
|
2356
|
+
(0, react.useEffect)(() => {
|
|
2357
|
+
if (!map || sdkStatus !== "ready" || !enabled) return;
|
|
2358
|
+
const recomputeOn = behavior?.recomputeOn ?? "idle";
|
|
2359
|
+
const debounceMs = behavior?.debounceMs ?? 200;
|
|
2360
|
+
let timerId = null;
|
|
2361
|
+
const listeners = [];
|
|
2362
|
+
const debouncedRecompute = () => {
|
|
2363
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
2364
|
+
timerId = setTimeout(() => {
|
|
2365
|
+
recompute();
|
|
2366
|
+
timerId = null;
|
|
2367
|
+
}, debounceMs);
|
|
2368
|
+
};
|
|
2369
|
+
if (recomputeOn === "idle") listeners.push(naver.maps.Event.addListener(map, "idle", debouncedRecompute));
|
|
2370
|
+
else if (recomputeOn === "move") listeners.push(naver.maps.Event.addListener(map, "bounds_changed", debouncedRecompute));
|
|
2371
|
+
else if (recomputeOn === "zoom") listeners.push(naver.maps.Event.addListener(map, "zoom_changed", debouncedRecompute));
|
|
2372
|
+
recompute();
|
|
2373
|
+
return () => {
|
|
2374
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
2375
|
+
if (listeners.length > 0) naver.maps.Event.removeListener(listeners);
|
|
2376
|
+
};
|
|
2377
|
+
}, [
|
|
2378
|
+
map,
|
|
2379
|
+
sdkStatus,
|
|
2380
|
+
enabled,
|
|
2381
|
+
behavior?.recomputeOn,
|
|
2382
|
+
behavior?.debounceMs,
|
|
2383
|
+
recompute
|
|
2384
|
+
]);
|
|
2385
|
+
(0, react.useEffect)(() => {
|
|
2386
|
+
if (enabled) return;
|
|
2387
|
+
setClusters([]);
|
|
2388
|
+
setVisibleIds(/* @__PURE__ */ new Set());
|
|
2389
|
+
}, [enabled]);
|
|
2390
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererContext.Provider, {
|
|
2391
|
+
value: registry,
|
|
2392
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererVisibilityContext.Provider, {
|
|
2393
|
+
value: visibleIds,
|
|
2394
|
+
children
|
|
2395
|
+
})
|
|
2396
|
+
}), enabled && clusters.map((cluster) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Marker, {
|
|
2397
|
+
position: cluster.position,
|
|
2398
|
+
clickable: true,
|
|
2399
|
+
onClick: () => {
|
|
2400
|
+
onClusterClickRef.current?.({
|
|
2401
|
+
cluster,
|
|
2402
|
+
helpers: helpersRef.current
|
|
2403
|
+
});
|
|
2404
|
+
},
|
|
2405
|
+
children: clusterIcon ? clusterIcon({
|
|
2406
|
+
cluster,
|
|
2407
|
+
count: cluster.count
|
|
2408
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultClusterIcon, { count: cluster.count })
|
|
2409
|
+
}, cluster.id))] });
|
|
2410
|
+
}
|
|
2411
|
+
MarkerClusterer.displayName = "MarkerClusterer";
|
|
2412
|
+
|
|
1152
2413
|
//#endregion
|
|
1153
2414
|
//#region src/overlays/infowindow/InfoWindow.tsx
|
|
1154
2415
|
function toInfoWindowOptions(props) {
|
|
@@ -2632,10 +3893,12 @@ const version = "0.0.1";
|
|
|
2632
3893
|
|
|
2633
3894
|
//#endregion
|
|
2634
3895
|
exports.Circle = Circle;
|
|
3896
|
+
exports.ClustererContext = ClustererContext;
|
|
2635
3897
|
exports.Ellipse = Ellipse;
|
|
2636
3898
|
exports.GroundOverlay = GroundOverlay;
|
|
2637
3899
|
exports.InfoWindow = InfoWindow;
|
|
2638
3900
|
exports.Marker = Marker;
|
|
3901
|
+
exports.MarkerClusterer = MarkerClusterer;
|
|
2639
3902
|
exports.NaverMap = NaverMap;
|
|
2640
3903
|
exports.NaverMapContext = NaverMapContext;
|
|
2641
3904
|
exports.NaverMapProvider = NaverMapProvider;
|