Added openlayers leaflet map

This commit is contained in:
2023-08-30 15:51:56 -04:00
parent 02c892a889
commit ac0ecdff5e
10 changed files with 1150 additions and 529 deletions

1450
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,16 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"next": "13.4.12", "leaflet": "^1.9.4",
"react": "18.2.0", "next": "^13.4.19",
"react-dom": "18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0",
"react-leaflet": "^4.2.1",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.3",
"@types/node": "20.4.5", "@types/node": "20.4.5",
"@types/react": "18.2.16", "@types/react": "18.2.16",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",

View File

@@ -11,7 +11,8 @@ import 'styles/globals.css';
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body className='bg-gray-200'> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.1/dist/leaflet.css" />
<body className='bg-gray-600'>
<RecoilRootWrapper>{children}</RecoilRootWrapper> <RecoilRootWrapper>{children}</RecoilRootWrapper>
</body> </body>
</html> </html>

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { setAirport } from "@/js/state"; import { getAirports, setAirport } from "@/js/state";
import { Airport } from "@/js/airport"; import { Airport } from "@/js/airport";
import MetarGrid from '@/components/MetarGrid'; import Metar from '@/components/Metar';
setAirport('KJYO', new Airport('Leesburg Executive Airport', 'KJYO')) setAirport('KJYO', new Airport('Leesburg Executive Airport', 'KJYO'))
setAirport('KHEF', new Airport('Manassas Regional Airpoirt', 'KHEF')) setAirport('KHEF', new Airport('Manassas Regional Airpoirt', 'KHEF'))
@@ -17,17 +17,20 @@ setAirport('KCJR', new Airport('Culpeper Regional Airport', 'KCJR'))
setAirport('KHWY', new Airport('Warrenton-Fauquier Airport', 'KHWY')) setAirport('KHWY', new Airport('Warrenton-Fauquier Airport', 'KHWY'))
setAirport('KRMN', new Airport('Stafford Regional Airport', 'KRMN')) setAirport('KRMN', new Airport('Stafford Regional Airport', 'KRMN'))
setAirport('KEZF', new Airport('Shannon Airport', 'KEZF')) setAirport('KEZF', new Airport('Shannon Airport', 'KEZF'))
setAirport('KDCA', new Airport('Ronald Reagan Washington National Airport', 'KDCA'))
// setAirport('KMQI', new Airport('Test Airport', 'KMQI')) // setAirport('KMQI', new Airport('Test Airport', 'KMQI'))
// setAirport('KEKQ', new Airport('Test Airport', 'KEKQ')) // setAirport('KEKQ', new Airport('Test Airport', 'KEKQ'))
// setAirport('KCSV', new Airport('Test Airport', 'KCSV')) // setAirport('KCSV', new Airport('Test Airport', 'KCSV'))
export default function Page() { export default function Page() {
return <> return <>
<div className="border-b border-gray-200 bg-gray-400 px-4 py-5 sm:px-6"> <div className="bg-gray-700 px-4 py-1 sm:px-6">
<h3 className="text-lg font-bold leading-6 text-gray-900">Airports</h3> <h3 className="text-lg font-bold leading-6 text-gray-200">Metar Map</h3>
</div> </div>
<div className='p-4'> <div>
<MetarGrid/> <Metar/>
</div> </div>
</> </>
} }

View File

@@ -4,30 +4,44 @@ import { Metar, getMetars } from "@/js/weather"
import Link from "next/link" import Link from "next/link"
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowsSpin, faLocationArrow } from '@fortawesome/free-solid-svg-icons' import { faArrowsSpin, faLocationArrow } from '@fortawesome/free-solid-svg-icons'
import dynamic from "next/dynamic";
export default async function MetarGrid() { export default async function Metar() {
const airports: Airport[] = getAirports(); const Map = dynamic(() => import("@/components/MetarMap"), {
loading: () => <div className="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div className="text-center"><p className="mt-4 text-3xl font-bold tracking-tight text-gray-300 sm:text-5xl">Loading...</p></div>
</div>,
ssr: false
});
async function update() { async function update() {
const airports: Airport[] = getAirports(); const airports: Airport[] = getAirports();
const metars = await getMetars(airports); const metars = await getMetars(airports);
for (let i = 0; i < airports.length; i++) { for (let i = 0; i < airports.length; i++) {
airports[i].metar = metars[i]; airports[i].metar = metars[i];
airports[i].latitude = metars[i].latitude;
airports[i].longitude = metars[i].longitude;
setAirport(airports[i].icao, airports[i]); setAirport(airports[i].icao, airports[i]);
} }
// setTimeout(update, 30 * 60 * 1000); return getAirports();
// setTimeout(update, 5000);
} }
await update(); await update();
return <>
<Map airportString={JSON.stringify(getAirports())}/>
</>
}
return ( export async function MetarGrid() {
const airports: Airport[] = getAirports();
return <>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{airports.map((airport) => ( {airports.map((airport) => (
<MetarCard airport={airport}/> <MetarCard airport={airport}/>
))} ))}
</div> </div>
); </>
} }
function MetarCard({ airport}: { airport: Airport}) { function MetarCard({ airport}: { airport: Airport}) {
@@ -46,10 +60,12 @@ function MetarCard({ airport}: { airport: Airport}) {
} }
function windColor(metar: Metar | undefined) { function windColor(metar: Metar | undefined) {
if (Number(metar?.wind_speed_kt) <= 12) { if (Number(metar?.wind_speed_kt) <= 9) {
return 'bg-green-300'; return 'bg-green-300';
} else if (Number(metar?.wind_speed_kt) > 12) { } else if (Number(metar?.wind_speed_kt) > 9) {
return 'bg-orange-300'; return 'bg-orange-300';
} else if (Number(metar?.wind_speed_kt) > 12) {
return 'bg-red-300';
} }
} }

145
src/components/MetarMap.tsx Normal file
View File

@@ -0,0 +1,145 @@
'use client';
import { Airport } from '@/js/airport';
import { Metar } from '@/js/weather';
import { faArrowsSpin, faLocationArrow, faLocationPin } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { DivIcon } from 'leaflet';
import { useEffect, useState } from 'react';
import ReactDOMServer from 'react-dom/server';
import { MapContainer, Marker, Popup, TileLayer, Tooltip, useMapEvents } from 'react-leaflet';
export default function Map({ airportString }: { airportString: string }) {
const [airports, setAirports] = useState(JSON.parse(airportString));
useEffect(() => {
}, []);
return (
<MapContainer
center={[38.7209, -77.5133]}
zoom={8}
maxZoom={12}
minZoom={3}
zoomControl={false}
style={{ height: '96.5vh' }}
className='overflow-y-hidden overflow-x-hidden'
attributionControl={false}
>
<MapTiles airports={airports}/>
</MapContainer>
);
}
function MapTiles({ airports }: {airports: Airport[] }) {
const [zoomLevel, setZoomLevel] = useState(8);
// const [dragging, setDragging] = useState(false);
// const [center, setCenter] = useState([50, 10.5]);
const mapEvents = useMapEvents({
zoomend: () => {
setZoomLevel(mapEvents.getZoom());
},
// mouseup: () => {
// setCenter([mapEvents.getCenter().lat, mapEvents.getCenter().lng]);
// }
});
function metarBGColor(metar: Metar | undefined) {
if (metar?.flight_category == 'VFR') {
return 'bg-emerald-600'
} else if (metar?.flight_category == 'MVFR') {
return 'bg-blue-600'
} else if (metar?.flight_category == 'IFR') {
return 'bg-orange-600'
} else if (metar?.flight_category == 'LIFR') {
return 'bg-red-600'
} else {
return 'bg-black'
}
}
function metarTextColor(metar: Metar | undefined) {
if (metar?.flight_category == 'VFR') {
return 'text-emerald-700'
} else if (metar?.flight_category == 'MVFR') {
return 'text-blue-700'
} else if (metar?.flight_category == 'IFR') {
return 'text-orange-700'
} else if (metar?.flight_category == 'LIFR') {
return 'text-red-700'
} else {
return 'text-black'
}
}
function windColor(metar: Metar | undefined) {
if (Number(metar?.wind_speed_kt) <= 9) {
return 'bg-green-300';
} else if (Number(metar?.wind_speed_kt) > 9) {
return 'bg-orange-300';
} else if (Number(metar?.wind_speed_kt) > 12) {
return 'bg-red-300';
}
}
function iconSize() {
if (zoomLevel <= 4) {
return 'text-xs'
} else if (zoomLevel <= 5) {
return 'text-sm';
} else if (zoomLevel <= 6) {
return 'text-base';
} else if (zoomLevel <= 7) {
return 'text-lg';
} else if (zoomLevel <= 9) {
return 'text-2xl';
} else if (zoomLevel <= 11) {
return 'text-3xl';
} else if (zoomLevel >= 12) {
return 'text-4xl';
}
}
function icon(airport: Airport) {
return new DivIcon({
html: ReactDOMServer.renderToString(<FontAwesomeIcon icon={faLocationPin} className={`${iconSize()} ${metarTextColor(airport.metar)}`}/>),
className: 'metar-marker-icon'
});
}
return <>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{airports.map((airport) => (
<>
<Marker position={[airport.latitude, airport.longitude]} icon={icon(airport)}>
<Tooltip className='metar-tooltip' direction="top" offset={[5, -5]} opacity={1}>{airport.icao}</Tooltip>
<Popup>
<div className="min-w-0 flex-1 select-none">
{/* <Link href={`/airport/${airport.icao}`}> */}
<h1 className="text-lg text-gray-900 pb-1"><span className='font-semibold'>{airport.icao}</span> {airport.name}</h1>
{/* </Link> */}
<hr/>
<p className='text-sm font-medium text-gray-500'>{airport.metar?.raw_text}</p>
<div className='mt-2 flex'>
<span className={`flex inline-block text-sm text-white ${metarBGColor(airport.metar)} py-2 px-4 rounded-full`}>{airport.metar?.flight_category? airport.metar?.flight_category : 'UNKN'}</span>
<div className="flex inline-block px-2">
<span className={`text-sm text-black ${windColor(airport.metar)} py-2 px-2 rounded-full`}>
{airport.metar && airport.metar.wind_dir_degrees && Number(airport.metar.wind_dir_degrees) > 0?
<FontAwesomeIcon className="pr-1" icon={faLocationArrow} style={{rotate: `${-45 + 180 + Number(airport.metar.wind_dir_degrees)}deg`}}/>: <></>
}
{airport.metar && airport.metar.wind_dir_degrees && airport.metar.wind_dir_degrees == 'VRB'?
<FontAwesomeIcon className="pr-1" icon={faArrowsSpin}/>: <></>
}
{airport.metar?.wind_speed_kt != undefined && airport.metar?.wind_speed_kt > 0? `${airport.metar?.wind_speed_kt} KT` : 'CALM'}
</span>
</div>
</div>
</div>
</Popup>
</Marker>
</>
))}
</>;
}

View File

@@ -3,11 +3,15 @@ import { Metar } from "./weather";
export class Airport { export class Airport {
name: string; name: string;
icao: string; icao: string;
latitude: number;
longitude: number;
metar: Metar | undefined; metar: Metar | undefined;
constructor(name: string, icao: string) { constructor(name: string, icao: string) {
this.name = name; this.name = name;
this.icao = icao; this.icao = icao;
this.latitude = 0;
this.longitude = 0;
this.metar = undefined; this.metar = undefined;
} }
} }

View File

@@ -20,7 +20,6 @@ export async function getMetars(airports: Airport[]): Promise<Metar[]> {
const json = xml2json(response.data, { compact: true }); const json = xml2json(response.data, { compact: true });
const jsonObject = JSON.parse(json); const jsonObject = JSON.parse(json);
let metarData = jsonObject?.response?.data?.METAR; let metarData = jsonObject?.response?.data?.METAR;
console.log(metarData);
if (!Array.isArray(metarData)) { if (!Array.isArray(metarData)) {
metarData = [metarData]; metarData = [metarData];
} }

View File

@@ -18,3 +18,19 @@ a {
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
.metar-tooltip {
position: absolute;
padding: 6px;
background-color: #000;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}