Files
hamview/ui/src/components/aprs/APRSSymbol.tsx
maze e83df1c143
Some checks failed
Test and build / Test and lint (push) Failing after 35s
Test and build / Build collector (push) Failing after 36s
Test and build / Build receiver (push) Failing after 36s
More APRS enhancements
2026-03-05 22:24:09 +01:00

149 lines
3.8 KiB
TypeScript

import React from 'react';
import './APRSSymbol.scss';
export interface APRSSymbolProps {
/**
* APRS symbol as 2-character string: table + code
* Examples: "/!" (police station), "\!" (emergency), "/-" (house)
*
* First character is table identifier:
* - '/' for primary table
* - '\' for secondary table
* - alphanumeric (0-9, A-Z) for overlay
*
* Second character is the symbol code
*/
symbol: string;
/**
* Symbol size in pixels (default: 24)
* Available sizes: 24, 32, 48, 56, 64, 128, 256
*/
size?: 24 | 32 | 48 | 56 | 64 | 128 | 256;
/**
* Alternative text for accessibility
*/
alt?: string;
/**
* Additional CSS class name
*/
className?: string;
}
/**
* Get the table ID for the sprite filename
* - Primary table (/): 0
* - Secondary table (\): 1
* - Overlay characters: 2
*/
const getTableId = (table: string): number => {
if (table === '/') return 0;
if (table === '\\') return 1;
return 2; // Overlay
};
/**
* Calculate sprite position based on ASCII character code
* Symbols are arranged in a 16-column grid, ordered by ASCII value
*/
const getSymbolPosition = (table: string, code: string): { row: number; col: number } | null => {
if (!code || code.length !== 1) return null;
const charCode = code.charCodeAt(0);
// Primary and secondary tables use characters from ! (33) to } (125)
if (table === '/' || table === '\\') {
if (charCode < 33 || charCode > 125) return null;
const index = charCode - 33;
return {
row: Math.floor(index / 16),
col: index % 16
};
}
// Overlay characters: 0-9 (48-57), A-Z (65-90)
if (charCode >= 48 && charCode <= 57) {
// 0-9
const index = charCode - 48;
return {
row: Math.floor(index / 16),
col: index % 16
};
} else if (charCode >= 65 && charCode <= 90) {
// A-Z
const index = 10 + (charCode - 65);
return {
row: Math.floor(index / 16),
col: index % 16
};
}
return null;
};
/**
* React component for rendering APRS symbols from sprite sheets
*/
export const APRSSymbol: React.FC<APRSSymbolProps> = ({
symbol,
size = 24,
alt,
className = ''
}) => {
// Parse the symbol string (format: table + code)
if (!symbol || symbol.length < 2) {
// Return empty div if symbol is invalid
return <div className={`aprs-symbol aprs-symbol-${size} ${className}`} title={alt} />;
}
const table = symbol.charAt(0);
const code = symbol.charAt(1);
const tableId = getTableId(table);
const position = getSymbolPosition(table, code);
if (!position) {
// Return empty div if symbol is invalid
return <div className={`aprs-symbol aprs-symbol-${size} ${className}`} title={alt} />;
}
const { row, col } = position;
// Build image paths for different resolutions
const imagePath1x = `/image/protocol/aprs/aprs-symbols-${size}-${tableId}.png`;
const imagePath2x = `/image/protocol/aprs/aprs-symbols-${size}-${tableId}@2x.png`;
const imagePath3x = `/image/protocol/aprs/aprs-symbols-${size}-${tableId}@3x.png`;
// Calculate background position
const bgX = -(col * size);
const bgY = -(row * size);
// Use image-set for retina display support
const backgroundImage = [
`-webkit-image-set(url('${imagePath1x}') 1x, url('${imagePath2x}') 2x, url('${imagePath3x}') 3x)`,
`image-set(url('${imagePath1x}') 1x, url('${imagePath2x}') 2x, url('${imagePath3x}') 3x)`,
`url('${imagePath1x}')` // Fallback
].join(', ');
const style: React.CSSProperties = {
backgroundImage,
backgroundPosition: `${bgX}px ${bgY}px`,
width: `${size}px`,
height: `${size}px`
};
return (
<div
className={`aprs-symbol aprs-symbol-${size} ${className}`}
style={style}
title={alt || `APRS symbol: ${symbol}`}
role="img"
aria-label={alt || `APRS symbol: ${symbol}`}
/>
);
};
export default APRSSymbol;