Skip to content

Commit e509696

Browse files
authored
Map Marker Clustering (#719)
* Added map marker clustering, manual + automatic clustering * Added clustering to example * Added tests for map view components * Add bootstrap ci step * Revert "Add bootstrap ci step" This reverts commit 97f3200. * Moves test to after build in ci * Implement suggestions
1 parent f1c5bc0 commit e509696

File tree

15 files changed

+871
-38
lines changed

15 files changed

+871
-38
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ jobs:
3131
- name: Lint
3232
run: yarn lint
3333

34-
- name: Test
35-
run: yarn test
36-
3734
- name: Build
3835
run: yarn build
3936

37+
- name: Test
38+
run: yarn test
39+
4040
- name: Release next packages
4141
if: github.event_name == 'pull_request'
4242
env:

example/src/MapViewExample.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import React from "react";
2-
import { MapView, MapMarker, MapCallout } from "@draftbit/maps";
2+
import {
3+
MapView,
4+
MapMarker,
5+
MapCallout,
6+
MapMarkerCluster,
7+
MapMarkerView,
8+
} from "@draftbit/maps";
39
import { ButtonSolid, withTheme } from "@draftbit/ui";
410
import { StyleSheet, Text, View } from "react-native";
11+
import { MapMarkerClusterView } from "@draftbit/maps";
512

613
const MapViewExample = ({ theme }) => {
714
const mapRef = React.createRef();
@@ -12,7 +19,8 @@ const MapViewExample = ({ theme }) => {
1219
ref={mapRef}
1320
showsCompass={true}
1421
style={styles.map}
15-
latitude={40.741895}
22+
latitude={43.741895}
23+
autoClusterMarkers
1624
longitude={-73.989308}
1725
zoom={16}
1826
>
@@ -32,6 +40,23 @@ const MapViewExample = ({ theme }) => {
3240
<Text>With Callout</Text>
3341
</MapCallout>
3442
</MapMarker>
43+
44+
<MapMarker
45+
latitude={43.741895}
46+
longitude={-73.989308}
47+
pinColor={theme.colors.primary}
48+
title="Draftbit"
49+
description="A simple MapView example"
50+
/>
51+
<MapMarker
52+
latitude={43.741895}
53+
longitude={-73.979308}
54+
pinColor={theme.colors.secondary}
55+
>
56+
<MapCallout showTooltip>
57+
<Text>With Callout</Text>
58+
</MapCallout>
59+
</MapMarker>
3560
</MapView>
3661
<ButtonSolid
3762
title="Zoom to Chicago"

packages/maps/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
},
4040
"homepage": "https://github.com/draftbit/react-native-jigsaw#readme",
4141
"dependencies": {
42+
"@draftbit/ui": "48.2.3",
4243
"@teovilla/react-native-web-maps": "^0.9.1",
4344
"react-native-maps": "1.3.2"
4445
},
@@ -47,13 +48,17 @@
4748
"lib/"
4849
],
4950
"jest": {
50-
"preset": "react-native",
51+
"preset": "jest-expo",
5152
"setupFilesAfterEnv": [
5253
"@testing-library/jest-native/extend-expect"
5354
],
5455
"testPathIgnorePatterns": [
5556
"lib",
5657
"__mocks__"
57-
]
58+
],
59+
"transformIgnorePatterns": [
60+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
61+
],
62+
"testEnvironment": "node"
5863
}
5964
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from "react";
2+
import { View } from "react-native";
3+
import { render, screen } from "@testing-library/react-native";
4+
import MapView from "../components/MapView";
5+
import MapMarker from "../components/MapMarker";
6+
import MapCallout from "../components/MapCallout";
7+
import { Callout as MapCalloutComponent } from "../components/react-native-maps";
8+
9+
jest.mock("../components/react-native-maps", () => {
10+
const React = require("react");
11+
12+
class MapView extends React.Component {
13+
render(): React.ReactNode {
14+
return <>{this.props.children}</>;
15+
}
16+
}
17+
18+
class Marker extends React.Component {
19+
render(): React.ReactNode {
20+
return <>{this.props.children}</>;
21+
}
22+
}
23+
24+
const Callout = (props: any) => {
25+
return <>{props.children}</>;
26+
};
27+
28+
return {
29+
__esModule: true,
30+
default: MapView,
31+
Marker,
32+
Callout,
33+
};
34+
});
35+
36+
beforeEach(() => {
37+
jest.clearAllMocks();
38+
});
39+
40+
describe("MapMarker tests", () => {
41+
test("should render default MapCallout when title and description provided", () => {
42+
const title = "Title";
43+
const description = "Some description";
44+
45+
render(
46+
<MapView apiKey="">
47+
<MapMarker
48+
latitude={43.741895}
49+
longitude={-73.989308}
50+
title={title}
51+
description={description}
52+
/>
53+
</MapView>
54+
);
55+
56+
const callout = screen.UNSAFE_queryByType(MapCalloutComponent);
57+
const titleText = screen.queryByText(title);
58+
const descriptionText = screen.queryByText(description);
59+
60+
expect(callout).toBeTruthy();
61+
expect(titleText).toBeTruthy();
62+
expect(descriptionText).toBeTruthy();
63+
});
64+
65+
test("should render MapCallout children as a callout", () => {
66+
render(
67+
<MapView apiKey="">
68+
<MapMarker latitude={43.741895} longitude={-73.989308}>
69+
<MapCallout>
70+
<View testID="callout-view" />
71+
</MapCallout>
72+
</MapMarker>
73+
</MapView>
74+
);
75+
76+
const callout = screen.UNSAFE_queryByType(MapCalloutComponent);
77+
const calloutView = screen.queryByTestId("callout-view");
78+
79+
expect(callout).toBeTruthy();
80+
expect(calloutView).toBeTruthy();
81+
});
82+
83+
test("should render non MapCallout children as a custom marker", () => {
84+
render(
85+
<MapView apiKey="">
86+
<MapMarker latitude={43.741895} longitude={-73.989308}>
87+
<View testID="custom-marker" />
88+
</MapMarker>
89+
</MapView>
90+
);
91+
92+
const callout = screen.UNSAFE_queryByType(MapCalloutComponent);
93+
const customMarker = screen.queryByTestId("custom-marker");
94+
95+
expect(callout).toBeFalsy();
96+
expect(customMarker).toBeTruthy();
97+
});
98+
99+
test("should render Image as a custom marker when pinImage is provided", () => {
100+
render(
101+
<MapView apiKey="">
102+
<MapMarker
103+
latitude={43.741895}
104+
longitude={-73.989308}
105+
pinImage={"image"}
106+
/>
107+
</MapView>
108+
);
109+
110+
const markerPinImage = screen.queryByTestId("map-marker-pin-image");
111+
112+
expect(markerPinImage).toBeTruthy();
113+
});
114+
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from "react";
2+
import { Text } from "react-native";
3+
import { render, screen } from "@testing-library/react-native";
4+
import MapMarker from "../components/MapMarker";
5+
import {
6+
MapMarkerCluster,
7+
MapMarkerClusterView,
8+
} from "../components/marker-cluster";
9+
import { Marker as MapMarkerComponent } from "../components/react-native-maps";
10+
11+
jest.mock("../components/react-native-maps", () => {
12+
const React = require("react");
13+
14+
class MapView extends React.Component {
15+
render(): React.ReactNode {
16+
return <>{this.props.children}</>;
17+
}
18+
}
19+
20+
const Marker = (props: any) => {
21+
return <>{props.children}</>;
22+
};
23+
24+
return {
25+
__esModule: true,
26+
default: MapView,
27+
Marker,
28+
};
29+
});
30+
31+
jest.mock("@teovilla/react-native-web-maps", () => {
32+
const React = require("react");
33+
34+
class MarkerClusterer extends React.Component {
35+
render(): React.ReactNode {
36+
return (
37+
<>
38+
{this.props.renderCluster?.({
39+
pointCount: React.Children.toArray(this.props.children).length,
40+
coordinate: { latitude: 0, longitude: 0 },
41+
expansionZoom: 4,
42+
})}
43+
{this.props.children}
44+
</>
45+
);
46+
}
47+
}
48+
49+
return {
50+
MarkerClusterer,
51+
};
52+
});
53+
54+
beforeEach(() => {
55+
jest.clearAllMocks();
56+
});
57+
58+
describe("MapMarkerCluster tests", () => {
59+
test("should render markers provided as children", () => {
60+
render(
61+
<MapMarkerCluster>
62+
<MapMarker latitude={41.741895} longitude={-73.989308} />
63+
<MapMarker latitude={42.741895} longitude={-73.989308} />
64+
<MapMarker latitude={43.741895} longitude={-73.989308} />
65+
<MapMarker latitude={44.741895} longitude={-73.989308} />
66+
</MapMarkerCluster>
67+
);
68+
69+
const renderedMarkers = screen.UNSAFE_queryAllByType(MapMarkerComponent);
70+
71+
expect(renderedMarkers.length).toBe(4 + 1); //4 clustered markers, cluster itself is also rendered as it's own marker (reasoning behind the +1)
72+
});
73+
74+
test("should render default cluster view when none provided", () => {
75+
render(
76+
<MapMarkerCluster>
77+
<MapMarker latitude={41.741895} longitude={-73.989308} />
78+
</MapMarkerCluster>
79+
);
80+
81+
const defaultClusterView = screen.queryByTestId(
82+
"default-map-marker-cluster-view"
83+
);
84+
85+
expect(defaultClusterView).toBeTruthy();
86+
});
87+
88+
test("should render custom cluster view when provided", () => {
89+
render(
90+
<MapMarkerCluster>
91+
<MapMarkerClusterView
92+
renderItem={({ markerCount }) => (
93+
<Text testID="custom-cluster-view">{markerCount}</Text>
94+
)}
95+
/>
96+
<MapMarker latitude={41.741895} longitude={-73.989308} />
97+
<MapMarker latitude={42.741895} longitude={-73.989308} />
98+
<MapMarker latitude={43.741895} longitude={-73.989308} />
99+
</MapMarkerCluster>
100+
);
101+
102+
const customClusterView = screen.queryByTestId("custom-cluster-view");
103+
104+
expect(customClusterView).toBeTruthy();
105+
expect(customClusterView?.props).toMatchInlineSnapshot(`
106+
{
107+
"children": 3,
108+
"testID": "custom-cluster-view",
109+
}
110+
`);
111+
});
112+
});

0 commit comments

Comments
 (0)