feat: Improve aircraft legend clarity and icon differentiation - resolves #13 #36

Merged
olemd merged 3 commits from feature/improve-aircraft-legend-issue-13 into main 2025-09-01 10:06:30 +02:00
26 changed files with 829 additions and 591 deletions

View file

@ -29,5 +29,6 @@ import "embed"
// external file deployment or complicated asset management.
//
// Updated to include database.html for database status page
//
//go:embed static
var Static embed.FS

View file

@ -262,21 +262,28 @@ body {
}
.legend-icon {
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid #ffffff;
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 8px;
}
.legend-icon.light { background: #00bfff; } /* Sky blue for light aircraft */
.legend-icon.medium { background: #00ff88; } /* Green for medium aircraft */
.legend-icon.large { background: #ff8c00; } /* Orange for large aircraft */
.legend-icon.high-vortex { background: #ff4500; } /* Red-orange for high vortex large */
.legend-icon.heavy { background: #ff0000; } /* Red for heavy aircraft */
.legend-icon.helicopter { background: #ff00ff; } /* Magenta for helicopters */
.legend-icon.military { background: #ff4444; } /* Red-orange for military */
.legend-icon.ga { background: #ffff00; } /* Yellow for general aviation */
.legend-icon.ground { background: #888888; } /* Gray for ground vehicles */
.legend-icon svg {
width: 100%;
height: 100%;
}
.legend-icon.light svg { color: #00bfff; } /* Sky blue for light aircraft */
.legend-icon.medium svg { color: #00ff88; } /* Green for medium aircraft */
.legend-icon.large svg { color: #ff8c00; } /* Orange for large aircraft */
.legend-icon.high-vortex svg { color: #ff4500; } /* Red-orange for high vortex large */
.legend-icon.heavy svg { color: #ff0000; } /* Red for heavy aircraft */
.legend-icon.helicopter svg { color: #ff00ff; } /* Magenta for helicopters */
.legend-icon.military svg { color: #ff4444; } /* Red-orange for military */
.legend-icon.ga svg { color: #ffff00; } /* Yellow for general aviation */
.legend-icon.ground svg { color: #888888; } /* Gray for ground vehicles */
.table-controls {
display: flex;

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(18,18)">
<!-- Heavy aircraft (jumbo jet like A380 or 747) -->
<!-- Fuselage (very wide) -->
<path d="M0,-16 L-2.5,-14 L-2.5,9 L-1.5,12 L0,13 L1.5,12 L2.5,9 L2.5,-14 Z" fill="currentColor"/>
<!-- Main wings (very long and wide) -->
<path d="M-2.5,0.5 L-16,4 L-16,6.5 L-2.5,3.5 Z" fill="currentColor"/>
<path d="M2.5,0.5 L16,4 L16,6.5 L2.5,3.5 Z" fill="currentColor"/>
<!-- Wing tips (large winglets) -->
<path d="M-16,4 L-17.5,2.5 L-17.5,5 L-16,6.5 Z" fill="currentColor"/>
<path d="M16,4 L17.5,2.5 L17.5,5 L16,6.5 Z" fill="currentColor"/>
<!-- Tail wings (large) -->
<path d="M-1.2,10 L-7,11.8 L-7,13 L-1.2,11.5 Z" fill="currentColor"/>
<path d="M1.2,10 L7,11.8 L7,13 L1.2,11.5 Z" fill="currentColor"/>
<!-- Vertical stabilizer (very tall) -->
<path d="M0,8 L-0.8,8 L-3,13.5 L0,13.5 L3,13.5 L0.8,8 Z" fill="currentColor"/>
<!-- Nose (large) -->
<ellipse cx="0" cy="-15" rx="2.5" ry="3" fill="currentColor"/>
<!-- Engine nacelles (very large, 4 engines) -->
<ellipse cx="-6" cy="3" rx="1.5" ry="3" fill="currentColor"/>
<ellipse cx="6" cy="3" rx="1.5" ry="3" fill="currentColor"/>
<ellipse cx="-4" cy="4" rx="1" ry="2" fill="currentColor"/>
<ellipse cx="4" cy="4" rx="1" ry="2" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="34" height="34" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(17,17)">
<!-- Large aircraft (wide-body airliner) -->
<!-- Fuselage (wider than commercial) -->
<path d="M0,-15 L-2,-13 L-2,8 L-1.2,11 L0,12 L1.2,11 L2,8 L2,-13 Z" fill="currentColor"/>
<!-- Main wings (longer, wider) -->
<path d="M-2,0 L-14,3.5 L-14,5.5 L-2,3 Z" fill="currentColor"/>
<path d="M2,0 L14,3.5 L14,5.5 L2,3 Z" fill="currentColor"/>
<!-- Wing tips (winglets) -->
<path d="M-14,3.5 L-15,2.5 L-15,4 L-14,5 Z" fill="currentColor"/>
<path d="M14,3.5 L15,2.5 L15,4 L14,5 Z" fill="currentColor"/>
<!-- Tail wings -->
<path d="M-1,9 L-6,10.5 L-6,11.5 L-1,10.5 Z" fill="currentColor"/>
<path d="M1,9 L6,10.5 L6,11.5 L1,10.5 Z" fill="currentColor"/>
<!-- Vertical stabilizer (taller) -->
<path d="M0,7 L-0.6,7 L-2.5,12 L0,12 L2.5,12 L0.6,7 Z" fill="currentColor"/>
<!-- Nose -->
<ellipse cx="0" cy="-14" rx="2" ry="2.5" fill="currentColor"/>
<!-- Engine nacelles (larger) -->
<ellipse cx="-5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
<ellipse cx="5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(14,14)">
<!-- Light aircraft (smaller, simpler design) -->
<!-- Fuselage -->
<path d="M0,-10 L-1,-9 L-1,5 L-0.5,7 L0,8 L0.5,7 L1,5 L1,-9 Z" fill="currentColor"/>
<!-- Main wings -->
<path d="M-1,-1 L-8,1 L-8,2.5 L-1,1 Z" fill="currentColor"/>
<path d="M1,-1 L8,1 L8,2.5 L1,1 Z" fill="currentColor"/>
<!-- Tail wings -->
<path d="M-0.5,5 L-3,6 L-3,6.5 L-0.5,6 Z" fill="currentColor"/>
<path d="M0.5,5 L3,6 L3,6.5 L0.5,6 Z" fill="currentColor"/>
<!-- Vertical stabilizer -->
<path d="M0,4 L-0.3,4 L-1,7 L0,7 L1,7 L0.3,4 Z" fill="currentColor"/>
<!-- Nose -->
<ellipse cx="0" cy="-9.5" rx="1" ry="1.5" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30" height="30" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(15,15)">
<!-- Medium aircraft (medium size, more detailed) -->
<!-- Fuselage -->
<path d="M0,-12 L-1.2,-11 L-1.2,6 L-0.8,8 L0,9 L0.8,8 L1.2,6 L1.2,-11 Z" fill="currentColor"/>
<!-- Main wings -->
<path d="M-1.2,-0.5 L-10,2 L-10,3.5 L-1.2,2 Z" fill="currentColor"/>
<path d="M1.2,-0.5 L10,2 L10,3.5 L1.2,2 Z" fill="currentColor"/>
<!-- Tail wings -->
<path d="M-0.7,6.5 L-4,7.5 L-4,8.2 L-0.7,7.5 Z" fill="currentColor"/>
<path d="M0.7,6.5 L4,7.5 L4,8.2 L0.7,7.5 Z" fill="currentColor"/>
<!-- Vertical stabilizer -->
<path d="M0,5 L-0.4,5 L-1.5,9 L0,9 L1.5,9 L0.4,5 Z" fill="currentColor"/>
<!-- Nose -->
<ellipse cx="0" cy="-11.5" rx="1.2" ry="1.8" fill="currentColor"/>
<!-- Engine nacelles -->
<ellipse cx="-3" cy="1.5" rx="0.8" ry="1.5" fill="currentColor"/>
<ellipse cx="3" cy="1.5" rx="0.8" ry="1.5" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -112,37 +112,175 @@
<div class="legend">
<h4>ADS-B Categories</h4>
<div class="legend-item">
<span class="legend-icon light"></span>
<span class="legend-icon light">
<svg width="24" height="24" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(14,14)">
<path d="M0,-10 L-1,-9 L-1,5 L-0.5,7 L0,8 L0.5,7 L1,5 L1,-9 Z" fill="currentColor"/>
<path d="M-1,-1 L-8,1 L-8,2.5 L-1,1 Z" fill="currentColor"/>
<path d="M1,-1 L8,1 L8,2.5 L1,1 Z" fill="currentColor"/>
<path d="M-0.5,5 L-3,6 L-3,6.5 L-0.5,6 Z" fill="currentColor"/>
<path d="M0.5,5 L3,6 L3,6.5 L0.5,6 Z" fill="currentColor"/>
<path d="M0,4 L-0.3,4 L-1,7 L0,7 L1,7 L0.3,4 Z" fill="currentColor"/>
<ellipse cx="0" cy="-9.5" rx="1" ry="1.5" fill="currentColor"/>
</g>
</svg>
</span>
<span>Light &lt; 7000kg</span>
</div>
<div class="legend-item">
<span class="legend-icon medium"></span>
<span class="legend-icon medium">
<svg width="24" height="24" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(15,15)">
<path d="M0,-12 L-1.2,-11 L-1.2,6 L-0.8,8 L0,9 L0.8,8 L1.2,6 L1.2,-11 Z" fill="currentColor"/>
<path d="M-1.2,-0.5 L-10,2 L-10,3.5 L-1.2,2 Z" fill="currentColor"/>
<path d="M1.2,-0.5 L10,2 L10,3.5 L1.2,2 Z" fill="currentColor"/>
<path d="M-0.7,6.5 L-4,7.5 L-4,8.2 L-0.7,7.5 Z" fill="currentColor"/>
<path d="M0.7,6.5 L4,7.5 L4,8.2 L0.7,7.5 Z" fill="currentColor"/>
<path d="M0,5 L-0.4,5 L-1.5,9 L0,9 L1.5,9 L0.4,5 Z" fill="currentColor"/>
<ellipse cx="0" cy="-11.5" rx="1.2" ry="1.8" fill="currentColor"/>
<ellipse cx="-3" cy="1.5" rx="0.8" ry="1.5" fill="currentColor"/>
<ellipse cx="3" cy="1.5" rx="0.8" ry="1.5" fill="currentColor"/>
</g>
</svg>
</span>
<span>Medium 7000-34000kg</span>
</div>
<div class="legend-item">
<span class="legend-icon large"></span>
<span class="legend-icon large">
<svg width="24" height="24" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(17,17)">
<path d="M0,-15 L-2,-13 L-2,8 L-1.2,11 L0,12 L1.2,11 L2,8 L2,-13 Z" fill="currentColor"/>
<path d="M-2,0 L-14,3.5 L-14,5.5 L-2,3 Z" fill="currentColor"/>
<path d="M2,0 L14,3.5 L14,5.5 L2,3 Z" fill="currentColor"/>
<path d="M-14,3.5 L-15,2.5 L-15,4 L-14,5 Z" fill="currentColor"/>
<path d="M14,3.5 L15,2.5 L15,4 L14,5 Z" fill="currentColor"/>
<path d="M-1,9 L-6,10.5 L-6,11.5 L-1,10.5 Z" fill="currentColor"/>
<path d="M1,9 L6,10.5 L6,11.5 L1,10.5 Z" fill="currentColor"/>
<path d="M0,7 L-0.6,7 L-2.5,12 L0,12 L2.5,12 L0.6,7 Z" fill="currentColor"/>
<ellipse cx="0" cy="-14" rx="2" ry="2.5" fill="currentColor"/>
<ellipse cx="-5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
<ellipse cx="5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
</g>
</svg>
</span>
<span>Large 34000-136000kg</span>
</div>
<div class="legend-item">
<span class="legend-icon high-vortex"></span>
<span class="legend-icon high-vortex">
<svg width="24" height="24" viewBox="0 0 34 34" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(17,17)">
<path d="M0,-15 L-2,-13 L-2,8 L-1.2,11 L0,12 L1.2,11 L2,8 L2,-13 Z" fill="currentColor"/>
<path d="M-2,0 L-14,3.5 L-14,5.5 L-2,3 Z" fill="currentColor"/>
<path d="M2,0 L14,3.5 L14,5.5 L2,3 Z" fill="currentColor"/>
<path d="M-14,3.5 L-15,2.5 L-15,4 L-14,5 Z" fill="currentColor"/>
<path d="M14,3.5 L15,2.5 L15,4 L14,5 Z" fill="currentColor"/>
<path d="M-1,9 L-6,10.5 L-6,11.5 L-1,10.5 Z" fill="currentColor"/>
<path d="M1,9 L6,10.5 L6,11.5 L1,10.5 Z" fill="currentColor"/>
<path d="M0,7 L-0.6,7 L-2.5,12 L0,12 L2.5,12 L0.6,7 Z" fill="currentColor"/>
<ellipse cx="0" cy="-14" rx="2" ry="2.5" fill="currentColor"/>
<ellipse cx="-5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
<ellipse cx="5" cy="2.5" rx="1.2" ry="2.5" fill="currentColor"/>
</g>
</svg>
</span>
<span>High Vortex Large</span>
</div>
<div class="legend-item">
<span class="legend-icon heavy"></span>
<span class="legend-icon heavy">
<svg width="24" height="24" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(18,18)">
<path d="M0,-16 L-2.5,-14 L-2.5,9 L-1.5,12 L0,13 L1.5,12 L2.5,9 L2.5,-14 Z" fill="currentColor"/>
<path d="M-2.5,0.5 L-16,4 L-16,6.5 L-2.5,3.5 Z" fill="currentColor"/>
<path d="M2.5,0.5 L16,4 L16,6.5 L2.5,3.5 Z" fill="currentColor"/>
<path d="M-16,4 L-17.5,2.5 L-17.5,5 L-16,6.5 Z" fill="currentColor"/>
<path d="M16,4 L17.5,2.5 L17.5,5 L16,6.5 Z" fill="currentColor"/>
<path d="M-1.2,10 L-7,11.8 L-7,13 L-1.2,11.5 Z" fill="currentColor"/>
<path d="M1.2,10 L7,11.8 L7,13 L1.2,11.5 Z" fill="currentColor"/>
<path d="M0,8 L-0.8,8 L-3,13.5 L0,13.5 L3,13.5 L0.8,8 Z" fill="currentColor"/>
<ellipse cx="0" cy="-15" rx="2.5" ry="3" fill="currentColor"/>
<ellipse cx="-6" cy="3" rx="1.5" ry="3" fill="currentColor"/>
<ellipse cx="6" cy="3" rx="1.5" ry="3" fill="currentColor"/>
<ellipse cx="-4" cy="4" rx="1" ry="2" fill="currentColor"/>
<ellipse cx="4" cy="4" rx="1" ry="2" fill="currentColor"/>
</g>
</svg>
</span>
<span>Heavy &gt; 136000kg</span>
</div>
<div class="legend-item">
<span class="legend-icon helicopter"></span>
<span class="legend-icon helicopter">
<svg width="24" height="24" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(16,16)">
<!-- Main rotor disc -->
<ellipse cx="0" cy="-2" rx="11" ry="1" fill="currentColor" opacity="0.2"/>
<path d="M-11,-2 L11,-2" stroke="currentColor" stroke-width="0.5" opacity="0.4"/>
<!-- Main fuselage -->
<path d="M0,-8 C-3,-8 -4,-6 -4,-3 L-4,4 C-4,6 -3,7 -1,7 L1,7 C3,7 4,6 4,4 L4,-3 C4,-6 3,-8 0,-8 Z" fill="currentColor"/>
<!-- Cockpit windscreen -->
<path d="M0,-8 C-2,-8 -3,-7 -3,-5 L-3,-3 L3,-3 L3,-5 C3,-7 2,-8 0,-8 Z" fill="currentColor" opacity="0.7"/>
<!-- Tail boom -->
<rect x="-1" y="6" width="2" height="8" fill="currentColor"/>
<!-- Tail rotor -->
<ellipse cx="0" cy="13" rx="1" ry="3" fill="currentColor"/>
<path d="M-3,13 L3,13" stroke="currentColor" stroke-width="0.8"/>
<!-- Landing skids -->
<path d="M-3,7 L-3,9 L-1,9" stroke="currentColor" stroke-width="1" fill="none"/>
<path d="M3,7 L3,9 L1,9" stroke="currentColor" stroke-width="1" fill="none"/>
<!-- Rotor hub -->
<circle cx="0" cy="-2" r="1" fill="currentColor"/>
</g>
</svg>
</span>
<span>Rotorcraft</span>
</div>
<div class="legend-item">
<span class="legend-icon ga"></span>
<span class="legend-icon ga">
<svg width="24" height="24" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(14,14)">
<path d="M0,-8 L-0.8,-7.5 L-0.8,4 L-0.4,5.5 L0,6 L0.4,5.5 L0.8,4 L0.8,-7.5 Z" fill="currentColor"/>
<path d="M-0.8,-0.5 L-6,1 L-6,2 L-0.8,1 Z" fill="currentColor"/>
<path d="M0.8,-0.5 L6,1 L6,2 L0.8,1 Z" fill="currentColor"/>
<path d="M-0.4,4 L-2.5,4.8 L-2.5,5.2 L-0.4,4.8 Z" fill="currentColor"/>
<path d="M0.4,4 L2.5,4.8 L2.5,5.2 L0.4,4.8 Z" fill="currentColor"/>
<path d="M0,3 L-0.2,3 L-0.8,6 L0,6 L0.8,6 L0.2,3 Z" fill="currentColor"/>
<ellipse cx="0" cy="-7.5" rx="0.8" ry="1" fill="currentColor"/>
</g>
</svg>
</span>
<span>Glider/Ultralight</span>
</div>
<div class="legend-item">
<span class="legend-icon ground"></span>
<span class="legend-icon ground">
<svg width="24" height="24" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(16,16)">
<path d="M-6,-2 L6,-2 L8,0 L8,2 L-8,2 L-8,0 Z" fill="currentColor"/>
<ellipse cx="-4" cy="4" rx="2" ry="2" fill="currentColor"/>
<ellipse cx="4" cy="4" rx="2" ry="2" fill="currentColor"/>
<path d="M-2,-2 L2,-2 L2,-6 L-2,-6 Z" fill="currentColor"/>
<path d="M-1,-6 L1,-6 L1,-8 L-1,-8 Z" fill="currentColor"/>
</g>
</svg>
</span>
<span>Surface Vehicle</span>
</div>
<div class="legend-item">
<span class="legend-icon military">
<svg width="24" height="24" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(16,16)">
<path d="M0,-14 L-1.2,-12 L-1.2,6 L-0.8,9 L0,10 L0.8,9 L1.2,6 L1.2,-12 Z" fill="currentColor"/>
<path d="M-1.2,-1 L-11,2 L-11,3.5 L-1.2,2 Z" fill="currentColor"/>
<path d="M1.2,-1 L11,2 L11,3.5 L1.2,2 Z" fill="currentColor"/>
<path d="M-0.7,7 L-4.5,8 L-4.5,8.8 L-0.7,8 Z" fill="currentColor"/>
<path d="M0.7,7 L4.5,8 L4.5,8.8 L0.7,8 Z" fill="currentColor"/>
<path d="M0,5.5 L-0.4,5.5 L-1.8,10 L0,10 L1.8,10 L0.4,5.5 Z" fill="currentColor"/>
<ellipse cx="0" cy="-13" rx="1.2" ry="2" fill="currentColor"/>
<ellipse cx="-3.5" cy="1.8" rx="0.8" ry="1.8" fill="currentColor"/>
<ellipse cx="3.5" cy="1.8" rx="0.8" ry="1.8" fill="currentColor"/>
</g>
</svg>
</span>
<span>Military</span>
</div>
<h4>Sources</h4>
<div id="sources-legend"></div>

View file

@ -331,60 +331,48 @@ export class AircraftManager {
}
getAircraftIconType(aircraft) {
// For icon selection, we still need basic categories
// This determines which SVG shape to use
// For icon selection, determine which SVG shape to use based on category
if (aircraft.OnGround) return 'ground';
if (aircraft.Category) {
const cat = aircraft.Category.toLowerCase();
// Map to basic icon types for visual representation
// Specialized aircraft types
if (cat.includes('helicopter') || cat.includes('rotorcraft')) return 'helicopter';
if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military';
if (cat.includes('cargo') || cat.includes('heavy') || cat.includes('super')) return 'cargo';
if (cat.includes('light') || cat.includes('glider') || cat.includes('ultralight')) return 'ga';
if (cat.includes('glider') || cat.includes('ultralight')) return 'ga';
// Weight-based categories with specific icons
if (cat.includes('light') && cat.includes('7000')) return 'light';
if (cat.includes('medium') && cat.includes('7000-34000')) return 'medium';
if (cat.includes('large') && cat.includes('34000-136000')) return 'large';
if (cat.includes('heavy') && cat.includes('136000')) return 'heavy';
if (cat.includes('high vortex')) return 'large'; // Use large icon for high vortex
// Fallback category matching
if (cat.includes('heavy') || cat.includes('super')) return 'heavy';
if (cat.includes('large')) return 'large';
if (cat.includes('medium')) return 'medium';
if (cat.includes('light')) return 'light';
}
// Default commercial icon for everything else
return 'commercial';
// Default to medium icon for unknown aircraft
return 'medium';
}
getAircraftColor(type, aircraft) {
// Special colors for specific types
if (type === 'military') return '#ff4444';
if (type === 'helicopter') return '#ff00ff';
if (type === 'ground') return '#888888';
if (type === 'ga') return '#ffff00';
// For commercial and cargo types, use weight-based colors
if (aircraft && aircraft.Category) {
const cat = aircraft.Category.toLowerCase();
// Check for specific weight ranges in the category string
// Light aircraft (< 7000kg) - Sky blue
if (cat.includes('light')) {
return '#00bfff';
}
// Medium aircraft (7000-34000kg) - Green
if (cat.includes('medium')) {
return '#00ff88';
}
// High Vortex Large - Red-orange (special wake turbulence category)
if (cat.includes('high vortex')) {
return '#ff4500';
}
// Large aircraft (34000-136000kg) - Orange
if (cat.includes('large')) {
return '#ff8c00';
}
// Heavy aircraft (> 136000kg) - Red
if (cat.includes('heavy') || cat.includes('super')) {
return '#ff0000';
}
// Color mapping based on aircraft type/size
switch (type) {
case 'military': return '#ff4444'; // Red-orange for military
case 'helicopter': return '#ff00ff'; // Magenta for helicopters
case 'ground': return '#888888'; // Gray for ground vehicles
case 'ga': return '#ffff00'; // Yellow for general aviation
case 'light': return '#00bfff'; // Sky blue for light aircraft
case 'medium': return '#00ff88'; // Green for medium aircraft
case 'large': return '#ff8c00'; // Orange for large aircraft
case 'heavy': return '#ff0000'; // Red for heavy aircraft
default: return '#00ff88'; // Default green for unknown
}
// Default to green for unknown commercial aircraft
return '#00ff88';
}

View file

@ -19,28 +19,28 @@ import (
// Shared configuration structures (should match main skyview)
type Config struct {
Server ServerConfig `json:"server"`
Sources []SourceConfig `json:"sources"`
Settings Settings `json:"settings"`
Database *database.Config `json:"database,omitempty"`
Callsign *CallsignConfig `json:"callsign,omitempty"`
Origin OriginConfig `json:"origin"`
Server ServerConfig `json:"server"`
Sources []SourceConfig `json:"sources"`
Settings Settings `json:"settings"`
Database *database.Config `json:"database,omitempty"`
Callsign *CallsignConfig `json:"callsign,omitempty"`
Origin OriginConfig `json:"origin"`
}
type CallsignConfig struct {
Enabled bool `json:"enabled"`
CacheHours int `json:"cache_hours"`
PrivacyMode bool `json:"privacy_mode"`
Sources map[string]CallsignSourceConfig `json:"sources"`
ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"`
Enabled bool `json:"enabled"`
CacheHours int `json:"cache_hours"`
PrivacyMode bool `json:"privacy_mode"`
Sources map[string]CallsignSourceConfig `json:"sources"`
ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"`
}
type CallsignSourceConfig struct {
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
License string `json:"license"`
RequiresConsent bool `json:"requires_consent,omitempty"`
UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"`
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
License string `json:"license"`
RequiresConsent bool `json:"requires_consent,omitempty"`
UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"`
}
type ExternalAPIConfig struct {
@ -458,7 +458,9 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
if len(result.Errors) > 0 {
log.Printf(" %d errors occurred during import (first few):", len(result.Errors))
for i, errMsg := range result.Errors {
if i >= 3 { break }
if i >= 3 {
break
}
log.Printf(" %s", errMsg)
}
}
@ -683,4 +685,3 @@ func cmdOptimize(db *database.Database, force bool) error {
fmt.Println("\n✅ Database optimization completed!")
return nil
}

View file

@ -32,24 +32,24 @@ type APIClientConfig struct {
}
type OpenSkyFlightInfo struct {
ICAO string `json:"icao"`
Callsign string `json:"callsign"`
Origin string `json:"origin"`
Destination string `json:"destination"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
AircraftType string `json:"aircraft_type"`
Registration string `json:"registration"`
FlightNumber string `json:"flight_number"`
Airline string `json:"airline"`
ICAO string `json:"icao"`
Callsign string `json:"callsign"`
Origin string `json:"origin"`
Destination string `json:"destination"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
AircraftType string `json:"aircraft_type"`
Registration string `json:"registration"`
FlightNumber string `json:"flight_number"`
Airline string `json:"airline"`
}
type APIError struct {
Operation string
StatusCode int
Message string
Retryable bool
RetryAfter time.Duration
Operation string
StatusCode int
Message string
Retryable bool
RetryAfter time.Duration
}
func (e *APIError) Error() string {

View file

@ -19,11 +19,11 @@ import (
// Database represents the main database connection and operations
type Database struct {
conn *sql.DB
config *Config
migrator *Migrator
callsign *CallsignManager
history *HistoryManager
conn *sql.DB
config *Config
migrator *Migrator
callsign *CallsignManager
history *HistoryManager
}
// Config holds database configuration options
@ -32,7 +32,7 @@ type Config struct {
Path string `json:"path"`
// Data retention settings
MaxHistoryDays int `json:"max_history_days"` // 0 = unlimited
MaxHistoryDays int `json:"max_history_days"` // 0 = unlimited
BackupOnUpgrade bool `json:"backup_on_upgrade"`
// Connection settings
@ -41,13 +41,13 @@ type Config struct {
ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` // Default: 1 hour
// Maintenance settings
VacuumInterval time.Duration `json:"vacuum_interval"` // Default: 24 hours
VacuumInterval time.Duration `json:"vacuum_interval"` // Default: 24 hours
CleanupInterval time.Duration `json:"cleanup_interval"` // Default: 1 hour
// Compression settings
EnableCompression bool `json:"enable_compression"` // Enable automatic compression
CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6)
PageSize int `json:"page_size"` // SQLite page size (default: 4096)
EnableCompression bool `json:"enable_compression"` // Enable automatic compression
CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6)
PageSize int `json:"page_size"` // SQLite page size (default: 4096)
}
// AircraftHistoryRecord represents a stored aircraft position update
@ -93,18 +93,18 @@ type AirlineRecord struct {
// AirportRecord represents embedded airport data from OpenFlights
type AirportRecord struct {
ID int `json:"id"`
Name string `json:"name"`
City string `json:"city"`
Country string `json:"country"`
IATA string `json:"iata"`
ICAO string `json:"icao"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude int `json:"altitude"`
ID int `json:"id"`
Name string `json:"name"`
City string `json:"city"`
Country string `json:"country"`
IATA string `json:"iata"`
ICAO string `json:"icao"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude int `json:"altitude"`
TimezoneOffset float64 `json:"timezone_offset"`
DST string `json:"dst"`
Timezone string `json:"timezone"`
DST string `json:"dst"`
Timezone string `json:"timezone"`
}
// DatabaseError represents database operation errors
@ -240,7 +240,6 @@ func (db *Database) Health() error {
return db.conn.Ping()
}
// DefaultConfig returns the default database configuration
func DefaultConfig() *Config {
return &Config{

View file

@ -37,12 +37,12 @@ type DataSource struct {
// LoadResult contains the results of a data loading operation
type LoadResult struct {
Source string `json:"source"`
RecordsTotal int `json:"records_total"`
RecordsNew int `json:"records_new"`
RecordsError int `json:"records_error"`
Source string `json:"source"`
RecordsTotal int `json:"records_total"`
RecordsNew int `json:"records_new"`
RecordsError int `json:"records_error"`
Duration time.Duration `json:"duration"`
Errors []string `json:"errors,omitempty"`
Errors []string `json:"errors,omitempty"`
}
// NewDataLoader creates a new data loader with HTTP client
@ -208,10 +208,18 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc
active := len(record) > 7 && strings.Trim(record[7], `"`) == "Y"
// Convert \N to empty strings
if alias == "\\N" { alias = "" }
if iata == "\\N" { iata = "" }
if icao == "\\N" { icao = "" }
if callsign == "\\N" { callsign = "" }
if alias == "\\N" {
alias = ""
}
if iata == "\\N" {
iata = ""
}
if icao == "\\N" {
icao = ""
}
if callsign == "\\N" {
callsign = ""
}
_, err = insertStmt.Exec(id, name, alias, iata, icao, callsign, country, active, source.Name)
if err != nil {
@ -298,10 +306,18 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc
timezone := strings.Trim(record[11], `"`)
// Convert \N to empty strings
if iata == "\\N" { iata = "" }
if icao == "\\N" { icao = "" }
if dst == "\\N" { dst = "" }
if timezone == "\\N" { timezone = "" }
if iata == "\\N" {
iata = ""
}
if icao == "\\N" {
icao = ""
}
if dst == "\\N" {
dst = ""
}
if timezone == "\\N" {
timezone = ""
}
_, err = insertStmt.Exec(id, name, city, country, iata, icao, lat, lon, alt, tzOffset, dst, timezone, source.Name)
if err != nil {
@ -497,7 +513,6 @@ func (dl *DataLoader) recordDataSource(tx *sql.Tx, source DataSource) error {
return err
}
// ClearDataSource removes all data from a specific source
func (dl *DataLoader) ClearDataSource(sourceName string) error {
tx, err := dl.conn.Begin()

View file

@ -34,8 +34,8 @@ func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) {
if err != nil {
// Network issues in tests are acceptable
if strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") {
strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") {
t.Skipf("Skipping network test due to connectivity issue: %v", err)
}
t.Fatal("LoadDataSource failed:", err)
@ -73,8 +73,8 @@ func TestDataLoader_LoadOurAirports(t *testing.T) {
if err != nil {
// Network issues in tests are acceptable
if strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") {
strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") {
t.Skipf("Skipping network test due to connectivity issue: %v", err)
}
t.Fatal("LoadDataSource failed:", err)

View file

@ -21,15 +21,15 @@ func TestCallsignManager_ParseCallsign(t *testing.T) {
manager := NewCallsignManager(db.GetConnection())
testCases := []struct {
callsign string
expectedValid bool
expectedAirline string
expectedFlight string
callsign string
expectedValid bool
expectedAirline string
expectedFlight string
}{
{"UAL123", true, "UAL", "123"},
{"BA4567", true, "BA", "4567"},
{"AFR89", true, "AFR", "89"},
{"N123AB", false, "", ""}, // Aircraft registration, not callsign
{"N123AB", false, "", ""}, // Aircraft registration, not callsign
{"INVALID", false, "", ""}, // No numbers
{"123", false, "", ""}, // Only numbers
{"A", false, "", ""}, // Too short

View file

@ -86,9 +86,9 @@ func (om *OptimizationManager) OptimizeDatabase() error {
fmt.Println("Optimizing database for storage efficiency...")
// Apply storage-friendly pragmas
optimizations := []struct{
name string
query string
optimizations := []struct {
name string
query string
description string
}{
{"Auto VACUUM", "PRAGMA auto_vacuum = INCREMENTAL", "Enable incremental auto-vacuum"},
@ -184,13 +184,13 @@ func (om *OptimizationManager) GetOptimizationStats() (*OptimizationStats, error
// OptimizationStats holds database storage optimization statistics
type OptimizationStats struct {
DatabaseSize int64 `json:"database_size"`
PageSize int `json:"page_size"`
PageCount int `json:"page_count"`
UsedPages int `json:"used_pages"`
FreePages int `json:"free_pages"`
Efficiency float64 `json:"efficiency_percent"`
AutoVacuumEnabled bool `json:"auto_vacuum_enabled"`
LastVacuum time.Time `json:"last_vacuum"`
PageSize int `json:"page_size"`
PageCount int `json:"page_count"`
UsedPages int `json:"used_pages"`
FreePages int `json:"free_pages"`
Efficiency float64 `json:"efficiency_percent"`
AutoVacuumEnabled bool `json:"auto_vacuum_enabled"`
LastVacuum time.Time `json:"last_vacuum"`
}
// getDatabaseSize returns the current database file size in bytes

View file

@ -221,13 +221,13 @@ func TestOptimizationManager_InvalidPath(t *testing.T) {
func TestOptimizationStats_JSON(t *testing.T) {
stats := &OptimizationStats{
DatabaseSize: 1024000,
PageSize: 4096,
PageCount: 250,
UsedPages: 200,
FreePages: 50,
Efficiency: 80.0,
PageSize: 4096,
PageCount: 250,
UsedPages: 200,
FreePages: 50,
Efficiency: 80.0,
AutoVacuumEnabled: true,
LastVacuum: time.Now(),
LastVacuum: time.Now(),
}
// Test that all fields are accessible

View file

@ -55,13 +55,13 @@ type OriginConfig struct {
// - Concurrent broadcast system for WebSocket clients
// - CORS support for cross-origin web applications
type Server struct {
host string // Bind address for HTTP server
port int // TCP port for HTTP server
merger *merger.Merger // Data source for aircraft information
host string // Bind address for HTTP server
port int // TCP port for HTTP server
merger *merger.Merger // Data source for aircraft information
database *database.Database // Optional database for persistence
staticFiles embed.FS // Embedded static web assets
server *http.Server // HTTP server instance
origin OriginConfig // Geographic reference point
staticFiles embed.FS // Embedded static web assets
server *http.Server // HTTP server instance
origin OriginConfig // Geographic reference point
// WebSocket management
wsClients map[*websocket.Conn]bool // Active WebSocket client connections
@ -1036,7 +1036,7 @@ func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) {
}
response := map[string]interface{}{
"callsign": callsignInfo,
"callsign": callsignInfo,
"timestamp": time.Now().Unix(),
}