@@ -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
1618const 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
2123const BUBBLE_PADDING = 8
2224const BUBBLE_WIDTH = 160
2325const BUBBLE_HEIGHT = 56
2426
25- const { mapInstance, flyTo, viewCenter } = useMapControls ()
27+ const { mapInstance, flyTo, viewCenter, zoom } = useMapControls ()
2628const { locationCount, clusterCount } = useVisibleLocations ()
2729const { width : windowWidth, height : windowHeight } = useWindowSize ()
2830
@@ -39,8 +41,20 @@ const markers = new Map<CountryCode, Marker>()
3941// Hide bubbles during fly animation
4042const 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
4660function 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)
59102function 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
109153const 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