mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 07:13:53 +02:00
release: prepare v0.9.7
This commit is contained in:
@@ -80,6 +80,32 @@ export const HELI_TYPES = new Set([
|
||||
'MRLX',
|
||||
'A149',
|
||||
'A119',
|
||||
// Common heli typecodes seen on the wire that were missing
|
||||
'B47G', // Bell 47
|
||||
'H500', // MD 500 / Hughes 500
|
||||
'H269', // Hughes 269/300
|
||||
'EC30', // EC130 / H130 (also alias)
|
||||
'EC35', // EC135 (also alias)
|
||||
'EC45', // EC145 (also alias)
|
||||
'EC75', // EC175
|
||||
'A169', // AW169
|
||||
'A189', // AW189
|
||||
'AW69', // AW69
|
||||
'H60', // UH-60 / S-70 Black Hawk (military variants on the wire)
|
||||
'H47', // CH-47 Chinook
|
||||
'H53', // CH-53
|
||||
'H64', // AH-64 Apache (alt code seen on the wire)
|
||||
'V22', // V-22 Osprey (rotorcraft for icon purposes)
|
||||
'KA32', // Kamov Ka-32
|
||||
'KA50', // Ka-50
|
||||
'MI17', // Mi-17
|
||||
'MI171', // Mi-171
|
||||
'MI2', // Mi-2
|
||||
'M530', // MD 530
|
||||
'EXPL', // MD Explorer
|
||||
'GA6C', // (some heli ICAOs)
|
||||
'CABR', // Cabri G2
|
||||
'SK76', // Sikorsky S-76 alt
|
||||
]);
|
||||
export const TURBOPROP_TYPES = new Set([
|
||||
'AT43',
|
||||
@@ -201,6 +227,98 @@ export const TURBOPROP_TYPES = new Set([
|
||||
'DR40',
|
||||
'TB20',
|
||||
'AA5',
|
||||
// Common GA / sport / utility / military-utility typecodes seen on the wire
|
||||
// that were defaulting to airliner shape
|
||||
'AN2', // Antonov An-2
|
||||
'T6', // T-6 Texan / II
|
||||
'TEX2', // T-6A Texan II
|
||||
'PA11', // PA-11 Cub
|
||||
'PA22', // PA-22 Tri-Pacer
|
||||
'PA24', // PA-24 Comanche
|
||||
'PA25', // PA-25 Pawnee
|
||||
'PA38', // PA-38 Tomahawk
|
||||
'PA46', // PA-46 Malibu / Mirage / Matrix
|
||||
'P32R', // PA-32R Lance/Saratoga
|
||||
'P46T', // PA-46 Meridian (turboprop)
|
||||
'C150', // Cessna 150
|
||||
'C152', // Cessna 152
|
||||
'C170', // Cessna 170
|
||||
'C177', // Cessna 177 Cardinal
|
||||
'C180', // Cessna 180
|
||||
'C185', // Cessna 185
|
||||
'C140', // Cessna 140
|
||||
'C120', // Cessna 120
|
||||
'C175', // Cessna 175
|
||||
'C72R', // Cessna 172 RG
|
||||
'C77R', // Cessna 177 RG
|
||||
'C82R', // Cessna 182 RG
|
||||
'C82S', // Cessna 182 S
|
||||
'C82T', // Cessna 182 T
|
||||
'T206', // Cessna T206 Stationair
|
||||
'T210', // Cessna T210 Centurion
|
||||
'C340', // Cessna 340
|
||||
'C56X', // Citation Excel/XLS (covered in BIZJET, but harmless)
|
||||
'M7', // Maule M-7
|
||||
'M20T', // Mooney M20 Turbo
|
||||
'BE9T', // King Air F90
|
||||
'BE9L', // King Air 90 (already in main)
|
||||
'BE10', // King Air 100
|
||||
'BE30', // King Air 300 (already in main)
|
||||
'BE76', // Beech Duchess
|
||||
'BE95', // Beech Travel Air
|
||||
'BE23', // Sundowner
|
||||
'BE40', // Beechjet 400 (also in BIZJET, harmless)
|
||||
'BE55', // Baron 55 (already)
|
||||
'GA8', // GippsAero Airvan
|
||||
'AC68', // Aero Commander 680
|
||||
'AC80', // Aero Commander 680
|
||||
'AC90', // Aero Commander 90
|
||||
'AC95', // Aero Commander 95
|
||||
'CH7A', // Champion 7A
|
||||
'CH7B', // Champion 7B
|
||||
'CH60', // Champion 60
|
||||
'BL8', // Bellanca 8 Decathlon
|
||||
'TBM7', // (also already)
|
||||
'TBM8',
|
||||
'TBM9',
|
||||
'M600', // Piper M600
|
||||
'PC21', // Pilatus PC-21
|
||||
'P180', // Piaggio Avanti
|
||||
'CN35', // CASA CN-235
|
||||
'C295', // CASA C-295
|
||||
'C212', // CASA C-212
|
||||
'D228', // Dornier 228
|
||||
'D328', // Dornier 328
|
||||
'L410', // LET L-410
|
||||
'AN24', // Antonov An-24
|
||||
'AN26', // An-26
|
||||
'AN30', // An-30
|
||||
'AN32', // An-32
|
||||
'YK40', // Yak-40
|
||||
'YK42', // Yak-42 (regional)
|
||||
'PARA', // skydiving
|
||||
'GLID', // glider
|
||||
'BALL', // balloon
|
||||
'ULAC', // ultralight
|
||||
'GYRO', // gyrocopter
|
||||
'DRON', // drone
|
||||
'FOX', // Aviat Husky / sim variants
|
||||
'HUSK', // Husky
|
||||
'NAVI', // Navion
|
||||
'AC11', // Grumman AA-1
|
||||
'AA5', // Grumman AA-5
|
||||
'RV4', 'RV6', 'RV7', 'RV8', 'RV9', 'RV10', 'RV12',
|
||||
'GLAS', // Glasair
|
||||
'ERCO', // Ercoupe
|
||||
'TAYB', // Taylorcraft
|
||||
'S108', // Stinson 108
|
||||
'S22T', // SR22T
|
||||
'DV20', // Diamond Katana
|
||||
'DA40', 'DA42', 'DA62',
|
||||
'SR20', 'SR22',
|
||||
'M20P',
|
||||
'P28A', 'P28B', 'P28R', 'P28', 'P32R',
|
||||
'C172', 'C182', 'C206',
|
||||
]);
|
||||
export const BIZJET_TYPES = new Set([
|
||||
'ASTR',
|
||||
@@ -287,5 +405,9 @@ export function classifyAircraft(
|
||||
if (category === 'heli' || HELI_TYPES.has(m)) return 'heli';
|
||||
if (BIZJET_TYPES.has(m)) return 'bizjet';
|
||||
if (TURBOPROP_TYPES.has(m)) return 'turboprop';
|
||||
// Default airliner shape — restores the original behavior where unknown /
|
||||
// unrecognized types render as the standard plane silhouette. The earlier
|
||||
// attempt to default-down to turboprop made every unidentified flight look
|
||||
// smaller and slimmer than it should.
|
||||
return 'airliner';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
* Alert spread collision resolution algorithm.
|
||||
* Takes news items with coordinates and resolves visual overlaps
|
||||
* so alert boxes don't stack on top of each other on the map.
|
||||
*
|
||||
* At very low zoom (< 3.5), applies geospatial clustering to merge
|
||||
* nearby alerts into single cluster badges before running spread.
|
||||
*/
|
||||
|
||||
import type { NewsArticle } from '@/types/dashboard';
|
||||
@@ -23,26 +26,62 @@ export interface SpreadAlertItem extends NewsArticle {
|
||||
/** Estimate rendered box height based on title length */
|
||||
function estimateBoxH(n: { title?: string; cluster_count?: number }): number {
|
||||
const titleLen = (n.title || '').length;
|
||||
// Title wraps at ~22 chars per line inside 260px maxWidth at 12px font
|
||||
const titleLines = Math.max(1, Math.ceil(titleLen / 22));
|
||||
const titleLines = Math.max(1, Math.ceil(titleLen / 20)); // ~20 chars per line at 9px in 160px
|
||||
const hasFooter = (n.cluster_count || 1) > 1;
|
||||
// padding(8+8) + header("!! ALERT LVL X !!" ~20px) + gap(4) + title(lines*17) + footer(18) + padding
|
||||
return 16 + 20 + 4 + titleLines * 17 + (hasFooter ? 18 : 0) + 8;
|
||||
return 10 + 14 + titleLines * 13 + (hasFooter ? 14 : 0) + 10; // padding + header + title + footer + padding
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves alert box collisions using iterative repulsion.
|
||||
* Higher-risk alerts get priority (sorted first, pushed less).
|
||||
* Pre-cluster nearby articles at low zoom to reduce visual clutter.
|
||||
* Uses a simple grid-based spatial merge: articles within `cellDeg` degrees
|
||||
* are collapsed into a single representative (the highest risk_score article).
|
||||
*/
|
||||
function clusterArticles(
|
||||
articles: NewsArticle[],
|
||||
cellDeg: number,
|
||||
): NewsArticle[] {
|
||||
const buckets = new Map<string, NewsArticle[]>();
|
||||
|
||||
for (const a of articles) {
|
||||
if (!a.coords) continue;
|
||||
const cx = Math.floor(a.coords[0] / cellDeg);
|
||||
const cy = Math.floor(a.coords[1] / cellDeg);
|
||||
const key = `${cx},${cy}`;
|
||||
const bucket = buckets.get(key);
|
||||
if (bucket) bucket.push(a);
|
||||
else buckets.set(key, [a]);
|
||||
}
|
||||
|
||||
const results: NewsArticle[] = [];
|
||||
for (const bucket of buckets.values()) {
|
||||
// Sort by risk_score descending — the top article becomes the representative
|
||||
bucket.sort((a, b) => (b.risk_score || 0) - (a.risk_score || 0));
|
||||
const rep = { ...bucket[0] };
|
||||
// Attach cluster_count to the representative
|
||||
(rep as NewsArticle & { cluster_count?: number }).cluster_count = bucket.length;
|
||||
results.push(rep);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves alert box collisions using a grid-based spatial algorithm (O(n) per iteration).
|
||||
* Returns positioned items with offsets and alert keys.
|
||||
*/
|
||||
export function spreadAlertItems(
|
||||
news: NewsArticle[],
|
||||
zoom: number,
|
||||
dismissedAlerts: Set<string>,
|
||||
dismissedAlerts: Set<string> = new Set(),
|
||||
): SpreadAlertItem[] {
|
||||
// At low zoom, pre-cluster nearby alerts to reduce clutter
|
||||
const effectiveNews = zoom < 3.5
|
||||
? clusterArticles(news, zoom < 2 ? 15 : 8)
|
||||
: news;
|
||||
|
||||
const pixelsPerDeg = (256 * Math.pow(2, zoom)) / 360;
|
||||
|
||||
const items = news
|
||||
const items = effectiveNews
|
||||
.map((n, idx) => ({ ...n, originalIdx: idx }))
|
||||
.filter((n) => n.coords)
|
||||
.map((n) => ({
|
||||
@@ -54,17 +93,14 @@ export function spreadAlertItems(
|
||||
boxH: estimateBoxH(n as { title?: string; cluster_count?: number }),
|
||||
}));
|
||||
|
||||
// Sort by risk score descending — high-risk alerts stay closer to origin
|
||||
items.sort((a, b) => ((b as any).risk_score || 0) - ((a as any).risk_score || 0));
|
||||
|
||||
const BOX_W = ALERT_BOX_WIDTH_PX;
|
||||
const GAP = 12; // Increased gap for breathing room
|
||||
const GAP = 6;
|
||||
const MAX_OFFSET = ALERT_MAX_OFFSET_PX;
|
||||
|
||||
// Grid-based Collision Resolution
|
||||
// Grid-based Collision Resolution (O(n) per iteration instead of O(n²))
|
||||
const CELL_W = BOX_W + GAP;
|
||||
const CELL_H = 80; // Smaller cells = better overlap detection
|
||||
const maxIter = 60; // More iterations for dense clusters
|
||||
const CELL_H = 100;
|
||||
const maxIter = 30;
|
||||
|
||||
for (let iter = 0; iter < maxIter; iter++) {
|
||||
let moved = false;
|
||||
@@ -95,41 +131,31 @@ export function spreadAlertItems(
|
||||
if (i === j) continue;
|
||||
const a = items[i],
|
||||
b = items[j];
|
||||
const ax = a.x + a.offsetX,
|
||||
ay = a.y + a.offsetY;
|
||||
const bx = b.x + b.offsetX,
|
||||
by = b.y + b.offsetY;
|
||||
const adx = Math.abs(ax - bx);
|
||||
const ady = Math.abs(ay - by);
|
||||
const adx = Math.abs(a.x + a.offsetX - (b.x + b.offsetX));
|
||||
const ady = Math.abs(a.y + a.offsetY - (b.y + b.offsetY));
|
||||
const minDistX = BOX_W + GAP;
|
||||
const minDistY = (a.boxH + b.boxH) / 2 + GAP;
|
||||
if (adx < minDistX && ady < minDistY) {
|
||||
moved = true;
|
||||
const overlapX = minDistX - adx;
|
||||
const overlapY = minDistY - ady;
|
||||
|
||||
// Higher-index items (lower risk) get pushed more
|
||||
// This keeps high-risk alerts closer to their true position
|
||||
const weightA = i < j ? 0.35 : 0.65;
|
||||
const weightB = 1 - weightA;
|
||||
|
||||
if (overlapY < overlapX) {
|
||||
const push = overlapY + 2;
|
||||
if (ay <= by) {
|
||||
a.offsetY -= push * weightA;
|
||||
b.offsetY += push * weightB;
|
||||
const push = overlapY / 2 + 1;
|
||||
if (a.y + a.offsetY <= b.y + b.offsetY) {
|
||||
a.offsetY -= push;
|
||||
b.offsetY += push;
|
||||
} else {
|
||||
a.offsetY += push * weightA;
|
||||
b.offsetY -= push * weightB;
|
||||
a.offsetY += push;
|
||||
b.offsetY -= push;
|
||||
}
|
||||
} else {
|
||||
const push = overlapX + 2;
|
||||
if (ax <= bx) {
|
||||
a.offsetX -= push * weightA;
|
||||
b.offsetX += push * weightB;
|
||||
const push = overlapX / 2 + 1;
|
||||
if (a.x + a.offsetX <= b.x + b.offsetX) {
|
||||
a.offsetX -= push;
|
||||
b.offsetX += push;
|
||||
} else {
|
||||
a.offsetX += push * weightA;
|
||||
b.offsetX -= push * weightB;
|
||||
a.offsetX += push;
|
||||
b.offsetX -= push;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user