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:
parent
1a690e00b0
commit
1a54f09814
|
|
@ -870,43 +870,92 @@ export function MapRenderer({
|
|||
}
|
||||
|
||||
if (shouldShow) {
|
||||
// Calculate zoom to make the region under cursor occupy ~15% of magnifier area
|
||||
// Magnifier area is 50% of container width × 25% height (aspect 2:1)
|
||||
// Target: region should occupy 15% of magnifier
|
||||
let adaptiveZoom = 10 // Default zoom
|
||||
// Binary search for optimal zoom level
|
||||
// Goal: Find zoom where regions fit nicely in magnifier (taking 10-20% of area)
|
||||
const TARGET_AREA_MIN = 0.10 // 10% of magnifier
|
||||
const TARGET_AREA_MAX = 0.20 // 20% of magnifier
|
||||
|
||||
if (regionUnderCursor && regionUnderCursorArea > 0) {
|
||||
// Get magnifier dimensions in screen pixels
|
||||
const magnifierWidth = containerRect.width * 0.5
|
||||
const magnifierHeight = magnifierWidth / 2
|
||||
const magnifierArea = magnifierWidth * magnifierHeight
|
||||
// Get magnifier dimensions
|
||||
const magnifierWidth = containerRect.width * 0.5
|
||||
const magnifierHeight = magnifierWidth / 2
|
||||
const magnifierArea = magnifierWidth * magnifierHeight
|
||||
|
||||
// Calculate zoom so region occupies 15% of magnifier area
|
||||
// regionArea * zoom^2 = magnifierArea * 0.15
|
||||
// zoom = sqrt((magnifierArea * 0.15) / regionArea)
|
||||
const targetAreaRatio = 0.15
|
||||
adaptiveZoom = Math.sqrt((magnifierArea * targetAreaRatio) / regionUnderCursorArea)
|
||||
// Get SVG viewBox for coordinate conversion
|
||||
const viewBoxParts = mapData.viewBox.split(' ').map(Number)
|
||||
const viewBoxWidth = viewBoxParts[2] || 1000
|
||||
const viewBoxHeight = viewBoxParts[3] || 1000
|
||||
|
||||
// Clamp zoom between reasonable bounds
|
||||
const preClampZoom = adaptiveZoom
|
||||
adaptiveZoom = Math.max(8, Math.min(MAX_ZOOM, adaptiveZoom))
|
||||
// Binary search bounds
|
||||
let minZoom = 1
|
||||
let maxZoom = MAX_ZOOM
|
||||
let adaptiveZoom = 10
|
||||
let iterations = 0
|
||||
const MAX_ITERATIONS = 20
|
||||
|
||||
// Debug logging for Gibraltar
|
||||
const hasGibraltar = regionUnderCursor === 'gi'
|
||||
if (hasGibraltar) {
|
||||
console.log(`[Zoom] 🎯 GIBRALTAR BREAKDOWN:`, {
|
||||
regionArea: `${regionUnderCursorArea.toFixed(6)}px²`,
|
||||
magnifierArea: `${magnifierArea.toFixed(0)}px²`,
|
||||
targetRatio: `${(targetAreaRatio * 100).toFixed(0)}%`,
|
||||
calculatedZoom: preClampZoom.toFixed(1),
|
||||
finalZoom: adaptiveZoom.toFixed(1),
|
||||
hitMaxZoom: preClampZoom > MAX_ZOOM,
|
||||
})
|
||||
while (iterations < MAX_ITERATIONS && maxZoom - minZoom > 0.1) {
|
||||
iterations++
|
||||
const testZoom = (minZoom + maxZoom) / 2
|
||||
|
||||
// Calculate magnified viewBox dimensions at this zoom
|
||||
const magnifiedViewBoxWidth = viewBoxWidth / testZoom
|
||||
const magnifiedViewBoxHeight = viewBoxHeight / testZoom
|
||||
const magnifiedViewBoxArea = magnifiedViewBoxWidth * magnifiedViewBoxHeight
|
||||
|
||||
// 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
|
||||
const countFactor = Math.min(regionsInBox / 10, 1)
|
||||
adaptiveZoom = 10 + countFactor * 20
|
||||
|
||||
adaptiveZoom = testZoom
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue