feat: implement binary search for optimal zoom level

Replace area-based formula with binary search to find zoom where
regions occupy 10-20% of magnifier area.

Binary search algorithm:
1. Start with minZoom=1, maxZoom=1000
2. Test midpoint zoom level
3. Calculate how much of magnified view each region occupies
4. If no regions fit → zoom out (maxZoom = mid)
5. If regions < 10% → zoom in (minZoom = mid)
6. If regions > 20% → zoom out (maxZoom = mid)
7. If 10-20% → perfect, done!
8. Iterate max 20 times or until range < 0.1

This handles all regions in the detection box, not just the one
under cursor, giving better overall framing.

For Gibraltar area:
- Binary search will find zoom ~800-1000x where Gibraltar occupies
  10-20% of magnifier
- Converges in ~10-15 iterations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-19 11:33:46 -06:00
parent 1a690e00b0
commit 1a54f09814
1 changed files with 81 additions and 32 deletions

View File

@ -870,43 +870,92 @@ export function MapRenderer({
} }
if (shouldShow) { if (shouldShow) {
// Calculate zoom to make the region under cursor occupy ~15% of magnifier area // Binary search for optimal zoom level
// Magnifier area is 50% of container width × 25% height (aspect 2:1) // Goal: Find zoom where regions fit nicely in magnifier (taking 10-20% of area)
// Target: region should occupy 15% of magnifier const TARGET_AREA_MIN = 0.10 // 10% of magnifier
let adaptiveZoom = 10 // Default zoom const TARGET_AREA_MAX = 0.20 // 20% of magnifier
if (regionUnderCursor && regionUnderCursorArea > 0) { // Get magnifier dimensions
// Get magnifier dimensions in screen pixels const magnifierWidth = containerRect.width * 0.5
const magnifierWidth = containerRect.width * 0.5 const magnifierHeight = magnifierWidth / 2
const magnifierHeight = magnifierWidth / 2 const magnifierArea = magnifierWidth * magnifierHeight
const magnifierArea = magnifierWidth * magnifierHeight
// Calculate zoom so region occupies 15% of magnifier area // Get SVG viewBox for coordinate conversion
// regionArea * zoom^2 = magnifierArea * 0.15 const viewBoxParts = mapData.viewBox.split(' ').map(Number)
// zoom = sqrt((magnifierArea * 0.15) / regionArea) const viewBoxWidth = viewBoxParts[2] || 1000
const targetAreaRatio = 0.15 const viewBoxHeight = viewBoxParts[3] || 1000
adaptiveZoom = Math.sqrt((magnifierArea * targetAreaRatio) / regionUnderCursorArea)
// Clamp zoom between reasonable bounds // Binary search bounds
const preClampZoom = adaptiveZoom let minZoom = 1
adaptiveZoom = Math.max(8, Math.min(MAX_ZOOM, adaptiveZoom)) let maxZoom = MAX_ZOOM
let adaptiveZoom = 10
let iterations = 0
const MAX_ITERATIONS = 20
// Debug logging for Gibraltar while (iterations < MAX_ITERATIONS && maxZoom - minZoom > 0.1) {
const hasGibraltar = regionUnderCursor === 'gi' iterations++
if (hasGibraltar) { const testZoom = (minZoom + maxZoom) / 2
console.log(`[Zoom] 🎯 GIBRALTAR BREAKDOWN:`, {
regionArea: `${regionUnderCursorArea.toFixed(6)}px²`, // Calculate magnified viewBox dimensions at this zoom
magnifierArea: `${magnifierArea.toFixed(0)}px²`, const magnifiedViewBoxWidth = viewBoxWidth / testZoom
targetRatio: `${(targetAreaRatio * 100).toFixed(0)}%`, const magnifiedViewBoxHeight = viewBoxHeight / testZoom
calculatedZoom: preClampZoom.toFixed(1), const magnifiedViewBoxArea = magnifiedViewBoxWidth * magnifiedViewBoxHeight
finalZoom: adaptiveZoom.toFixed(1),
hitMaxZoom: preClampZoom > MAX_ZOOM, // Check regions in detection box to see how they fit
}) let anyRegionFullyInside = false
let largestRegionRatio = 0
detectedRegions.forEach((regionId) => {
const region = mapData.regions.find((r) => r.id === regionId)
if (!region) return
const regionPath = svgRef.current?.querySelector(`path[data-region-id="${regionId}"]`)
if (!regionPath) return
const pathRect = regionPath.getBoundingClientRect()
const regionPixelArea = pathRect.width * pathRect.height
// Convert pixel area to viewBox area (approximate)
const scaleX = viewBoxWidth / svgRect.width
const scaleY = viewBoxHeight / svgRect.height
const regionViewBoxArea = regionPixelArea * scaleX * scaleY
// Check if region fits in magnified view
const regionRatioInMagnifier = regionViewBoxArea / magnifiedViewBoxArea
if (regionRatioInMagnifier < 1.0) {
anyRegionFullyInside = true
largestRegionRatio = Math.max(largestRegionRatio, regionRatioInMagnifier)
}
})
// Binary search logic
if (!anyRegionFullyInside) {
// No regions fit - zoom out
maxZoom = testZoom
} else if (largestRegionRatio < TARGET_AREA_MIN) {
// Regions too small - zoom in
minZoom = testZoom
} else if (largestRegionRatio > TARGET_AREA_MAX) {
// Regions too large - zoom out
maxZoom = testZoom
} else {
// Just right!
adaptiveZoom = testZoom
break
} }
} else {
// No region under cursor - use density-based zoom adaptiveZoom = testZoom
const countFactor = Math.min(regionsInBox / 10, 1) }
adaptiveZoom = 10 + countFactor * 20
// Debug logging for Gibraltar
const hasGibraltar = detectedRegions.includes('gi')
if (hasGibraltar) {
console.log(`[Zoom] 🎯 BINARY SEARCH RESULT:`, {
iterations,
finalZoom: adaptiveZoom.toFixed(1),
detectedRegions,
})
} }
// Calculate magnifier position (opposite corner from cursor) // Calculate magnifier position (opposite corner from cursor)