Skip to content

fix: optimize SuperClusterViewportAlgorithm rendering and eliminate flickering #1016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 232 additions & 36 deletions src/algorithms/superviewport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { initialize, MapCanvasProjection } from "@googlemaps/jest-mocks";
import { Marker } from "../marker-utils";
import { ClusterFeature } from "supercluster";
import { Cluster } from "../cluster";

initialize();
const markerClasses = [
Expand All @@ -29,13 +30,28 @@
"SuperCluster works with legacy and Advanced Markers",
(markerClass) => {
let map: google.maps.Map;
let mapCanvasProjection: MapCanvasProjection;

beforeEach(() => {
map = new google.maps.Map(document.createElement("div"));
mapCanvasProjection = new MapCanvasProjection();

mapCanvasProjection.fromLatLngToDivPixel = jest
.fn()
.mockImplementation((latLng: google.maps.LatLng) => ({
x: latLng.lng() * 100,
y: latLng.lat() * 100,
}));

mapCanvasProjection.fromDivPixelToLatLng = jest
.fn()
.mockImplementation((point: google.maps.Point) => ({
lat: () => point.y / 100,
lng: () => point.x / 100,
}));
});

test("should only call load if markers change", () => {
const mapCanvasProjection = new MapCanvasProjection();
const markers: Marker[] = [new markerClass()];

const superCluster = new SuperClusterViewportAlgorithm({});
Expand Down Expand Up @@ -63,25 +79,31 @@
});

test("should cluster markers", () => {
const mapCanvasProjection = new MapCanvasProjection();
const markers: Marker[] = [new markerClass(), new markerClass()];

const superCluster = new SuperClusterViewportAlgorithm({});
map.getZoom = jest.fn().mockReturnValue(0);

const northEast = {
lat: jest.fn().mockReturnValue(-3),
lng: jest.fn().mockReturnValue(34),
};
const southWest = {
lat: jest.fn().mockReturnValue(29),
lng: jest.fn().mockReturnValue(103),
};

map.getBounds = jest.fn().mockReturnValue({
toJSON: () => ({
west: -180,
south: -90,
east: 180,
north: 90,
}),
getNorthEast: jest
.fn()
.mockReturnValue({ getLat: () => -3, getLng: () => 34 }),
getSouthWest: jest
.fn()
.mockReturnValue({ getLat: () => 29, getLng: () => 103 }),
getNorthEast: jest.fn().mockReturnValue(northEast),
getSouthWest: jest.fn().mockReturnValue(southWest),
});

const { clusters } = superCluster.calculate({
markers,
map,
Expand All @@ -106,7 +128,6 @@
},
};

// mock out the supercluster implementation
jest
.spyOn(superCluster["superCluster"], "getLeaves")
.mockImplementation(() => [clusterFeature]);
Expand All @@ -117,7 +138,6 @@
});

test("should not cluster if zoom didn't change", () => {
const mapCanvasProjection = new MapCanvasProjection();
const markers: Marker[] = [new markerClass(), new markerClass()];

const superCluster = new SuperClusterViewportAlgorithm({});
Expand All @@ -134,12 +154,11 @@
mapCanvasProjection,
});

expect(changed).toBeTruthy();
expect(changed).toBeFalsy();
expect(clusters).toBe(superCluster["clusters"]);
});

test("should not cluster if zoom beyond maxZoom", () => {
const mapCanvasProjection = new MapCanvasProjection();
const markers: Marker[] = [new markerClass(), new markerClass()];

const superCluster = new SuperClusterViewportAlgorithm({});
Expand All @@ -150,40 +169,46 @@

map.getZoom = jest.fn().mockReturnValue(superCluster["state"].zoom + 1);

const northEast = {
lat: jest.fn().mockReturnValue(0),
lng: jest.fn().mockReturnValue(0),
};
const southWest = {
lat: jest.fn().mockReturnValue(0),
lng: jest.fn().mockReturnValue(0),
};
map.getBounds = jest.fn().mockReturnValue({
getNorthEast: jest.fn().mockReturnValue(northEast),
getSouthWest: jest.fn().mockReturnValue(southWest),
});

const { clusters, changed } = superCluster.calculate({
markers,
map,
mapCanvasProjection,
});

expect(changed).toBeTruthy();
expect(changed).toBeFalsy();
expect(clusters).toBe(superCluster["clusters"]);
expect(superCluster["state"]).toEqual({ zoom: 21, view: [0, 0, 0, 0] });
expect(superCluster["state"].zoom).toBe(21);
expect(Array.isArray(superCluster["state"].view)).toBeTruthy();
});

test("should round fractional zoom", () => {
const mapCanvasProjection = new MapCanvasProjection();
const markers: Marker[] = [new markerClass(), new markerClass()];
mapCanvasProjection.fromLatLngToDivPixel = jest
.fn()
.mockImplementation((b: google.maps.LatLng) => ({
x: b.lat() * 100,
y: b.lng() * 100,
}));
mapCanvasProjection.fromDivPixelToLatLng = jest
.fn()
.mockImplementation(
(p: google.maps.Point) =>
new google.maps.LatLng({ lat: p.x / 100, lng: p.y / 100 })
);

const northEast = {
lat: jest.fn().mockReturnValue(-3),
lng: jest.fn().mockReturnValue(34),
};
const southWest = {
lat: jest.fn().mockReturnValue(29),
lng: jest.fn().mockReturnValue(103),
};

map.getBounds = jest.fn().mockReturnValue({
getNorthEast: jest
.fn()
.mockReturnValue({ lat: () => -3, lng: () => 34 }),
getSouthWest: jest
.fn()
.mockReturnValue({ lat: () => 29, lng: () => 103 }),
getNorthEast: jest.fn().mockReturnValue(northEast),
getSouthWest: jest.fn().mockReturnValue(southWest),
});

const superCluster = new SuperClusterViewportAlgorithm({});
Expand All @@ -195,19 +220,190 @@
map.getZoom = jest.fn().mockReturnValue(1.534);
expect(
superCluster.calculate({ markers, map, mapCanvasProjection })
).toEqual({ changed: true, clusters: [] });
).toEqual({ changed: false, clusters: [] });

expect(superCluster["superCluster"].getClusters).toHaveBeenCalledWith(
[0, 0, 0, 0],
expect.any(Array),
2
);

map.getZoom = jest.fn().mockReturnValue(3.234);
superCluster.calculate({ markers, map, mapCanvasProjection });
expect(superCluster["superCluster"].getClusters).toHaveBeenCalledWith(
[0, 0, 0, 0],
expect.any(Array),
3
);
});

test("should return changed=false when viewport changes but clusters remain the same", () => {
const markers: Marker[] = [new markerClass(), new markerClass()];

map.getZoom = jest.fn().mockReturnValue(10);

const initialNorthEast = {
lat: jest.fn().mockReturnValue(10),
lng: jest.fn().mockReturnValue(10),
};
const initialSouthWest = {
lat: jest.fn().mockReturnValue(0),
lng: jest.fn().mockReturnValue(0),
};
const initialBounds = {
getNorthEast: jest.fn().mockReturnValue(initialNorthEast),
getSouthWest: jest.fn().mockReturnValue(initialSouthWest),
};
map.getBounds = jest.fn().mockReturnValue(initialBounds);

const algorithm = new SuperClusterViewportAlgorithm({
viewportPadding: 60,
});

const sameCluster = [
new Cluster({
markers: markers,
position: { lat: 5, lng: 5 },
}),
];

algorithm.cluster = jest.fn().mockReturnValue(sameCluster);

const firstResult = algorithm.calculate({
markers,
map,
mapCanvasProjection,
});

expect(firstResult.changed).toBeTruthy();

const newNorthEast = {
lat: jest.fn().mockReturnValue(15),
lng: jest.fn().mockReturnValue(15),
};
const newSouthWest = {
lat: jest.fn().mockReturnValue(5),
lng: jest.fn().mockReturnValue(5),
};
const newBounds = {
getNorthEast: jest.fn().mockReturnValue(newNorthEast),
getSouthWest: jest.fn().mockReturnValue(newSouthWest),
};
map.getBounds = jest.fn().mockReturnValue(newBounds);

const secondResult = algorithm.calculate({
markers,
map,
mapCanvasProjection,
});

expect(secondResult.changed).toBeFalsy();
expect(secondResult.clusters).toEqual(sameCluster);
});

test("should detect cluster changes accurately with areClusterArraysEqual", () => {
const markers: Marker[] = [new markerClass(), new markerClass()];

map.getZoom = jest.fn().mockReturnValue(10);
map.getBounds = jest.fn().mockReturnValue({
getNorthEast: jest
.fn()
.mockReturnValue({ lat: () => 10, lng: () => 10 }),
getSouthWest: jest.fn().mockReturnValue({ lat: () => 0, lng: () => 0 }),
});

const algorithm = new SuperClusterViewportAlgorithm({});

const cluster1 = [
new Cluster({
markers: [markers[0]],
position: { lat: 2, lng: 2 },
}),
new Cluster({
markers: [markers[1]],
position: { lat: 8, lng: 8 },
}),
];

algorithm.cluster = jest.fn().mockReturnValueOnce(cluster1);

const result1 = algorithm.calculate({
markers,
map,
mapCanvasProjection,
});
expect(result1.changed).toBeTruthy();

const cluster2 = [
new Cluster({
markers: [markers[0]],
position: { lat: 2, lng: 2 },
}),
new Cluster({
markers: [markers[1]],
position: { lat: 8, lng: 8 },
}),
];

algorithm.cluster = jest.fn().mockReturnValueOnce(cluster2);

const result2 = algorithm.calculate({
markers,
map,
mapCanvasProjection,
});
expect(result2.changed).toBeFalsy();

const cluster3 = [
new Cluster({
markers: markers,
position: { lat: 5, lng: 5 },
}),
];

algorithm.cluster = jest.fn().mockReturnValueOnce(cluster3);

const result3 = algorithm.calculate({
markers,
map,
mapCanvasProjection,
});
expect(result3.changed).toBeTruthy();
});

test("should correctly calculate viewport state with getPaddedViewport", () => {
const markers: Marker[] = [new markerClass()];

map.getZoom = jest.fn().mockReturnValue(10);

const northEast = {
lat: jest.fn().mockReturnValue(10),
lng: jest.fn().mockReturnValue(20),
};
const southWest = {
lat: jest.fn().mockReturnValue(0),
lng: jest.fn().mockReturnValue(10),
};

const bounds = {
getNorthEast: jest.fn().mockReturnValue(northEast),
getSouthWest: jest.fn().mockReturnValue(southWest),
};
map.getBounds = jest.fn().mockReturnValue(bounds);

const algorithm = new SuperClusterViewportAlgorithm({
viewportPadding: 60,
});
algorithm.cluster = jest.fn().mockReturnValue([]);

const result = algorithm.calculate({

Check failure on line 397 in src/algorithms/superviewport.test.ts

View workflow job for this annotation

GitHub Actions / test

'result' is assigned a value but never used

Check failure on line 397 in src/algorithms/superviewport.test.ts

View workflow job for this annotation

GitHub Actions / test / test

'result' is assigned a value but never used
markers,
map,
mapCanvasProjection,
});

const state = algorithm["state"];

expect(state.view).toEqual([0, 0, 0, 0]);
expect(state.zoom).toBe(10);
});
}
);
Loading
Loading