Working on metars, updating ui drawer

This commit is contained in:
2025-04-16 22:43:03 -04:00
parent 81335f1b48
commit 3aa8954626
9 changed files with 279 additions and 139 deletions

View File

@@ -1,8 +1,9 @@
import { Box, Divider, Drawer, Group, Text } from '@mantine/core';
import { Box, Drawer, Group, Tabs, TabsList, Text, Tooltip } from '@mantine/core';
import { Airport, AirportCategory } from '@lib/airport.types.ts';
import { getMarkerColor, Metar } from '@lib/metar.types.ts';
import { useEffect, useState } from 'react';
import { getMetars } from '@lib/metar.ts';
import { useMediaQuery } from '@mantine/hooks';
export default function AirportDrawer({
airport,
@@ -12,15 +13,27 @@ export default function AirportDrawer({
setAirport: (airport: Airport | null) => void;
}) {
const [metar, setMetar] = useState<Metar | undefined>(undefined);
const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => {
if (airport != null) {
if (!airport) return;
function updateMetar() {
if (!airport) return;
console.log(airport.icao);
getMetars({ icaos: [airport.icao] }).then((m) => {
if (m.length > 0) {
setMetar(m[0]);
} else {
setMetar(undefined);
}
});
}
updateMetar();
const interval = setInterval(updateMetar, 60000);
return () => clearInterval(interval);
}, [airport]);
if (!airport) {
@@ -35,47 +48,58 @@ export default function AirportDrawer({
onClose={() => setAirport(null)}
title={airport.name}
withinPortal
zIndex={10000}
styles={{ root: { width: 0, height: 0 } }}
zIndex={1000}
styles={{ root: { padding: 0, margin: 0, width: 0, height: 0 } }}
padding='md'
size='md'
size={isMobile ? '100%' : 'md'}
position='left'
withOverlay={false}
closeOnClickOutside={false}
>
<Box mb='lg'>
{metar && metar.flight_category && (
<Group justify='space-between' mb='md'>
<Group
justify='space-between'
mb='md'
style={{
backgroundColor: '#272f38',
borderTop: '1px solid #1a242f',
borderBottom: '1px solid #1a242f',
padding: '10px'
}}
>
<Text style={{ color: metarColor }}>{metar.flight_category}</Text>
<Text size='sm'>{metar.observation_time ? new Date(metar.observation_time).toLocaleString() : 'N/A'}</Text>
<Tooltip zIndex={1001} label={new Date(metar.observation_time).toLocaleString()}>
<TimeSince date={metar.observation_time} />
</Tooltip>
</Group>
)}
<Group>
<div>ICAO: {airport.icao}</div>
<div>Category: {airportCategoryToText(airport.category)}</div>
<div>
Country / Region: {airport.iso_country}, {airport.iso_region}
</div>
<div>Municipality: {airport.municipality || 'N/A'}</div>
<div>Local Code: {airport.local || 'N/A'}</div>
<div>Elevation: {airport.elevation_ft}</div>
<div>
Coordinates: {airport.latitude.toFixed(4)}, {airport.longitude.toFixed(4)}
</div>
<div>Control Tower: {airport.has_tower ? 'Yes' : 'No'}</div>
<div>Beacon: {airport.has_beacon ? 'Yes' : 'No'}</div>
{metar && metar.flight_category && (
<>
<Divider my='sm' />
<div>Flight Category: {metar.flight_category}</div>
</>
<Tabs variant={'outline'} defaultValue={'info'}>
<TabsList grow>
<Tabs.Tab value={'info'}>Info</Tabs.Tab>
<Tabs.Tab value={'weather'}>Weather</Tabs.Tab>
</TabsList>
<Tabs.Panel value={'info'}><AirportInfo airport={airport}/></Tabs.Panel>
{airport.latest_metar && (
<Tabs.Panel value={'weather'}><WeatherInfo metar={airport.latest_metar} /></Tabs.Panel>
)}
</Group>
</Tabs>
</Box>
</Drawer>
);
}
function AirportInfo({ airport }: { airport: Airport }) {
return (<div>
<Text>ICAO: {airport.icao}</Text>
<Text>Category: {airportCategoryToText(airport.category)}</Text>
</div>);
}
function WeatherInfo({ metar }: { metar: Metar }) {
return <>{metar.raw_text}</>
}
function airportCategoryToText(category: AirportCategory): string {
switch (category) {
case AirportCategory.SMALL:
@@ -96,3 +120,19 @@ function airportCategoryToText(category: AirportCategory): string {
return 'Unknown';
}
}
function TimeSince({ date }: { date: string }) {
const inputDate = new Date(date);
// @ts-expect-error doing arithmetic with dates
const seconds = Math.floor((new Date() - inputDate) / 1000);
if (seconds < 60) {
const content = seconds + (seconds === 1 ? " second ago" : " seconds ago");
return <Text>{content}</Text>;
} else {
const minutes = Math.floor(seconds / 60);
const content = minutes + (minutes === 1 ? " minute ago" : " minutes ago");
// If more than 60 minutes have passed, set the text color to yellow
return <Text style={{ color: minutes >= 60 ? '#fca903' : undefined }}>{content}</Text>;
}
}

View File

@@ -4,13 +4,22 @@ import { useMapEvents } from 'react-leaflet';
import { getAirports } from '@lib/airport.ts';
import AirportMarker from '@components/AirportMarker.tsx';
import { LeafletEvent } from 'leaflet';
import { LayerInfo } from '@/App.tsx';
interface Bounds {
northEast: { lat: number; lon: number };
southWest: { lat: number; lon: number };
}
export default function AirportLayer({ setAirport }: { setAirport: (airport: Airport) => void }) {
export default function AirportLayer({
setAirport,
showNoMetar,
selectedLayer
}: {
setAirport: (airport: Airport) => void;
showNoMetar: boolean;
selectedLayer: LayerInfo;
}) {
const [airports, setAirports] = useState<Airport[]>([]);
function loadAirports(event: LeafletEvent) {
@@ -52,40 +61,44 @@ export default function AirportLayer({ setAirport }: { setAirport: (airport: Air
}
}, [map]);
const categoryOrder: { [key in AirportCategory]?: number } = {
[AirportCategory.LARGE]: 3,
[AirportCategory.MEDIUM]: 2,
[AirportCategory.SMALL]: 1,
[AirportCategory.HELIPORT]: 0
};
// const categoryOrder: { [key in AirportCategory]?: number } = {
// [AirportCategory.LARGE]: 3,
// [AirportCategory.MEDIUM]: 2,
// [AirportCategory.SMALL]: 1,
// [AirportCategory.HELIPORT]: 0
// };
const sortedAirports = airports.slice().sort((a, b) => {
// Compare by airport category first.
const categoryA = categoryOrder[a.category] ?? 4;
const categoryB = categoryOrder[b.category] ?? 4;
if (categoryA !== categoryB) {
return categoryA - categoryB;
}
// Then compare by flight category if available.
// Assuming that latest_metar.flight_category is a string and "UNKN" needs to come last.
const fcA = a.latest_metar?.flight_category ?? 'UNKN';
const fcB = b.latest_metar?.flight_category ?? 'UNKN';
if (fcA === 'UNKN' && fcB !== 'UNKN') return 1;
if (fcB === 'UNKN' && fcA !== 'UNKN') return -1;
// If both flight categories are not "UNKN", do a simple alphabetical comparison.
// (You may wish to customize this logic based on the actual flight category values.)
if (fcA < fcB) return -1;
if (fcA > fcB) return 1;
return 0;
});
// const sortedAirports = airports.slice().sort((a, b) => {
// // Compare by airport category first.
// const categoryA = categoryOrder[a.category] ?? 4;
// const categoryB = categoryOrder[b.category] ?? 4;
// if (categoryA !== categoryB) {
// return categoryA - categoryB;
// }
//
// // Then compare by flight category if available.
// // Assuming that latest_metar.flight_category is a string and "UNKN" needs to come last.
// const fcA = a.latest_metar?.flight_category ?? 'UNKN';
// const fcB = b.latest_metar?.flight_category ?? 'UNKN';
//
// if (fcA === 'UNKN' && fcB !== 'UNKN') return 1;
// if (fcB === 'UNKN' && fcA !== 'UNKN') return -1;
//
// // If both flight categories are not "UNKN", do a simple alphabetical comparison.
// // (You may wish to customize this logic based on the actual flight category values.)
// if (fcA < fcB) return -1;
// if (fcA > fcB) return 1;
// return 0;
// });
return (
<>
{sortedAirports.map((airport, index) => (
<AirportMarker key={index} airport={airport} index={index} setAirport={setAirport} />
{airports.map((airport, index) => (
<div key={index}>
{(showNoMetar || airport.latest_metar != undefined) && (
<AirportMarker airport={airport} index={index} setAirport={setAirport} selectedLayer={selectedLayer} />
)}
</div>
))}
</>
);

View File

@@ -3,17 +3,20 @@ import { Marker, Popup } from 'react-leaflet';
import L from 'leaflet';
import { useRef } from 'react';
import { getMarkerColor } from '@lib/metar.types.ts';
import { LayerInfo } from '@/App.tsx';
export default function AirportMarker({
index,
airport,
setAirport
setAirport,
selectedLayer,
}: {
index: number;
airport: Airport;
setAirport: (airport: Airport) => void;
selectedLayer: LayerInfo;
}) {
const icon = createCustomIcon(airport);
const icon = createCustomIcon(airport, selectedLayer);
const markerRef = useRef<L.Marker>(null);
return (
@@ -35,7 +38,7 @@ export default function AirportMarker({
);
}
function createCustomIcon(airport: Airport): L.DivIcon {
function createCustomIcon(airport: Airport, selectedLayer: LayerInfo): L.DivIcon {
if (airport.category === AirportCategory.HELIPORT) {
return L.divIcon({
html: `
@@ -44,7 +47,7 @@ function createCustomIcon(airport: Airport): L.DivIcon {
height: 14px;
border-radius: 50%;
border: 2px solid black;
background-color: white;
background-color: ${selectedLayer.markerOutline};
display: flex;
align-items: center;
justify-content: center;
@@ -83,7 +86,7 @@ function createCustomIcon(airport: Airport): L.DivIcon {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid #fff;
border: 2px solid ${selectedLayer.markerOutline};
z-index: {info[1]}">
</div>
`,