@react-native-ohos/react-native-map-clustering 3.4.1-rc.1 → 3.5.0-rc.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/COMMITTERS.md +6 -0
  3. package/LICENSE +21 -21
  4. package/OAT.xml +57 -57
  5. package/README.OpenSource +10 -10
  6. package/README.md +13 -13
  7. package/example/.eslintrc +19 -0
  8. package/example/.node-version +6 -0
  9. package/example/.prettierrc.js +7 -0
  10. package/example/.watchmanconfig +6 -0
  11. package/example/app.json +4 -0
  12. package/example/babel.config.js +6 -0
  13. package/example/contexts.ts +9 -0
  14. package/example/harmony/AppScope/app.json5 +10 -0
  15. package/example/harmony/AppScope/resources/base/element/string.json +8 -0
  16. package/example/harmony/AppScope/resources/base/media/app_icon.png +0 -0
  17. package/example/harmony/build-profile.template.json5 +36 -0
  18. package/example/harmony/codelinter.json +32 -0
  19. package/example/harmony/entry/build-profile.json5 +22 -0
  20. package/example/harmony/entry/hvigorfile.ts +8 -0
  21. package/example/harmony/entry/oh-package.json5 +11 -0
  22. package/example/harmony/entry/src/main/cpp/CMakeLists.txt +42 -0
  23. package/example/harmony/entry/src/main/cpp/PackageProvider.cpp +17 -0
  24. package/example/harmony/entry/src/main/ets/RNPackagesFactory.ets +15 -0
  25. package/example/harmony/entry/src/main/ets/assets/fonts/Pacifico-Regular.ttf +0 -0
  26. package/example/harmony/entry/src/main/ets/assets/fonts/StintUltraCondensed-Regular.ttf +0 -0
  27. package/example/harmony/entry/src/main/ets/entryability/EntryAbility.ets +27 -0
  28. package/example/harmony/entry/src/main/ets/pages/Index.ets +209 -0
  29. package/example/harmony/entry/src/main/ets/pages/SurfaceDeadlockTest.ets +135 -0
  30. package/example/harmony/entry/src/main/ets/pages/TouchDisplayer.ets +44 -0
  31. package/example/harmony/entry/src/main/module.json5 +72 -0
  32. package/example/harmony/entry/src/main/resources/base/element/color.json +8 -0
  33. package/example/harmony/entry/src/main/resources/base/element/string.json +20 -0
  34. package/example/harmony/entry/src/main/resources/base/media/icon.png +0 -0
  35. package/example/harmony/entry/src/main/resources/base/profile/main_pages.json +5 -0
  36. package/example/harmony/entry/src/main/resources/rawfile/1.txt +1 -0
  37. package/example/harmony/format.ps1 +18 -0
  38. package/example/harmony/hvigor/hvigor-config.json5 +21 -0
  39. package/example/harmony/hvigorfile.ts +9 -0
  40. package/example/harmony/oh-package.json5 +12 -0
  41. package/example/index.js +11 -0
  42. package/example/jest.config.js +11 -0
  43. package/example/metro.config.js +30 -0
  44. package/example/package.json +61 -0
  45. package/example/react-native.config.js +11 -0
  46. package/example/scripts/create-build-profile.js +46 -0
  47. package/example/src/App.tsx +186 -0
  48. package/example/src/Navigation.tsx +163 -0
  49. package/example/src/index.tsx +59 -0
  50. package/example/tsconfig.json +14 -0
  51. package/index.d.ts +40 -40
  52. package/index.js +2 -2
  53. package/lib/ClusterHarmony.js +21 -21
  54. package/lib/ClusteredMapView.js +310 -310
  55. package/package.json +42 -42
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
3
+ * Use of this source code is governed by a MIT license that can be
4
+ * found in the LICENSE file.
5
+ */
6
+
7
+ module.exports = {
8
+ project: {
9
+ },
10
+ assets: ['./assets/fonts/'],
11
+ };
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
3
+ * Use of this source code is governed by a MIT license that can be
4
+ * found in the LICENSE file.
5
+ */
6
+
7
+
8
+ const fs = require('fs');
9
+ const JSON5 = require('json5');
10
+ const path = require('path');
11
+
12
+ const templatePath = path.join(
13
+ __dirname,
14
+ '..',
15
+ 'harmony',
16
+ 'build-profile.template.json5',
17
+ );
18
+ const existingProfilePath = path.join(
19
+ __dirname,
20
+ '..',
21
+ 'harmony',
22
+ 'build-profile.json5',
23
+ );
24
+
25
+ if (fs.existsSync(existingProfilePath)) {
26
+ let existingProfile = JSON5.parse(
27
+ fs.readFileSync(existingProfilePath, 'utf-8'),
28
+ );
29
+ let template = JSON5.parse(fs.readFileSync(templatePath, 'utf-8'));
30
+ let signingConfigs =
31
+ existingProfile.app && existingProfile.app.signingConfigs;
32
+
33
+ existingProfile = {...template};
34
+
35
+ if (signingConfigs) {
36
+ existingProfile.app.signingConfigs = signingConfigs;
37
+ }
38
+
39
+ fs.writeFileSync(
40
+ existingProfilePath,
41
+ JSON5.stringify(existingProfile, null, 2),
42
+ );
43
+ } else {
44
+ // File doesn't exist, create a copy from the template
45
+ fs.copyFileSync(templatePath, existingProfilePath);
46
+ }
@@ -0,0 +1,186 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ SafeAreaView,
4
+ StatusBar,
5
+ Text,
6
+ View,
7
+ Dimensions,
8
+ Button,
9
+ ScrollView
10
+ } from 'react-native';
11
+ import { NavigationContainer, Page } from './Navigation';
12
+ import { PortalProvider } from '@gorhom/portal';
13
+ import MapView from 'react-native-map-clustering';
14
+ import { Marker } from 'react-native-maps';
15
+ const { width, height } = Dimensions.get('window');
16
+
17
+
18
+ const INITIAL_REGION = {
19
+ latitude: 52.5,
20
+ longitude: 19.2,
21
+ latitudeDelta: 8.5,
22
+ longitudeDelta: 8.5,
23
+ };
24
+ const markers = [
25
+ { coordinate: { latitude: 52.4, longitude: 18.7 } },
26
+ { coordinate: { latitude: 52.1, longitude: 18.4 } },
27
+ { coordinate: { latitude: 52.6, longitude: 18.3 } },
28
+ { coordinate: { latitude: 51.6, longitude: 18.0 } },
29
+ { coordinate: { latitude: 53.1, longitude: 18.8 } },
30
+ { coordinate: { latitude: 52.9, longitude: 19.4 } },
31
+ { coordinate: { latitude: 52.2, longitude: 21 } },
32
+ { coordinate: { latitude: 52.4, longitude: 21 } },
33
+ { coordinate: { latitude: 51.8, longitude: 20 } }
34
+ ]
35
+
36
+
37
+
38
+ const App = () => {
39
+ const [onPressText, setOnPressText] = useState('');
40
+ const [onRegionChangeCompleteText, setOnRegionChangeCompleteText] = useState('');
41
+ const [onMarkerText, setOnMarkerText] = useState('');
42
+ const [rrefText, setRrefText] = useState('');
43
+ const rref = useRef(null);
44
+ const comList = [
45
+ {
46
+ name: 'onClusterPress',
47
+ prop: {
48
+ onClusterPress: (cluster, markers) => {
49
+ setOnPressText(`onClusterPress触发, cluster:${JSON.stringify(cluster)},markers:聚合数组长度${markers?.length}`);
50
+ }
51
+ },
52
+ text: onPressText
53
+ },
54
+ {
55
+ name: 'width: 200',
56
+ prop: { width: 200 },
57
+ },
58
+ {
59
+ name: 'width: 250',
60
+ prop: { width: 250 },
61
+ },
62
+ {
63
+ name: 'radius: 20',
64
+ prop: { radius: 20 },
65
+ },
66
+ {
67
+ name: 'radius: 200',
68
+ prop: { radius: 200 },
69
+ },
70
+ {
71
+ name: 'extent: 512',
72
+ prop: { extent: 512 },
73
+ },
74
+ {
75
+ name: 'extent: 112',
76
+ prop: { extent: 112 },
77
+ },
78
+ {
79
+ name: 'minZoom: 4',
80
+ prop: { minZoom: 4 },
81
+ },
82
+ {
83
+ name: 'minZoom: 1',
84
+ prop: { minZoom: 1 },
85
+ },
86
+ {
87
+ name: 'minPoints: 2',
88
+ prop: { minPoints: 2 },
89
+ },
90
+ {
91
+ name: 'minPoints: 4',
92
+ prop: { minPoints: 4 },
93
+ },
94
+ {
95
+ name: 'preserveClusterPressBehavior: false',
96
+ prop: { preserveClusterPressBehavior: false },
97
+ },
98
+ {
99
+ name: 'preserveClusterPressBehavior: true',
100
+ prop: { preserveClusterPressBehavior: true },
101
+ },
102
+ {
103
+ name: 'onRegionChangeComplete',
104
+ prop: {
105
+ onRegionChangeComplete: (region, markers) => {
106
+ setOnRegionChangeCompleteText(`onRegionChangeComplete触发, region:${JSON.stringify(region)},markers:聚合数组长度${markers?.length}`);
107
+ }
108
+ },
109
+ text: onRegionChangeCompleteText
110
+ },
111
+ {
112
+ name: 'onMarkersChange',
113
+ prop: {
114
+ onMarkersChange: (markers) => {
115
+ setOnMarkerText(`onMarkersChange触发, markers:聚合数组长度${markers?.length}`);
116
+ }
117
+ },
118
+ text: onMarkerText
119
+ },
120
+ {
121
+ name: 'superClusterRef',
122
+ prop: { superClusterRef: rref },
123
+ btn: () => {
124
+ setRrefText(`superClusterRef触发, superClusterRef.current.options:${JSON.stringify(rref?.current.options)}`);
125
+ },
126
+ text: rrefText
127
+ },
128
+ {
129
+ name: 'clusteringEnabled: false',
130
+ prop: { clusteringEnabled: false },
131
+ },
132
+ {
133
+ name: 'clusteringEnabled: true',
134
+ prop: { clusteringEnabled: true },
135
+ }
136
+ ]
137
+ return (
138
+ <View style={{ backgroundColor: 'black' }}>
139
+ <StatusBar barStyle="light-content" />
140
+ <SafeAreaView>
141
+ <NavigationContainer>
142
+ <PortalProvider>
143
+ {comList.map((com: any) => {
144
+ return (
145
+ <Page
146
+ key={com.name}
147
+ name={`${com.name}`}>
148
+ <View style={{ height: height - 315 }}>
149
+ <MapView
150
+ initialRegion={INITIAL_REGION}
151
+ style={{ flex: 1 }}
152
+ //@ts-ignore
153
+ clusterDistance={100}
154
+ {...com.prop}
155
+ >
156
+ {markers.map((marker, idx) => <Marker key={idx} coordinate={marker.coordinate} />)}
157
+ </MapView>
158
+ {com?.text && <Text style={{ color: "red" }}>{com?.text}</Text>}
159
+ {com?.btn && <Button color="red" title={com.name} onPress={com?.btn} />}
160
+ </View>
161
+ </Page>
162
+ );
163
+ })}
164
+ </PortalProvider>
165
+ </NavigationContainer>
166
+ </SafeAreaView>
167
+ </View>
168
+ );
169
+ }
170
+ export default App;
171
+
172
+ export const displayName = 'react-native-map-clustering';
173
+ export const framework = 'React';
174
+ export const category = 'UI';
175
+ export const title = 'react-native-map-clustering';
176
+ export const documentationURL = 'https://github.com/tomekvenits/react-native-map-clustering';
177
+ export const description = 'React Native Map Clustering';
178
+
179
+ export const examples = [
180
+ {
181
+ title: 'react-native-map-clustering',
182
+ render: function () {
183
+ return <App />;
184
+ },
185
+ },
186
+ ];
@@ -0,0 +1,163 @@
1
+ import React, {useEffect} from 'react';
2
+ import {
3
+ FlatList,
4
+ Image,
5
+ Platform,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+
12
+ export const PALETTE = {
13
+ REACT_CYAN_LIGHT: 'hsl(193, 95%, 68%)',
14
+ REACT_CYAN_DARK: 'hsl(193, 95%, 30%)',
15
+ };
16
+
17
+ const NavigationContext = React.createContext<
18
+ | {
19
+ currentPageName: string;
20
+ navigateTo: (pageName: string) => void;
21
+ registerPageName: (pageName: string) => void;
22
+ registeredPageNames: string[];
23
+ }
24
+ | undefined
25
+ >(undefined);
26
+
27
+ export function NavigationContainer({
28
+ initialPage = 'INDEX',
29
+ hasHeader = true,
30
+ children,
31
+ }: {
32
+ initialPage?: string;
33
+ children: any;
34
+ hasHeader?: boolean
35
+ }) {
36
+ const [currentPageName, setCurrentPageName] = React.useState(initialPage);
37
+ const [registeredPageNames, setRegisteredPageNames] = React.useState<
38
+ string[]
39
+ >([]);
40
+
41
+ return (
42
+ <NavigationContext.Provider
43
+ value={{
44
+ currentPageName,
45
+ navigateTo: setCurrentPageName,
46
+ registerPageName: (pageName: string) => {
47
+ setRegisteredPageNames(pageNames => {
48
+ if (pageNames.includes(pageName)) {
49
+ return pageNames;
50
+ }
51
+ return [...pageNames, pageName];
52
+ });
53
+ },
54
+ registeredPageNames,
55
+ }}>
56
+ <View style={{width: '100%', height: '100%', flexDirection: 'column'}}>
57
+ <Page name="INDEX">
58
+ <IndexPage hasHeader={hasHeader} />
59
+ </Page>
60
+ {children}
61
+ </View>
62
+ </NavigationContext.Provider>
63
+ );
64
+ }
65
+
66
+ export function useNavigation() {
67
+ return React.useContext(NavigationContext)!;
68
+ }
69
+
70
+ export function Page({name, children}: {name: string; children: any}) {
71
+ const {currentPageName, navigateTo, registerPageName} = useNavigation();
72
+
73
+ useEffect(() => {
74
+ if (name !== 'INDEX') {
75
+ registerPageName(name);
76
+ }
77
+ }, [name]);
78
+
79
+ return name === currentPageName ? (
80
+ <View style={{width: '100%', height: '100%'}}>
81
+ {name !== 'INDEX' && (
82
+ <View style={{backgroundColor: PALETTE.REACT_CYAN_DARK}}>
83
+ <TouchableOpacity
84
+ onPress={() => {
85
+ navigateTo('INDEX');
86
+ }}>
87
+ <Text
88
+ style={[styles.buttonText, {color: PALETTE.REACT_CYAN_LIGHT}]}>
89
+ {'‹ Back'}
90
+ </Text>
91
+ </TouchableOpacity>
92
+ </View>
93
+ )}
94
+ <View style={{width: '100%', flex: 1}}>{children}</View>
95
+ </View>
96
+ ) : null;
97
+ }
98
+
99
+ export function IndexPage({ hasHeader }: { hasHeader: boolean }) {
100
+ const {navigateTo, registeredPageNames} = useNavigation();
101
+
102
+ return (
103
+ <FlatList
104
+ data={registeredPageNames}
105
+ ListHeaderComponent={
106
+ hasHeader ? <View
107
+ style={{
108
+ flexDirection: 'row',
109
+ alignItems: 'center',
110
+ paddingHorizontal: 16,
111
+ paddingVertical: 16,
112
+ }}>
113
+ <Text
114
+ style={{
115
+ color: '#EEE',
116
+ fontSize: 24,
117
+ fontWeight: 'bold',
118
+ padding: 16,
119
+ }}>
120
+ RN Map Clustering Capi Tester
121
+ {'rnohArchitecture' in Platform.constants
122
+ ? (` (${Platform.constants.rnohArchitecture})` as string)
123
+ : ''}
124
+ </Text>
125
+ </View>
126
+ : null
127
+ }
128
+ renderItem={({item}) => {
129
+ return (
130
+ <View style={{backgroundColor: PALETTE.REACT_CYAN_DARK}}>
131
+ <TouchableOpacity
132
+ onPress={() => {
133
+ navigateTo(item);
134
+ }}>
135
+ <Text style={styles.buttonText}>{item}</Text>
136
+ </TouchableOpacity>
137
+ </View>
138
+ );
139
+ }}
140
+ ItemSeparatorComponent={() => (
141
+ <View
142
+ style={{height: StyleSheet.hairlineWidth, backgroundColor: '#666'}}
143
+ />
144
+ )}
145
+ />
146
+ );
147
+ }
148
+
149
+ const styles = StyleSheet.create({
150
+ container: {
151
+ width: '100%',
152
+ height: '100%',
153
+ backgroundColor: '#888',
154
+ },
155
+ buttonText: {
156
+ width: '100%',
157
+ fontWeight: 'bold',
158
+ paddingHorizontal: 16,
159
+ paddingVertical: 24,
160
+ color: 'white',
161
+ backgroundColor: 'black',
162
+ },
163
+ });
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
3
+ * Use of this source code is governed by a MIT license that can be
4
+ * found in the LICENSE file.
5
+ */
6
+
7
+ import React from "react";
8
+ import MapView from "react-native-map-clustering";
9
+ import { Marker } from "react-native-maps";
10
+
11
+ function getRandomLatitude(min = 48, max = 56) {
12
+ return Math.random() * (max - min) + min;
13
+ }
14
+
15
+ function getRandomLongitude(min = 14, max = 24) {
16
+ return Math.random() * (max - min) + min;
17
+ }
18
+
19
+ const INITIAL_REGION = {
20
+ latitude: 52.5,
21
+ longitude: 19.2,
22
+ latitudeDelta: 8.5,
23
+ longitudeDelta: 8.5
24
+ };
25
+
26
+ function MapClusteringExample() {
27
+ const _generateMarkers = (count: number) => {
28
+ const markers = [];
29
+
30
+ for (let i = 0; i < count; i++) {
31
+ markers.push(
32
+ <Marker
33
+
34
+ title='456'
35
+ key={i}
36
+ coordinate={{
37
+ latitude: getRandomLatitude(),
38
+ longitude: getRandomLongitude()
39
+ }}
40
+ />
41
+ );
42
+ }
43
+
44
+ return markers;
45
+ };
46
+
47
+ return (
48
+ <MapView initialRegion={INITIAL_REGION}
49
+ style={{ flex: 1 }}
50
+ >
51
+ {_generateMarkers(200)}
52
+ </MapView>
53
+ );
54
+ };
55
+
56
+
57
+
58
+ //export default App
59
+ export default MapClusteringExample;
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "@react-native/typescript-config/tsconfig.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-native",
5
+ "paths": {
6
+ "react-native": [
7
+ "./node_modules/react-native-harmony"
8
+ ],
9
+ },
10
+ },
11
+ "exclude": [
12
+ "harmony"
13
+ ]
14
+ }
package/index.d.ts CHANGED
@@ -1,40 +1,40 @@
1
- declare module "react-native-map-clustering" {
2
- import * as React from "react";
3
- import { LayoutAnimationConfig } from "react-native";
4
- import Map, { MapViewProps, Marker } from "react-native-maps";
5
-
6
- export type Cluster = {};
7
-
8
- interface MapClusteringProps {
9
- clusteringEnabled?: boolean;
10
- spiralEnabled?: boolean;
11
- animationEnabled?: boolean;
12
- preserveClusterPressBehavior?: boolean;
13
- tracksViewChanges?: boolean;
14
- layoutAnimationConf?: LayoutAnimationConfig;
15
- radius?: number;
16
- maxZoom?: number;
17
- minZoom?: number;
18
- extent?: number;
19
- nodeSize?: number;
20
- minPoints?: number;
21
- edgePadding?: { top: number; left: number; right: number; bottom: number };
22
- clusterColor?: string;
23
- clusterTextColor?: string;
24
- clusterFontFamily?: string;
25
- selectedClusterId?: string;
26
- selectedClusterColor?: string;
27
- spiderLineColor?: string;
28
- superClusterRef?: React.MutableRefObject<any>;
29
- mapRef?: (ref: React.Ref<Map>) => void;
30
- onClusterPress?: (cluster: Marker, markers?: Marker[]) => void;
31
- getClusterEngine?: (ref: any) => void;
32
- onMarkersChange?: (markers?: Marker[]) => void;
33
- renderCluster?: (cluster: any) => React.ReactNode;
34
- }
35
-
36
- export default class MapView extends React.Component<
37
- MapViewProps & MapClusteringProps,
38
- any
39
- > {}
40
- }
1
+ declare module "react-native-map-clustering" {
2
+ import * as React from "react";
3
+ import { LayoutAnimationConfig } from "react-native";
4
+ import Map, { MapViewProps, Marker } from "react-native-maps";
5
+
6
+ export type Cluster = {};
7
+
8
+ interface MapClusteringProps {
9
+ clusteringEnabled?: boolean;
10
+ spiralEnabled?: boolean;
11
+ animationEnabled?: boolean;
12
+ preserveClusterPressBehavior?: boolean;
13
+ tracksViewChanges?: boolean;
14
+ layoutAnimationConf?: LayoutAnimationConfig;
15
+ radius?: number;
16
+ maxZoom?: number;
17
+ minZoom?: number;
18
+ extent?: number;
19
+ nodeSize?: number;
20
+ minPoints?: number;
21
+ edgePadding?: { top: number; left: number; right: number; bottom: number };
22
+ clusterColor?: string;
23
+ clusterTextColor?: string;
24
+ clusterFontFamily?: string;
25
+ selectedClusterId?: string;
26
+ selectedClusterColor?: string;
27
+ spiderLineColor?: string;
28
+ superClusterRef?: React.MutableRefObject<any>;
29
+ mapRef?: (ref: React.Ref<Map>) => void;
30
+ onClusterPress?: (cluster: Marker, markers?: Marker[]) => void;
31
+ getClusterEngine?: (ref: any) => void;
32
+ onMarkersChange?: (markers?: Marker[]) => void;
33
+ renderCluster?: (cluster: any) => React.ReactNode;
34
+ }
35
+
36
+ export default class MapView extends React.Component<
37
+ MapViewProps & MapClusteringProps,
38
+ any
39
+ > {}
40
+ }
package/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import * as MapView from "./lib/ClusteredMapView";
2
- module.exports = MapView;
1
+ import * as MapView from "./lib/ClusteredMapView";
2
+ module.exports = MapView;
@@ -1,21 +1,21 @@
1
- /*
2
- * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
3
- * Use of this source code is governed by a MIT license that can be
4
- * found in the LICENSE file.
5
- */
6
-
7
- import React, { memo } from "react";
8
- import { Cluster } from "react-native-maps";
9
-
10
- const ClusterHarmony = ({ marker, distance, onPress }) => {
11
- return (
12
- <Cluster
13
- clusterItems={marker.clusterChildItem}
14
- distance={distance ? distance : 40}
15
- onPress={onPress}
16
- style={{ zIndex: marker.properties.point_count + 1 }}
17
- ></Cluster>
18
- );
19
- };
20
-
21
- export default memo(ClusterHarmony);
1
+ /*
2
+ * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
3
+ * Use of this source code is governed by a MIT license that can be
4
+ * found in the LICENSE file.
5
+ */
6
+
7
+ import React, { memo } from "react";
8
+ import { Cluster } from "react-native-maps";
9
+
10
+ const ClusterHarmony = ({ marker, distance, onPress }) => {
11
+ return (
12
+ <Cluster
13
+ clusterItems={marker.clusterChildItem}
14
+ distance={distance ? distance : 40}
15
+ onPress={onPress}
16
+ style={{ zIndex: marker.properties.point_count + 1 }}
17
+ ></Cluster>
18
+ );
19
+ };
20
+
21
+ export default memo(ClusterHarmony);