import { getOrganizationIconSvg } from '@/components/map/organizationIcons'; import { mapDatabaseSubtypeToSchemaSubtype } from '@/lib/organizationSubtypeMapper.ts'; import { DivIcon } from 'leaflet'; import { Building } from 'lucide-react'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; /** * Icon cache to prevent recreating icons on every render * Uses WeakMap for automatic garbage collection when icons are no longer needed */ const iconCache = new WeakMap>(); /** * Create a cache key for an icon based on its properties */ function createIconCacheKey( orgId: string, isSelected: boolean, isHovered: boolean, logoUrl?: string | null ): string { return `${orgId}-${isSelected}-${isHovered}-${logoUrl || 'no-logo'}`; } /** * Get or create a cached icon for an organization marker * Includes error handling for edge cases */ /** * Get computed CSS variable value * This ensures CSS variables resolve properly in inline HTML */ function getCSSVariableValue(variableName: string, fallback: string = '#000000'): string { if (typeof window === 'undefined') return fallback; try { const value = getComputedStyle(document.documentElement) .getPropertyValue(variableName) .trim(); return value || fallback; } catch { return fallback; } } /** * Get sector color from CSS variable or fallback * Ensures colors are visible and never black/dark */ function getSectorColor(colorKey: string | undefined): string { if (!colorKey) return '#6b7280'; // gray fallback const cssVar = `--sector-${colorKey}`; const color = getCSSVariableValue(cssVar); // If we got a hex color, use it directly; otherwise try to parse if (color.startsWith('#')) { // Validate it's not black or too dark const hex = color.replace('#', ''); if (hex === '000000' || hex === '000') { // Use fallback instead of black return getSectorFallbackColor(colorKey); } return color; } // Use fallback colors for common sectors return getSectorFallbackColor(colorKey); } /** * Get fallback color for a sector (ensures visibility) */ function getSectorFallbackColor(colorKey: string): string { // Bright, visible colors for sectors const fallbackColors: Record = { construction: '#5d7c9a', // Blue-gray production: '#2e7d32', // Green recreation: '#d97757', // Orange logistics: '#7b68b8', // Purple agriculture: '#4caf50', // Green energy: '#ff9800', // Orange technology: '#2196f3', // Blue manufacturing: '#607d8b', // Blue-gray commercial: '#9c27b0', // Purple healthcare: '#e91e63', // Pink education: '#3f51b5', // Indigo government: '#795548', // Brown hospitality: '#f44336', // Red transportation: '#00bcd4', // Cyan infrastructure: '#ff5722', // Deep orange }; return fallbackColors[colorKey] || '#6b7280'; // Gray fallback } export function getCachedOrganizationIcon( orgId: string, org: { LogoURL?: string | null; Name: string; Subtype?: string | null | undefined }, sector: { icon?: React.ReactElement; colorKey?: string } | null, isSelected: boolean, isHovered: boolean ): DivIcon { try { // Use org object as WeakMap key for automatic cleanup const orgCache = iconCache.get(org) || new Map(); const cacheKey = createIconCacheKey(orgId, isSelected, isHovered, org.LogoURL); // Return cached icon if available const cached = orgCache.get(cacheKey); if (cached) { return cached; } // Create new icon - larger size for better visibility // Base size increased for better visibility at all zoom levels const size = Math.max(28, isSelected ? 40 : isHovered ? 34 : 32); // Increased base size for better icon visibility // Ensure size is a valid number if (!Number.isFinite(size) || size <= 0) { console.warn('[IconCache] Invalid size calculated:', size, { isSelected, isHovered }); } const borderWidth = isSelected || isHovered ? 3 : 2; // Get actual color values instead of CSS variables for inline styles const primaryColor = getCSSVariableValue('--primary', '#3b82f6'); const borderColorValue = isSelected || isHovered ? primaryColor : getCSSVariableValue('--border', '#e5e7eb'); // Get background color - ensure it's never black/dark unless explicitly set const bgColor = org.LogoURL ? getCSSVariableValue('--primary-foreground', '#ffffff') : (() => { const sectorColor = getSectorColor(sector?.colorKey); // Ensure we don't get black/dark colors that make icons invisible // If color is too dark, use a lighter fallback if (sectorColor === '#000000' || sectorColor === '#000' || !sectorColor || sectorColor.trim() === '') { return '#6b7280'; // Use gray as fallback instead of black } return sectorColor; })(); /** * Calculate a contrasting color for the icon based on background brightness */ function getContrastingColor(bgColor: string): string { const hex = bgColor.replace('#', ''); if (hex.length !== 6) return '#ffffff'; const r = parseInt(hex.substr(0, 2), 16); const g = parseInt(hex.substr(2, 2), 16); const b = parseInt(hex.substr(4, 2), 16); const brightness = (r * 299 + g * 587 + b * 114) / 1000; if (brightness > 128) { // Light background - use darker contrasting color const darkerR = Math.max(0, Math.min(255, r * 0.4)); const darkerG = Math.max(0, Math.min(255, g * 0.4)); const darkerB = Math.max(0, Math.min(255, b * 0.4)); return `rgb(${Math.round(darkerR)}, ${Math.round(darkerG)}, ${Math.round(darkerB)})`; } else { // Dark background - use lighter contrasting color const lighterR = Math.min(255, r + (255 - r) * 0.7); const lighterG = Math.min(255, g + (255 - g) * 0.7); const lighterB = Math.min(255, b + (255 - b) * 0.7); return `rgb(${Math.round(lighterR)}, ${Math.round(lighterG)}, ${Math.round(lighterB)})`; } } // Render sector icon to HTML string if no logo (deprecated - using subtype icons instead) // This code path is kept for backward compatibility but typically not used let iconHtml = ''; if (!org.LogoURL && sector?.icon && React.isValidElement(sector.icon)) { try { const iconSize = Math.max(16, Math.round(size * 0.65)); // Increased size for better visibility const contrastingColor = getContrastingColor(bgColor); const iconElement = React.cloneElement( sector.icon as React.ReactElement<{ width?: number; height?: number; className?: string; style?: React.CSSProperties; }>, { width: iconSize, height: iconSize, color: contrastingColor, // Use contrasting color instead of plain white strokeWidth: 2.5, // Thicker for visibility } ); iconHtml = renderToStaticMarkup(iconElement); } catch { // Fallback if icon rendering fails iconHtml = ''; } } // Escape HTML in organization name for safety const escapedName = org.Name.replace(//g, '>') .replace(/"/g, '"'); // Map database subtype to schema subtype // Database has granular subtypes (retail, food_beverage, etc.) // Schema expects broader categories (commercial, healthcare, etc.) const schemaSubtype = mapDatabaseSubtypeToSchemaSubtype(org.Subtype); // Create fallback HTML for when logo fails to load // Use organization subtype icon based on mapped schema subtype // Note: iconColor parameter is now ignored - getOrganizationIconSvg calculates contrasting color automatically const iconColor = '#ffffff'; // This is a placeholder - actual color is calculated inside getOrganizationIconSvg let fallbackIconSvg: string; // Prefer schema subtype icon (mapped from database subtype) // getOrganizationIconSvg will automatically calculate a contrasting color based on bgColor fallbackIconSvg = getOrganizationIconSvg(schemaSubtype, size, iconColor, bgColor); // Fallback HTML with proper centering // getOrganizationIconSvg returns just the SVG icon, we wrap it for centering // Note: background is already set on the outer div, so we don't need it here const fallbackHtml = `
${fallbackIconSvg}
`; const html = `
${ org.LogoURL ? `${escapedName}
${fallbackHtml}
` : fallbackHtml }
`; const icon = new DivIcon({ html, className: 'custom-organization-marker', iconSize: [size, size], iconAnchor: [size / 2, size / 2], popupAnchor: [0, -size / 2], }); // Debug logging in development if (process.env.NODE_ENV === 'development') { console.log(`[IconCache] Created icon for ${org.Name}:`, { size, bgColor, borderColor: borderColorValue, hasLogo: !!org.LogoURL, sector: sector?.colorKey, }); } // Cache the icon orgCache.set(cacheKey, icon); iconCache.set(org, orgCache); return icon; } catch { // Fallback icon in case of any error const size = 24; return new DivIcon({ html: `
`, className: 'custom-organization-marker', iconSize: [size, size], iconAnchor: [size / 2, size / 2], }); } } /** * Get or create a cached icon for a historical landmark marker */ export function getCachedHistoricalIcon( landmarkId: string, landmark: { id: string }, isSelected: boolean, isHovered: boolean ): DivIcon { const landmarkCache = iconCache.get(landmark) || new Map(); const cacheKey = createIconCacheKey(landmarkId, isSelected, isHovered); const cached = landmarkCache.get(cacheKey); if (cached) { return cached; } const size = isSelected ? 40 : isHovered ? 32 : 28; const borderWidth = isSelected || isHovered ? 3 : 2; // Get actual color values const primaryColor = getCSSVariableValue('--primary', '#3b82f6'); const borderColorValue = isSelected || isHovered ? primaryColor : getCSSVariableValue('--border', '#e5e7eb'); const bgColor = '#8b5cf6'; // Purple/violet color for historical buildings const iconColor = '#ffffff'; // Create a friendly building icon using Lucide const iconSize = size * 0.7; const buildingIcon = renderToStaticMarkup( React.createElement(Building, { size: iconSize, color: iconColor, strokeWidth: 1.5 }) ); const html = `
${buildingIcon} ${isSelected || isHovered ? `
` : ''}
`; const icon = new DivIcon({ html, className: 'custom-historical-marker', iconSize: [size, size], iconAnchor: [size / 2, size / 2], popupAnchor: [0, -size / 2], }); landmarkCache.set(cacheKey, icon); iconCache.set(landmark, landmarkCache); return icon; }