Skip to content

Commit 8b756a7

Browse files
vicbsemantic-release-botamuramoto
authored
fix: reduce flickering (#660)
* chore(release): 2.2.0 [skip ci] ## [2.2.0](v2.1.4...v2.2.0) (2023-06-01) ### Features * add a dev server ([#656](#656)) ([b025dec](b025dec)) ### Code Refactoring * **distanceBetweenPoints:** optimize ([#646](#646)) ([e0f9a11](e0f9a11)) ### Miscellaneous Chores * **deps-dev:** bump @babel/preset-env from 7.21.5 to 7.22.4 ([#650](#650)) ([982730d](982730d)) * **deps-dev:** bump @babel/runtime-corejs3 from 7.21.5 to 7.22.3 ([#649](#649)) ([1c9f4fb](1c9f4fb)) * **deps-dev:** bump @types/selenium-webdriver from 4.1.14 to 4.1.15 ([#655](#655)) ([526e65c](526e65c)) * **deps-dev:** bump @typescript-eslint/eslint-plugin ([#654](#654)) ([68a94db](68a94db)) * **deps-dev:** bump @typescript-eslint/parser from 5.59.7 to 5.59.8 ([#653](#653)) ([a46d0a7](a46d0a7)) * **deps-dev:** bump geckodriver from 4.0.0 to 4.0.2 ([#652](#652)) ([19518a4](19518a4)) * update actions to use Node.js 16 ([#659](#659)) ([bcbdddf](bcbdddf)) * fix: reduce flickering * Individual markers Before this PR all the individual markers were removed from the map during the render cycle by calling the reset method and to maybe be added back later in renderClusters. After this CL the individual markers that will still be displayed on the map after the render cycle are not removed from the map. * Group markers This PR still renders the group markers each time but the former group markers are only removed from the map after the new one are added. It turns out that removing the group markers in the next raf really help with flickering. *  chore: resolves merge conflicts on to beta --------- Co-authored-by: semantic-release-bot <[email protected]> Co-authored-by: Alex Muramoto <[email protected]>
1 parent 90a62ee commit 8b756a7

File tree

2 files changed

+152
-7
lines changed

2 files changed

+152
-7
lines changed

src/markerclusterer.test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,23 @@ describe.each(markerClasses)(
4040
const renderer = { render };
4141

4242
let map: google.maps.Map;
43+
let rafSpy: jest.SpyInstance;
4344

4445
beforeEach(() => {
4546
map = new google.maps.Map(document.createElement("div"));
47+
// Runs the raf callback immediately.
48+
rafSpy = jest
49+
.spyOn(window, "requestAnimationFrame")
50+
.mockImplementation((cb) => {
51+
cb(performance.now());
52+
return 0;
53+
});
4654
});
4755

4856
afterEach(() => {
4957
calculate.mockClear();
5058
render.mockClear();
59+
rafSpy.mockRestore();
5160
});
5261

5362
test("markerClusterer does not render if no map", () => {
@@ -81,7 +90,7 @@ describe.each(markerClasses)(
8190
markerClusterer.render();
8291

8392
expect(calculate).toBeCalledWith({ map, markers, mapCanvasProjection });
84-
expect(markerClusterer["reset"]).toHaveBeenCalledTimes(1);
93+
expect(markerClusterer["reset"]).toHaveBeenCalledTimes(0);
8594
expect(markerClusterer["renderClusters"]).toHaveBeenCalledTimes(1);
8695
});
8796

@@ -125,6 +134,86 @@ describe.each(markerClasses)(
125134
expect(deleteSpy).toHaveBeenCalledTimes(1);
126135
});
127136

137+
test("markerClusterer render should not remove markers from the map if they were already rendered", () => {
138+
const marker = new markerClass();
139+
const markers: Marker[] = [marker];
140+
141+
const algorithm = {
142+
calculate: jest.fn().mockReturnValue({
143+
clusters: [new Cluster({ markers })],
144+
changed: true,
145+
}),
146+
};
147+
const markerClusterer = new MarkerClusterer({
148+
markers,
149+
algorithm,
150+
});
151+
markerClusterer.getMap = jest.fn().mockImplementation(() => map);
152+
markerClusterer.getProjection = jest
153+
.fn()
154+
.mockImplementation(() => jest.fn());
155+
markerClusterer["renderClusters"] = jest.fn();
156+
markerClusterer["clusters"] = [new Cluster({ markers })];
157+
158+
MarkerUtils.setMap = jest.fn().mockImplementation(() => null);
159+
160+
markerClusterer["render"]();
161+
162+
expect(MarkerUtils.setMap).toHaveBeenCalledTimes(0);
163+
});
164+
165+
test("markerClusterer render should remove markers from the map if they are no more rendered", () => {
166+
const marker = new markerClass();
167+
const markers: Marker[] = [marker];
168+
169+
const algorithm = {
170+
calculate: jest.fn().mockReturnValue({ clusters: [], changed: true }),
171+
};
172+
const markerClusterer = new MarkerClusterer({
173+
markers,
174+
algorithm,
175+
});
176+
markerClusterer.getMap = jest.fn().mockImplementation(() => map);
177+
markerClusterer.getProjection = jest
178+
.fn()
179+
.mockImplementation(() => jest.fn());
180+
markerClusterer["renderClusters"] = jest.fn();
181+
const cluster = new Cluster({ markers });
182+
cluster.marker = marker;
183+
markerClusterer["clusters"] = [cluster];
184+
185+
MarkerUtils.setMap = jest.fn().mockImplementation(() => null);
186+
187+
markerClusterer["render"]();
188+
189+
expect(MarkerUtils.setMap).toHaveBeenCalledWith(marker, null);
190+
});
191+
192+
test("markerClusterer render should remove all group cluster markers from the map", () => {
193+
const markers: Marker[] = [new markerClass(), new markerClass()];
194+
const algorithm = {
195+
calculate: jest.fn().mockReturnValue({ clusters: [], changed: true }),
196+
};
197+
const markerClusterer = new MarkerClusterer({
198+
markers,
199+
algorithm,
200+
});
201+
markerClusterer.getMap = jest.fn().mockImplementation(() => map);
202+
markerClusterer.getProjection = jest
203+
.fn()
204+
.mockImplementation(() => jest.fn());
205+
markerClusterer["renderClusters"] = jest.fn();
206+
const cluster = new Cluster({ markers });
207+
cluster.marker = new markerClass();
208+
markerClusterer["clusters"] = [cluster];
209+
210+
MarkerUtils.setMap = jest.fn().mockImplementation(() => null);
211+
212+
markerClusterer["render"]();
213+
214+
expect(MarkerUtils.setMap).toHaveBeenCalledWith(cluster.marker, null);
215+
});
216+
128217
test("markerClusterer renderClusters bypasses renderer if just one", () => {
129218
const markers: Marker[] = [new markerClass()];
130219

@@ -174,6 +263,28 @@ describe.each(markerClasses)(
174263
);
175264
});
176265

266+
test("markerClusterer renderClusters remove all individual markers from the map", () => {
267+
const marker1 = new markerClass();
268+
const marker2 = new markerClass();
269+
const markers: Marker[] = [marker1, marker2];
270+
271+
const markerClusterer = new MarkerClusterer({
272+
markers,
273+
renderer,
274+
});
275+
276+
MarkerUtils.setMap = jest.fn();
277+
markerClusterer.getMap = jest.fn().mockImplementation(() => map);
278+
279+
const clusters = [new Cluster({ markers })];
280+
281+
markerClusterer["clusters"] = clusters;
282+
markerClusterer["renderClusters"]();
283+
284+
expect(MarkerUtils.setMap).toBeCalledWith(marker1, null);
285+
expect(MarkerUtils.setMap).toBeCalledWith(marker2, null);
286+
});
287+
177288
test("markerClusterer renderClusters does not set click handler", () => {
178289
const markers: Marker[] = [new markerClass()];
179290

src/markerclusterer.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,44 @@ export class MarkerClusterer extends OverlayViewSafe {
178178
mapCanvasProjection: this.getProjection(),
179179
});
180180

181-
// allow algorithms to return flag on whether the clusters/markers have changed
181+
// Allow algorithms to return flag on whether the clusters/markers have changed.
182182
if (changed || changed == undefined) {
183-
// reset visibility of markers and clusters
184-
this.reset();
185-
// store new clusters
186-
this.clusters = clusters;
183+
// Accumulate the markers of the clusters composed of a single marker.
184+
// Those clusters directly use the marker.
185+
// Clusters with more than one markers use a group marker generated by a renderer.
186+
const singleMarker = new Set<Marker>();
187+
for (const cluster of clusters) {
188+
if (cluster.markers.length == 1) {
189+
singleMarker.add(cluster.markers[0]);
190+
}
191+
}
192+
193+
const groupMarkers: Marker[] = [];
194+
// Iterate the clusters that are currently rendered.
195+
for (const cluster of this.clusters) {
196+
if (cluster.marker == null) {
197+
continue;
198+
}
199+
if (cluster.markers.length == 1) {
200+
if (!singleMarker.has(cluster.marker)) {
201+
// The marker:
202+
// - was previously rendered because it is from a cluster with 1 marker,
203+
// - should no more be rendered as it is not in singleMarker.
204+
MarkerUtils.setMap(cluster.marker, null);
205+
}
206+
} else {
207+
// Delay the removal of old group markers to avoid flickering.
208+
groupMarkers.push(cluster.marker);
209+
}
210+
}
187211

212+
this.clusters = clusters;
188213
this.renderClusters();
214+
215+
// Delayed removal of the markers of the former groups.
216+
requestAnimationFrame(() =>
217+
groupMarkers.forEach((marker) => MarkerUtils.setMap(marker, null))
218+
);
189219
}
190220
google.maps.event.trigger(
191221
this,
@@ -215,14 +245,18 @@ export class MarkerClusterer extends OverlayViewSafe {
215245
}
216246

217247
protected renderClusters(): void {
218-
// generate stats to pass to renderers
248+
// Generate stats to pass to renderers.
219249
const stats = new ClusterStats(this.markers, this.clusters);
220250
const map = this.getMap() as google.maps.Map;
251+
221252
this.clusters.forEach((cluster) => {
222253
if (cluster.markers.length === 1) {
223254
cluster.marker = cluster.markers[0];
224255
} else {
256+
// Generate the marker to represent the group.
225257
cluster.marker = this.renderer.render(cluster, stats, map);
258+
// Make sure all individual markers are removed from the map.
259+
cluster.markers.forEach((marker) => MarkerUtils.setMap(marker, null));
226260
if (this.onClusterClick) {
227261
cluster.marker.addListener(
228262
"click",

0 commit comments

Comments
 (0)