diff --git a/apps/dispatch/app/_components/left/SettingsBoard.tsx b/apps/dispatch/app/_components/left/SettingsBoard.tsx index f5b4fd8f..32e97aec 100644 --- a/apps/dispatch/app/_components/left/SettingsBoard.tsx +++ b/apps/dispatch/app/_components/left/SettingsBoard.tsx @@ -2,10 +2,12 @@ import { useLeftMenuStore } from "_store/leftMenuStore"; import { cn } from "@repo/shared-components"; import { SettingsIcon } from "lucide-react"; +import { usePilotConnectionStore } from "_store/pilot/connectionStore"; export const SettingsBoard = () => { const { setSituationTabOpen, situationTabOpen } = useLeftMenuStore(); - + const { followOwnAircraft, showOtherAircrafts, showOtherMissions, setMapOptions } = + usePilotConnectionStore(); const cross = ( {
@@ -67,7 +73,11 @@ export const SettingsBoard = () => {
@@ -75,7 +85,11 @@ export const SettingsBoard = () => {
diff --git a/apps/dispatch/app/_components/map/AircraftMarker.tsx b/apps/dispatch/app/_components/map/AircraftMarker.tsx index 28fee344..b1635b85 100644 --- a/apps/dispatch/app/_components/map/AircraftMarker.tsx +++ b/apps/dispatch/app/_components/map/AircraftMarker.tsx @@ -16,6 +16,7 @@ import { useQuery } from "@tanstack/react-query"; import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getMissionsAPI } from "_querys/missions"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; +import { usePilotConnectionStore } from "_store/pilot/connectionStore"; const AircraftPopupContent = ({ aircraft, @@ -396,10 +397,42 @@ export const AircraftLayer = () => { queryFn: () => getConnectedAircraftsAPI(), refetchInterval: 10_000, }); + const { setMap } = useMapStore((state) => state); + const map = useMap(); + const { + connectedAircraft, + status: pilotConnectionStatus, + showOtherAircrafts, + followOwnAircraft, + } = usePilotConnectionStore((state) => state); + + const filteredAircrafts = useMemo(() => { + if (!aircrafts) return []; + return aircrafts.filter((aircraft) => { + if (pilotConnectionStatus === "connected" && !showOtherAircrafts) { + return connectedAircraft?.stationId === aircraft.stationId; + } + return true; + }); + }, [aircrafts, pilotConnectionStatus, connectedAircraft, showOtherAircrafts]); + + const ownAircraft = useMemo(() => { + return aircrafts?.find((aircraft) => aircraft.id === connectedAircraft?.id); + }, [aircrafts, connectedAircraft]); + + useEffect(() => { + if (pilotConnectionStatus === "connected" && followOwnAircraft && ownAircraft) { + if (!ownAircraft.posLat || !ownAircraft.posLng) return; + setMap({ + center: [ownAircraft.posLat, ownAircraft.posLng], + zoom: map.getZoom(), + }); + } + }, [pilotConnectionStatus, followOwnAircraft, ownAircraft, setMap, map]); return ( <> - {aircrafts?.map((aircraft) => { + {filteredAircrafts?.map((aircraft) => { return ; })} diff --git a/apps/dispatch/app/_components/map/MissionMarkers.tsx b/apps/dispatch/app/_components/map/MissionMarkers.tsx index 5b186776..b7502b3e 100644 --- a/apps/dispatch/app/_components/map/MissionMarkers.tsx +++ b/apps/dispatch/app/_components/map/MissionMarkers.tsx @@ -17,6 +17,7 @@ import { getMissionsAPI } from "_querys/missions"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired"; import { getConnectedAircraftsAPI } from "_querys/aircrafts"; +import { usePilotConnectionStore } from "_store/pilot/connectionStore"; export const MISSION_STATUS_COLORS: Record = { draft: "#0092b8", @@ -396,6 +397,11 @@ const MissionMarker = ({ mission }: { mission: Mission }) => { export const MissionLayer = () => { const dispatchState = useDispatchConnectionStore((s) => s); const dispatcherConnected = dispatchState.status === "connected"; + const { + status: pilotConnectionStatus, + showOtherMissions, + selectedStation, + } = usePilotConnectionStore((state) => state); const { data: missions = [] } = useQuery({ queryKey: ["missions"], @@ -410,9 +416,18 @@ export const MissionLayer = () => { return missions.filter((m: Mission) => { if (m.state === "draft" && !dispatcherConnected) return false; if (dispatchState.hideDraftMissions && m.state === "draft") return false; + if (pilotConnectionStatus === "connected" && !showOtherMissions) + return m.missionStationIds.includes(selectedStation!.id); return true; }); - }, [missions, dispatcherConnected, dispatchState.hideDraftMissions]); + }, [ + missions, + dispatcherConnected, + dispatchState.hideDraftMissions, + pilotConnectionStatus, + showOtherMissions, + selectedStation, + ]); // IDEA: Add Marker to Map Layer / LayerGroup return ( diff --git a/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx b/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx index d63cca8e..a8a342a1 100644 --- a/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx +++ b/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx @@ -11,6 +11,7 @@ import { getMissionsAPI } from "_querys/missions"; import { useEffect, useMemo, useState } from "react"; import { useMap } from "react-leaflet"; import { HPGValidationRequired } from "_helpers/hpgValidationRequired"; +import { usePilotConnectionStore } from "_store/pilot/connectionStore"; const PopupContent = ({ aircrafts, @@ -136,6 +137,7 @@ const PopupContent = ({ export const MarkerCluster = () => { const map = useMap(); const dispatchState = useDispatchConnectionStore((s) => s); + const pilotState = usePilotConnectionStore((s) => s); const dispatcherConnected = dispatchState.status === "connected"; const { data: aircrafts } = useQuery({ queryKey: ["aircrafts"], @@ -155,9 +157,36 @@ export const MarkerCluster = () => { return missions.filter((m: Mission) => { if (m.state === "draft" && !dispatcherConnected) return false; if (dispatchState.hideDraftMissions && m.state === "draft") return false; + if ( + pilotState.status === "connected" && + !pilotState.showOtherMissions && + pilotState.selectedStation + ) + return m.missionStationIds.includes(pilotState.selectedStation.id); return true; }); - }, [missions, dispatcherConnected, dispatchState.hideDraftMissions]); + }, [ + missions, + dispatcherConnected, + dispatchState.hideDraftMissions, + pilotState.selectedStation, + pilotState.showOtherMissions, + pilotState.status, + ]); + + const filteredAircrafts = useMemo(() => { + return aircrafts?.filter((a: ConnectedAircraft) => { + if (pilotState.status === "connected" && !pilotState.showOtherAircrafts) { + return a.stationId === pilotState.connectedAircraft?.stationId; + } + return true; + }); + }, [ + aircrafts, + pilotState.status, + pilotState.showOtherAircrafts, + pilotState.connectedAircraft?.stationId, + ]); // Track zoom level in state const [zoom, setZoom] = useState(() => map.getZoom()); @@ -178,7 +207,7 @@ export const MarkerCluster = () => { lat: number; lng: number; }[] = []; - aircrafts?.forEach((aircraft) => { + filteredAircrafts?.forEach((aircraft) => { const lat = aircraft.posLat!; const lng = aircraft.posLng!; @@ -255,7 +284,7 @@ export const MarkerCluster = () => { }); return clusterWithAvgPos; - }, [aircrafts, filteredMissions, zoom]); + }, [filteredAircrafts, filteredMissions, zoom]); return ( <> diff --git a/apps/dispatch/app/_helpers/radioAudio.ts b/apps/dispatch/app/_helpers/radioEffect.ts similarity index 90% rename from apps/dispatch/app/_helpers/radioAudio.ts rename to apps/dispatch/app/_helpers/radioEffect.ts index ab2a7f88..a7e486b1 100644 --- a/apps/dispatch/app/_helpers/radioAudio.ts +++ b/apps/dispatch/app/_helpers/radioEffect.ts @@ -1,3 +1,5 @@ +import { AudioTrack, RemoteAudioTrack, RemoteTrack } from "livekit-client"; + // Helper function for distortion curve generation function createDistortionCurve(amount: number): Float32Array { const k = typeof amount === "number" ? amount : 50; @@ -12,10 +14,12 @@ function createDistortionCurve(amount: number): Float32Array { return curve; } -export const getRadioStream = (stream: MediaStream, volume: number): MediaStream | null => { +export const getRadioStream = (track: RemoteAudioTrack, volume: number): MediaStream | null => { try { const audioContext = new window.AudioContext(); - const sourceNode = audioContext.createMediaStreamSource(stream); + const sourceNode = audioContext.createMediaStreamSource( + new MediaStream([track.mediaStreamTrack]), + ); const destinationNode = audioContext.createMediaStreamDestination(); const gainNode = audioContext.createGain(); diff --git a/apps/dispatch/app/_store/mapStore.ts b/apps/dispatch/app/_store/mapStore.ts index 01d308c6..b6f06493 100644 --- a/apps/dispatch/app/_store/mapStore.ts +++ b/apps/dispatch/app/_store/mapStore.ts @@ -6,6 +6,7 @@ export interface MapStore { lat: number; lng: number; } | null; + map: { center: L.LatLngExpression; zoom: number; diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 0693d92f..c7acd60c 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -27,6 +27,14 @@ interface ConnectionStore { debug?: boolean, ) => Promise; disconnect: () => void; + followOwnAircraft: boolean; + showOtherAircrafts: boolean; + showOtherMissions: boolean; + setMapOptions: (options: { + followOwnAircraft?: boolean; + showOtherAircrafts?: boolean; + showOtherMissions?: boolean; + }) => void; } export const usePilotConnectionStore = create((set) => ({ @@ -37,7 +45,15 @@ export const usePilotConnectionStore = create((set) => ({ connectedAircraft: null, activeMission: null, debug: false, - + followOwnAircraft: false, + showOtherAircrafts: false, + showOtherMissions: false, + setMapOptions(options) { + set((state) => ({ + ...state, + ...options, + })); + }, connect: async (uid, stationId, logoffTime, station, user, debug) => new Promise((resolve) => { set({