More APRS enhancements
This commit is contained in:
36
ui/src/components/aprs/APRSSymbol.scss
Normal file
36
ui/src/components/aprs/APRSSymbol.scss
Normal file
@@ -0,0 +1,36 @@
|
||||
.aprs-symbol {
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Each sprite sheet is 16 symbols wide
|
||||
// Background size needs to be 16 * symbol-size for proper sprite positioning
|
||||
|
||||
&.aprs-symbol-24 {
|
||||
background-size: 384px auto; // 16 * 24
|
||||
}
|
||||
|
||||
&.aprs-symbol-32 {
|
||||
background-size: 512px auto; // 16 * 32
|
||||
}
|
||||
|
||||
&.aprs-symbol-48 {
|
||||
background-size: 768px auto; // 16 * 48
|
||||
}
|
||||
|
||||
&.aprs-symbol-56 {
|
||||
background-size: 896px auto; // 16 * 56
|
||||
}
|
||||
|
||||
&.aprs-symbol-64 {
|
||||
background-size: 1024px auto; // 16 * 64
|
||||
}
|
||||
|
||||
&.aprs-symbol-128 {
|
||||
background-size: 2048px auto; // 16 * 128
|
||||
}
|
||||
|
||||
&.aprs-symbol-256 {
|
||||
background-size: 4096px auto; // 16 * 256
|
||||
}
|
||||
}
|
||||
148
ui/src/components/aprs/APRSSymbol.tsx
Normal file
148
ui/src/components/aprs/APRSSymbol.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
1
ui/src/components/aprs/index.ts
Normal file
1
ui/src/components/aprs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { APRSSymbol, type APRSSymbolProps } from './APRSSymbol';
|
||||
Reference in New Issue
Block a user