mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-23 09:20:51 +02:00
Merge remote-tracking branch 'origin/pr/559' into codex/pr-561-562-559
This commit is contained in:
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { filterShipsByActiveFilters } from '@/components/map/shipFilters';
|
||||||
|
|
||||||
|
const nimitz = { name: 'USS Nimitz (CVN-68)', type: 'carrier' as const };
|
||||||
|
const eclipse = { name: 'ECLIPSE', type: 'yacht' as const };
|
||||||
|
const frigate = { name: 'HMS Diamond', type: 'military_vessel' as const };
|
||||||
|
const ships = [nimitz, eclipse, frigate];
|
||||||
|
|
||||||
|
describe('filterShipsByActiveFilters', () => {
|
||||||
|
it('returns the same array when no ship filter is set', () => {
|
||||||
|
expect(filterShipsByActiveFilters(ships, undefined)).toBe(ships);
|
||||||
|
expect(filterShipsByActiveFilters(ships, { ship_type: [] })).toBe(ships);
|
||||||
|
expect(filterShipsByActiveFilters(ships, { commercial_airline: ['UAL'] })).toBe(ships);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps carriers only when their own type is selected', () => {
|
||||||
|
expect(filterShipsByActiveFilters(ships, { ship_type: ['yacht'] })).toEqual([eclipse]);
|
||||||
|
expect(filterShipsByActiveFilters(ships, { ship_type: ['military_vessel'] })).toEqual([frigate]);
|
||||||
|
expect(filterShipsByActiveFilters(ships, { ship_type: ['carrier', 'yacht'] })).toEqual([
|
||||||
|
nimitz,
|
||||||
|
eclipse,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines name and type filters', () => {
|
||||||
|
expect(
|
||||||
|
filterShipsByActiveFilters(ships, { ship_name: ['ECLIPSE'], ship_type: ['carrier'] }),
|
||||||
|
).toEqual([]);
|
||||||
|
expect(filterShipsByActiveFilters(ships, { ship_name: ['USS Nimitz (CVN-68)'] })).toEqual([
|
||||||
|
nimitz,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a missing ship array', () => {
|
||||||
|
expect(filterShipsByActiveFilters(undefined, { ship_type: ['yacht'] })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -152,6 +152,7 @@ import { useImperativeSource } from '@/components/map/hooks/useImperativeSource'
|
|||||||
import { useDynamicMapLayersWorker } from '@/components/map/hooks/useDynamicMapLayersWorker';
|
import { useDynamicMapLayersWorker } from '@/components/map/hooks/useDynamicMapLayersWorker';
|
||||||
import { useStaticMapLayersWorker } from '@/components/map/hooks/useStaticMapLayersWorker';
|
import { useStaticMapLayersWorker } from '@/components/map/hooks/useStaticMapLayersWorker';
|
||||||
import { applyDynamicLayerInterp } from '@/components/map/applyDynamicLayerInterp';
|
import { applyDynamicLayerInterp } from '@/components/map/applyDynamicLayerInterp';
|
||||||
|
import { filterShipsByActiveFilters } from '@/components/map/shipFilters';
|
||||||
import {
|
import {
|
||||||
ClusterCountLabels,
|
ClusterCountLabels,
|
||||||
TrackedFlightLabels,
|
TrackedFlightLabels,
|
||||||
@@ -1448,9 +1449,14 @@ const MaplibreViewer = ({
|
|||||||
const shipClusters = useClusterLabels(mapRef, 'ships-clusters-layer', shipsGeoJSON);
|
const shipClusters = useClusterLabels(mapRef, 'ships-clusters-layer', shipsGeoJSON);
|
||||||
const eqClusters = useClusterLabels(mapRef, 'eq-clusters-layer', earthquakesGeoJSON);
|
const eqClusters = useClusterLabels(mapRef, 'eq-clusters-layer', earthquakesGeoJSON);
|
||||||
|
|
||||||
|
// Carriers bypass the worker, so apply the operator's vessel filters here.
|
||||||
|
const carrierShips = useMemo(
|
||||||
|
() => (activeLayers.ships_military ? filterShipsByActiveFilters(data?.ships, activeFilters) : []),
|
||||||
|
[activeLayers.ships_military, data?.ships, activeFilters],
|
||||||
|
);
|
||||||
const carriersGeoJSON = useMemo(
|
const carriersGeoJSON = useMemo(
|
||||||
() => (activeLayers.ships_military ? buildCarriersGeoJSON(data?.ships) : null),
|
() => (activeLayers.ships_military ? buildCarriersGeoJSON(carrierShips) : null),
|
||||||
[activeLayers.ships_military, data?.ships],
|
[activeLayers.ships_military, carrierShips],
|
||||||
);
|
);
|
||||||
|
|
||||||
// SAR anomaly pins (Mode B) + AOI watchbox circles. AOIs render whenever
|
// SAR anomaly pins (Mode B) + AOI watchbox circles. AOIs render whenever
|
||||||
@@ -4482,8 +4488,8 @@ const MaplibreViewer = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* HTML labels for carriers (orange names, with ESTIMATED badge for OSINT positions) */}
|
{/* HTML labels for carriers (orange names, with ESTIMATED badge for OSINT positions) */}
|
||||||
{carriersGeoJSON && !selectedEntity && !isMapInteracting && data?.ships && (
|
{carriersGeoJSON && !selectedEntity && !isMapInteracting && carrierShips.length > 0 && (
|
||||||
<CarrierLabels ships={data.ships} inView={inView} interpShip={interpShip} />
|
<CarrierLabels ships={carrierShips} inView={inView} interpShip={interpShip} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* HTML labels for tracked yachts (pink owner names) */}
|
{/* HTML labels for tracked yachts (pink owner names) */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { classifyAircraft } from '@/utils/aircraftClassification';
|
import { classifyAircraft } from '@/utils/aircraftClassification';
|
||||||
import type { Flight, Ship, SigintSignal } from '@/types/dashboard';
|
import type { Flight, Ship, SigintSignal } from '@/types/dashboard';
|
||||||
import type { FlightLayerConfig } from '@/components/map/geoJSONBuilders';
|
import type { FlightLayerConfig } from '@/components/map/geoJSONBuilders';
|
||||||
|
import { filterShipsByActiveFilters } from '@/components/map/shipFilters';
|
||||||
|
|
||||||
type BoundsTuple = [number, number, number, number];
|
type BoundsTuple = [number, number, number, number];
|
||||||
type FC = GeoJSON.FeatureCollection | null;
|
type FC = GeoJSON.FeatureCollection | null;
|
||||||
@@ -523,16 +524,7 @@ function applyFilters(activeFilters: Record<string, string[]> | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Ships ──
|
// ── Ships ──
|
||||||
let ships = dynamicData.ships;
|
const ships = dynamicData.ships ? filterShipsByActiveFilters(dynamicData.ships, f) : dynamicData.ships;
|
||||||
if (ships && (has('ship_name') || has('ship_type'))) {
|
|
||||||
const nameSet = has('ship_name') ? set('ship_name') : null;
|
|
||||||
const typeSet = has('ship_type') ? set('ship_type') : null;
|
|
||||||
ships = ships.filter((s: any) => {
|
|
||||||
if (nameSet && !nameSet.has(s.name)) return false;
|
|
||||||
if (typeSet && !typeSet.has(s.type)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { commercial, private_, jets, military, tracked, ships };
|
return { commercial, private_, jets, military, tracked, ships };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Ship } from '@/types/dashboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator vessel filters from the Data Filters panel. Shared by the worker
|
||||||
|
* (regular ship icons) and the main thread (carrier icons and labels) so every
|
||||||
|
* ship source honours the same name / type selection.
|
||||||
|
*/
|
||||||
|
export function filterShipsByActiveFilters<T extends Pick<Ship, 'name' | 'type'>>(
|
||||||
|
ships: T[] | undefined,
|
||||||
|
activeFilters: Record<string, string[]> | undefined,
|
||||||
|
): T[] {
|
||||||
|
if (!ships) return [];
|
||||||
|
const nameSet = activeFilters?.ship_name?.length ? new Set(activeFilters.ship_name) : null;
|
||||||
|
const typeSet = activeFilters?.ship_type?.length ? new Set(activeFilters.ship_type) : null;
|
||||||
|
if (!nameSet && !typeSet) return ships;
|
||||||
|
return ships.filter((s) => {
|
||||||
|
if (nameSet && !nameSet.has(s.name)) return false;
|
||||||
|
if (typeSet && !typeSet.has(s.type)) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user