Merge remote-tracking branch 'origin/pr/559' into codex/pr-561-562-559

This commit is contained in:
Codex Integration
2026-09-12 15:53:45 -06:00
4 changed files with 70 additions and 14 deletions
@@ -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([]);
});
});
+10 -4
View File
@@ -152,6 +152,7 @@ import { useImperativeSource } from '@/components/map/hooks/useImperativeSource'
import { useDynamicMapLayersWorker } from '@/components/map/hooks/useDynamicMapLayersWorker';
import { useStaticMapLayersWorker } from '@/components/map/hooks/useStaticMapLayersWorker';
import { applyDynamicLayerInterp } from '@/components/map/applyDynamicLayerInterp';
import { filterShipsByActiveFilters } from '@/components/map/shipFilters';
import {
ClusterCountLabels,
TrackedFlightLabels,
@@ -1448,9 +1449,14 @@ const MaplibreViewer = ({
const shipClusters = useClusterLabels(mapRef, 'ships-clusters-layer', shipsGeoJSON);
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(
() => (activeLayers.ships_military ? buildCarriersGeoJSON(data?.ships) : null),
[activeLayers.ships_military, data?.ships],
() => (activeLayers.ships_military ? buildCarriersGeoJSON(carrierShips) : null),
[activeLayers.ships_military, carrierShips],
);
// 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) */}
{carriersGeoJSON && !selectedEntity && !isMapInteracting && data?.ships && (
<CarrierLabels ships={data.ships} inView={inView} interpShip={interpShip} />
{carriersGeoJSON && !selectedEntity && !isMapInteracting && carrierShips.length > 0 && (
<CarrierLabels ships={carrierShips} inView={inView} interpShip={interpShip} />
)}
{/* HTML labels for tracked yachts (pink owner names) */}
@@ -3,6 +3,7 @@
import { classifyAircraft } from '@/utils/aircraftClassification';
import type { Flight, Ship, SigintSignal } from '@/types/dashboard';
import type { FlightLayerConfig } from '@/components/map/geoJSONBuilders';
import { filterShipsByActiveFilters } from '@/components/map/shipFilters';
type BoundsTuple = [number, number, number, number];
type FC = GeoJSON.FeatureCollection | null;
@@ -523,16 +524,7 @@ function applyFilters(activeFilters: Record<string, string[]> | undefined) {
}
// ── Ships ──
let ships = 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;
});
}
const ships = dynamicData.ships ? filterShipsByActiveFilters(dynamicData.ships, f) : dynamicData.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;
});
}