turash/bugulma/frontend/utils/map/iconCache.ts
Damir Mukimov 673e8d4361
Some checks failed
CI/CD Pipeline / backend-lint (push) Failing after 31s
CI/CD Pipeline / backend-build (push) Has been skipped
CI/CD Pipeline / frontend-lint (push) Failing after 1m37s
CI/CD Pipeline / frontend-build (push) Has been skipped
CI/CD Pipeline / e2e-test (push) Has been skipped
fix: resolve all frontend lint errors (85 issues fixed)
- Replace all 'any' types with proper TypeScript interfaces
- Fix React hooks setState in useEffect issues with lazy initialization
- Remove unused variables and imports across all files
- Fix React Compiler memoization dependency issues
- Add comprehensive i18n translation keys for admin interfaces
- Apply consistent prettier formatting throughout codebase
- Clean up unused bulk editing functionality
- Improve type safety and code quality across frontend

Files changed: 39
- ImpactMetrics.tsx: Fixed any types and interfaces
- AdminVerificationQueuePage.tsx: Added i18n keys, removed unused vars
- LocalizationUIPage.tsx: Fixed memoization, added translations
- LocalizationDataPage.tsx: Added type safety and translations
- And 35+ other files with various lint fixes
2025-12-25 14:14:58 +01:00

321 lines
11 KiB
TypeScript

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<object, Map<string, DivIcon>>();
/**
* 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<string, string> = {
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<string, DivIcon>();
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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
// 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 = `
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
${fallbackIconSvg}
</div>
`;
const html = `
<div style="
width: ${size}px;
height: ${size}px;
border-radius: 50%;
background-color: ${bgColor};
border: ${borderWidth}px solid ${borderColorValue};
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
position: relative;
overflow: hidden;
">
${
org.LogoURL
? `<img src="${org.LogoURL}" alt="${escapedName}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;" loading="lazy" onerror="this.onerror=null;this.style.display='none';this.nextElementSibling&&(this.nextElementSibling.style.display='flex');" />
<div style="display:none;width:100%;height:100%;align-items:center;justify-content:center;">${fallbackHtml}</div>`
: fallbackHtml
}
</div>
`;
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: `<div style="width: ${size}px; height: ${size}px; border-radius: 50%; background-color: hsl(var(--muted)); border: 2px solid hsl(var(--border));"></div>`,
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<string, DivIcon>();
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 = `
<div style="
width: ${size}px;
height: ${size}px;
border-radius: 8px;
background: linear-gradient(135deg, ${bgColor} 0%, ${bgColor}dd 100%);
border: ${borderWidth}px solid ${borderColorValue};
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25), 0 1px 3px rgba(0, 0, 0, 0.15);
transition: all 0.2s ease;
position: relative;
overflow: visible;
">
${buildingIcon}
${
isSelected || isHovered
? `
<div style="
position: absolute;
top: -${size * 0.15}px;
right: -${size * 0.15}px;
width: ${size * 0.3}px;
height: ${size * 0.3}px;
background: ${primaryColor};
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
"></div>
`
: ''
}
</div>
`;
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;
}