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; })(); // 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 const fallbackIconSvg: string = getOrganizationIconSvg(schemaSubtype, size, iconColor, bgColor); // 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; }