import { DivIcon } from 'leaflet'; import React from 'react'; import { renderToString } from 'react-dom/server'; import { Building } from 'lucide-react'; import { getOrganizationIconSvg } from '@/components/map/organizationIcons'; import { mapDatabaseSubtypeToSchemaSubtype } from '@/lib/organizationSubtypeMapper.ts'; import type { OrganizationSubtype } from '@/schemas/organizationSubtype.ts'; /** * 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 */ 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('#')) { return color; } // Fallback colors for common sectors const fallbackColors: Record = { construction: '#5d7c9a', production: '#2e7d32', recreation: '#d97757', logistics: '#7b68b8', }; return fallbackColors[colorKey] || '#6b7280'; } 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(20, isSelected ? 36 : isHovered ? 28 : 26); // Ensure minimum size // 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'); const bgColor = org.LogoURL ? getCSSVariableValue('--primary-foreground', '#ffffff') : getSectorColor(sector?.colorKey); // Render sector icon to HTML string if no logo let iconHtml = ''; if (!org.LogoURL && sector?.icon && React.isValidElement(sector.icon)) { try { const iconSize = Math.max(12, size * 0.6); // Ensure minimum icon size const iconElement = React.cloneElement( sector.icon as React.ReactElement<{ width?: number; height?: number; className?: string; style?: React.CSSProperties; }>, { width: iconSize, height: iconSize, className: 'text-primary-foreground', style: { width: `${iconSize}px`, height: `${iconSize}px`, color: 'hsl(var(--primary-foreground))', fill: 'currentColor', }, } ); iconHtml = renderToString(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 const iconColor = '#ffffff'; // White icons on colored backgrounds const fallbackBgColor = getCSSVariableValue('--primary-foreground', '#ffffff'); let fallbackIconSvg: string; // Prefer schema subtype icon (mapped from database subtype) fallbackIconSvg = getOrganizationIconSvg(schemaSubtype, size, iconColor, bgColor); 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 = renderToString( 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; }