Skip to content

Commit 1e13867

Browse files
committed
fix: prevent edge bubbles flashing on mobile at high zoom
1 parent 6637c7b commit 1e13867

File tree

1 file changed

+51
-7
lines changed

1 file changed

+51
-7
lines changed

app/components/CountryBubbles.vue

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ interface CountryHotspot {
1111
center: { lat: number, lng: number }
1212
flagIcon: string
1313
zoom: number
14+
// Bounding box to detect if user is already viewing this country
15+
bounds: { north: number, south: number, east: number, west: number }
1416
}
1517
1618
const COUNTRY_HOTSPOTS: CountryHotspot[] = [
17-
{ code: 'SV', name: 'El Salvador', center: { lat: 13.7942, lng: -88.8965 }, flagIcon: 'flag:sv-4x3', zoom: 6.8 },
18-
{ code: 'CH', name: 'Switzerland', center: { lat: 46.8, lng: 8.2 }, flagIcon: 'flag:ch-1x1', zoom: 6.5 },
19+
{ code: 'SV', name: 'El Salvador', center: { lat: 13.7942, lng: -88.8965 }, flagIcon: 'flag:sv-4x3', zoom: 6.8, bounds: { north: 14.45, south: 13.15, east: -87.7, west: -90.13 } },
20+
{ code: 'CH', name: 'Switzerland', center: { lat: 46.8, lng: 8.2 }, flagIcon: 'flag:ch-1x1', zoom: 6.5, bounds: { north: 47.81, south: 45.82, east: 10.49, west: 5.96 } },
1921
]
2022
2123
const BUBBLE_PADDING = 8
2224
const BUBBLE_WIDTH = 160
2325
const BUBBLE_HEIGHT = 56
2426
25-
const { mapInstance, flyTo, viewCenter } = useMapControls()
27+
const { mapInstance, flyTo, viewCenter, zoom } = useMapControls()
2628
const { locationCount, clusterCount } = useVisibleLocations()
2729
const { width: windowWidth, height: windowHeight } = useWindowSize()
2830
@@ -39,8 +41,20 @@ const markers = new Map<CountryCode, Marker>()
3941
// Hide bubbles during fly animation
4042
const isFlying = ref(false)
4143
42-
// Show bubbles when nothing visible on map and not flying
43-
const showBubbles = computed(() => !isFlying.value && locationCount.value === 0 && clusterCount.value === 0)
44+
// At zoom 9+, individual location pins are visible - never show bubbles
45+
// This prevents race condition where queryRenderedFeatures returns 0 while tiles are loading on slow devices
46+
const MIN_ZOOM_FOR_PINS = 9
47+
48+
// Show bubbles when: not flying, zoomed out enough, and nothing visible
49+
const showBubbles = computed(() => {
50+
if (isFlying.value)
51+
return false
52+
// Primary gate: if zoomed in enough for pins to render, never show bubbles
53+
if (zoom.value >= MIN_ZOOM_FOR_PINS)
54+
return false
55+
// Secondary check: at low zoom, only show if no clusters visible
56+
return locationCount.value === 0 && clusterCount.value === 0
57+
})
4458
4559
// Check if a point is within current map viewport
4660
function isPointInViewport(point: { lat: number, lng: number }): boolean {
@@ -55,6 +69,35 @@ function isPointInViewport(point: { lat: number, lng: number }): boolean {
5569
}
5670
}
5771
72+
// Check if viewport significantly overlaps with country bounds (user is "within" the country)
73+
function isViewportWithinCountry(country: CountryHotspot): boolean {
74+
if (!mapInstance.value)
75+
return false
76+
try {
77+
const vp = mapInstance.value.getBounds()
78+
const cb = country.bounds
79+
80+
// Calculate intersection area
81+
const intersectNorth = Math.min(vp.getNorth(), cb.north)
82+
const intersectSouth = Math.max(vp.getSouth(), cb.south)
83+
const intersectEast = Math.min(vp.getEast(), cb.east)
84+
const intersectWest = Math.max(vp.getWest(), cb.west)
85+
86+
// No intersection
87+
if (intersectNorth <= intersectSouth || intersectEast <= intersectWest)
88+
return false
89+
90+
const intersectArea = (intersectNorth - intersectSouth) * (intersectEast - intersectWest)
91+
const vpArea = (vp.getNorth() - vp.getSouth()) * (vp.getEast() - vp.getWest())
92+
93+
// If >50% of viewport is within country bounds, user is "in" the country
94+
return vpArea > 0 && (intersectArea / vpArea) > 0.5
95+
}
96+
catch {
97+
return false
98+
}
99+
}
100+
58101
// Calculate rhumb line bearing (straight line on Mercator projection)
59102
function calculateMercatorBearing(from: { lat: number, lng: number }, to: { lat: number, lng: number }): number {
60103
const toRad = Math.PI / 180
@@ -106,6 +149,7 @@ interface Bubble extends CountryHotspot {
106149
}
107150
108151
// Compute bubbles - either at country center (in view) or at edge (out of view)
152+
// Skip countries where user is already within the country's bounds
109153
const bubbles = computed<Bubble[]>(() => {
110154
if (!showBubbles.value || !mapInstance.value)
111155
return []
@@ -115,7 +159,7 @@ const bubbles = computed<Bubble[]>(() => {
115159
if (!vpWidth || !vpHeight)
116160
return []
117161
118-
return COUNTRY_HOTSPOTS.map((country) => {
162+
return COUNTRY_HOTSPOTS.filter(country => !isViewportWithinCountry(country)).map((country) => {
119163
const count = countryCounts.value?.[country.code] || null
120164
const inView = isPointInViewport(country.center)
121165
@@ -263,7 +307,7 @@ function flyToCountry(country: CountryHotspot) {
263307
}
264308
markers.clear()
265309
266-
flyTo(country.center, country.zoom)
310+
flyTo(country.center, { zoom: country.zoom })
267311
268312
// Reset after fly animation completes + delay for clusters to load
269313
mapInstance.value?.once('moveend', () => {

0 commit comments

Comments
 (0)