This commit is contained in:
Kilian committed 2025-07-15 12:19:41 +02:00
1 parent 13bea68195
commit e4ed55cac9
15 files changed
+1089 -820

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
public
pnpm-lock.yaml
node_modules
+9
View File
@@ -0,0 +1,9 @@
{
"printWidth": 120,
"tabWidth": 2,
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"arrowParens": "always",
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"]
}
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
printWidth: 120,
tabWidth: 2,
semi: true,
trailingComma: "es5",
singleQuote: true,
arrowParens: "always",
plugins: ["prettier-plugin-organize-imports"],
};
+3 -1
View File
@@ -2,8 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="/src/style.css" rel="stylesheet">
<title>MD-11 NavData Browser</title>
</head>
<body>
+7 -4
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"lint": "eslint . --ext .ts,.tsx --fix",
"preview": "vite preview",
"parser": "node --experimental-strip-types src/parser/node.ts"
},
@@ -18,12 +18,14 @@
"magvar": "^2.0.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-leaflet": "^5.0.0"
"react-leaflet": "^5.0.0",
"tailwindcss": "^4.1.11"
},
"devDependencies": {
"@eslint/js": "^9.31.0",
"@tailwindcss/vite": "^4.1.11",
"@types/leaflet": "^1.9.20",
"@types/node": "^24.0.13",
"@types/node": "^24.0.14",
"@types/object-hash": "^3.0.6",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
@@ -35,8 +37,9 @@
"object-hash": "^3.0.0",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"typescript": "~5.8.3",
"typescript-eslint": "^8.36.0",
"typescript-eslint": "^8.37.0",
"vite": "^7.0.4"
}
}
+958 -685
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
[{"ID":1,"Name":"KATMAI LODGE","ICAO":"00AN","PrimaryID":null,"Latitude":59.09347222,"Longitude":-156.45583333,"Elevation":80,"TransAlt":18000},{"ID":2,"Name":"ARLAND","ICAO":"00AR","PrimaryID":null,"Latitude":38.96965,"Longitude":-97.60155556,"Elevation":1352,"TransAlt":null},{"ID":3,"Name":"WILLIAMS AG","ICAO":"00CL","PrimaryID":null,"Latitude":39.42718889,"Longitude":-121.76342778,"Elevation":87,"TransAlt":null},{"ID":4,"Name":"GOLTL","ICAO":"00IG","PrimaryID":null,"Latitude":39.72402778,"Longitude":-101.39599444,"Elevation":3359,"TransAlt":null},{"ID":5,"Name":"THOMPSON FARMS","ICAO":"00TN","PrimaryID":null,"Latitude":36.12045,"Longitude":-86.35953333,"Elevation":675,"TransAlt":null},{"ID":6,"Name":"NOWHERE","ICAO":"00TT","PrimaryID":null,"Latitude":34.51660556,"Longitude":-99.93694167,"Elevation":1752,"TransAlt":null},{"ID":7,"Name":"DELVA","ICAO":"00TX","PrimaryID":null,"Latitude":32.54445278,"Longitude":-95.67313056,"Elevation":545,"TransAlt":18000},{"ID":8,"Name":"WEESKI RANCH","ICAO":"00XA","PrimaryID":null,"Latitude":30.22403889,"Longitude":-96.01415278,"Elevation":271,"TransAlt":null},{"ID":9,"Name":"MC HONE HEIGHTS","ICAO":"01AN","PrimaryID":null,"Latitude":61.64902778,"Longitude":-149.33902778,"Elevation":610,"TransAlt":null},{"ID":10,"Name":"SWANSBORO COUNTRY","ICAO":"01CL","PrimaryID":null,"Latitude":38.79990278,"Longitude":-120.734375,"Elevation":2594,"TransAlt":18000},{"ID":11,"Name":"COLEMAN HOMESTEAD","ICAO":"01CN","PrimaryID":null,"Latitude":62.10698333,"Longitude":-150.10929722,"Elevation":282,"TransAlt":null},{"ID":12,"Name":"THE FARM","ICAO":"01GE","PrimaryID":null,"Latitude":32.67515,"Longitude":-82.77099444,"Elevation":369,"TransAlt":null},{"ID":13,"Name":"LAVA HOT SPRINGS","ICAO":"01ID","PrimaryID":null,"Latitude":42.60825,"Longitude":-112.03246111,"Elevation":5268,"TransAlt":18000},{"ID":14,"Name":"GRANVILLE","ICAO":"01NK","PrimaryID":null,"Latitude":43.42294444,"Longitude":-73.268025,"Elevation":421,"TransAlt":18000},{"ID":15,"Name":"SPRING CREEK RANCH EAST","ICAO":"01OL","PrimaryID":null,"Latitude":34.37256944,"Longitude":-96.71603611,"Elevation":1060,"TransAlt":null},{"ID":16,"Name":"BEULAH FLD","ICAO":"01TX","PrimaryID":null,"Latitude":33.17058056,"Longitude":-98.62222222,"Elevation":1120,"TransAlt":null},{"ID":17,"Name":"BASS","ICAO":"02AL","PrimaryID":null,"Latitude":30.37146389,"Longitude":-87.76444444,"Elevation":61,"TransAlt":18000},{"ID":18,"Name":"EMERALD RIDGE","ICAO":"02AN","PrimaryID":null,"Latitude":59.70422222,"Longitude":-151.29586944,"Elevation":221,"TransAlt":null},{"ID":19,"Name":"OSBORN","ICAO":"02FA","PrimaryID":null,"Latitude":28.52666667,"Longitude":-81.8748,"Elevation":121,"TransAlt":18000},{"ID":20,"Name":"TRIPLE R RANCH","ICAO":"02FD","PrimaryID":null,"Latitude":30.95234722,"Longitude":-86.63541667,"Elevation":209,"TransAlt":null},{"ID":21,"Name":"FAIRPLAINS AIRPARK","ICAO":"02MI","PrimaryID":null,"Latitude":43.15700278,"Longitude":-85.14892778,"Elevation":850,"TransAlt":null},{"ID":22,"Name":"GREGORY","ICAO":"02MN","PrimaryID":null,"Latitude":45.62973611,"Longitude":-93.17497222,"Elevation":950,"TransAlt":null},{"ID":23,"Name":"TROY","ICAO":"02MO","PrimaryID":null,"Latitude":39.04626111,"Longitude":-91.02821944,"Elevation":630,"TransAlt":18000},{"ID":24,"Name":"FFR ANIMAS LANDING STRIP","ICAO":"02NM","PrimaryID":null,"Latitude":32.09071111,"Longitude":-108.87073611,"Elevation":4240,"TransAlt":null},{"ID":25,"Name":"CAWLEYS SOUTH PRAIRIE","ICAO":"02WA","PrimaryID":null,"Latitude":47.152325,"Longitude":-122.09400278,"Elevation":690,"TransAlt":18000},{"ID":26,"Name":"BLUE WATER","ICAO":"03CO","PrimaryID":null,"Latitude":38.09791944,"Longitude":-106.08975556,"Elevation":7710,"TransAlt":null},{"ID":27,"Name":"GREENS FARM FLD","ICAO":"03FL","PrimaryID":null,"Latitude":26.70547778,"Longitude":-82.05236944,"Elevation":15,"TransAlt":null},{"ID":28,"Name":"FLATLANDER","ICAO":"03IA","PrimaryID":null,"Latitude":41.14141111,"Longitude":-95.19438056,"Elevation":1081,"TransAlt":null},{"ID":29,"Name":"DORTON","ICAO":"03MO","PrimaryID":null,"Latitude":39.28883056,"Longitude":-93.99242222,"Elevation":930,"TransAlt":null},{"ID":30,"Name":"BLUEBIRD","ICAO":"03OL","PrimaryID":null,"Latitude":35.01233333,"Longitude":-97.70273611,"Elevation":1232,"TransAlt":null},{"ID":31,"Name":"BRAZOS POLO","ICAO":"03TT","PrimaryID":null,"Latitude":29.63211667,"Longitude":-95.93248056,"Elevation":117,"TransAlt":null},{"ID":32,"Name":"AEROSTONE AIRFIELD","ICAO":"03WN","PrimaryID":null,"Latitude":45.87545556,"Longitude":-120.672325,"Elevation":2342,"TransAlt":null},{"ID":33,"Name":"NORTH 40","ICAO":"04AN","PrimaryID":null,"Latitude":61.73751111,"Longitude":-148.700725,"Elevation":943,"TransAlt":null},{"ID":34,"Name":"WHITE HILLS","ICAO":"04AZ","PrimaryID":null,"Latitude":35.72177778,"Longitude":-114.45053889,"Elevation":2470,"TransAlt":null},{"ID":35,"Name":"GRAY BUTTE","ICAO":"04CA","PrimaryID":null,"Latitude":34.56664444,"Longitude":-117.67077222,"Elevation":3039,"TransAlt":18000},{"ID":36Line truncated
+8 -109
View File
@@ -1,8 +1,5 @@
import { default as L, default as Leaflet } from 'leaflet';
import 'leaflet-svg-shape-markers';
import hash from 'object-hash';
import { createRef, useEffect, useState } from 'react';
import { GeoJSON, MapContainer, TileLayer } from 'react-leaflet';
import { useEffect, useState } from 'react';
import { Map } from './components/map/Map';
import Parser from './parser/parser';
const parser = await Parser.instance();
@@ -13,122 +10,24 @@ function App() {
const [selectedTerminal, setSelectedTerminal] = useState(terminals[0]);
const [procedures, setProcedures] = useState<object[]>([]);
const mapRef = createRef<Leaflet.Map>();
const layerRef = createRef<Leaflet.GeoJSON>();
useEffect(() => {
(async () => {
setProcedures(await parser.parse(selectedTerminal));
})();
}, [selectedTerminal]);
useEffect(() => {
if (layerRef.current && mapRef.current) {
mapRef.current.flyToBounds(layerRef.current.getBounds(), {
animate: false,
padding: [50, 50],
});
}
});
return (
<div style={{ display: 'flex', height: '100vh', width: '100vw' }}>
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomSnap={0}
zoomDelta={0.1}
wheelPxPerZoomLevel={1000}
style={{ height: '100%', width: '100%' }}
ref={mapRef}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{procedures.map((procedure) => (
<>
<GeoJSON
key={hash(procedure ?? '') + 'lines'}
data={procedure}
style={({ properties }) => ({
color: '#ff00ff',
stroke: true,
weight: 5,
opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined,
})}
filter={(feature) => feature.geometry.type !== 'Point'}
ref={layerRef}
/>
<GeoJSON
key={hash(procedure ?? '') + 'points'}
data={procedure}
style={{
color: 'black',
fill: true,
fillColor: 'transparent',
stroke: true,
weight: 3,
}}
pointToLayer={({ properties }, latlng) => {
if (properties.isFlyOver)
return L.shapeMarker(latlng, {
shape: 'triangle',
radius: 6,
});
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
return L.shapeMarker(latlng, {
shape: 'star-4',
radius: 10,
rotation: 45,
});
}}
onEachFeature={({ geometry, properties }, layer) => {
if (geometry.type === 'Point') {
layer.bindPopup(
`${properties.name}<br>
${properties.altitude} ft<br>
${properties.speed} kts<br>
CNSTR:
${properties.altitudeConstraint ?? ''}
${properties.speedConstraint ?? ''}<br>`
);
}
}}
filter={(feature) => feature.geometry.type === 'Point'}
/>
</>
))}
</MapContainer>
<div
style={{
overflowY: 'scroll',
width: '200px',
}}
>
<div
style={{
padding: '5px',
display: 'flex',
flexDirection: 'column',
gap: '10px',
}}
>
<div className="flex h-dvh w-dvw">
<Map procedures={procedures} />
<div className="w-[200px] overflow-y-auto">
<div className="flex flex-col gap-1 p-1">
{terminals.map((terminal) => (
<div
key={terminal}
style={{
display: 'flex',
flexDirection: 'column',
background: '#eeeeee',
border: selectedTerminal === terminal ? '1px solid black' : '1px solid #eeeeee',
padding: '5px',
}}
className={`flex cursor-pointer flex-col border bg-gray-300 p-1 ${selectedTerminal === terminal ? 'border-black' : 'border-gray-300'}`}
onClick={() => setSelectedTerminal(terminal)}
>
<span style={{ whiteSpace: 'nowrap' }}>
<span className="whitespace-nowrap">
{(() => {
const t = parser.terminals.find(({ ID }) => ID === terminal);
return `${t?.ICAO} - ${t?.FullName}`;
+95
View File
@@ -0,0 +1,95 @@
import { default as L } from 'leaflet';
import 'leaflet-svg-shape-markers';
import hash from 'object-hash';
import { createRef, Fragment, useEffect, type FC } from 'react';
import { GeoJSON, MapContainer, TileLayer } from 'react-leaflet';
interface MapProps {
procedures: object[];
}
export const Map: FC<MapProps> = ({ procedures }) => {
const mapRef = createRef<L.Map>();
const layerRef = createRef<L.GeoJSON>();
useEffect(() => {
if (layerRef.current && mapRef.current) {
mapRef.current.flyToBounds(layerRef.current.getBounds(), {
animate: false,
padding: [50, 50],
});
}
});
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomSnap={0}
zoomDelta={0.1}
wheelPxPerZoomLevel={1000}
ref={mapRef}
className="h-full w-full"
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{procedures.map((procedure) => (
<Fragment key={hash(procedure ?? '')}>
<GeoJSON
key={hash(procedure ?? '') + 'lines'}
data={procedure}
style={({ properties }) => ({
color: '#ff00ff',
stroke: true,
weight: 5,
opacity: 1,
dashArray: properties.isManual ? '20, 20' : undefined,
})}
filter={(feature) => feature.geometry.type !== 'Point'}
ref={layerRef}
/>
<GeoJSON
key={hash(procedure ?? '') + 'points'}
data={procedure}
style={{
color: 'black',
fill: true,
fillColor: 'transparent',
stroke: true,
weight: 3,
}}
pointToLayer={({ properties }, latlng) => {
if (properties.isFlyOver)
return L.shapeMarker(latlng, {
shape: 'triangle',
radius: 6,
});
if (properties.isIntersection) return L.circleMarker(latlng, { radius: 6 });
return L.shapeMarker(latlng, {
shape: 'star-4',
radius: 10,
rotation: 45,
});
}}
onEachFeature={({ geometry, properties }, layer) => {
if (geometry.type === 'Point') {
layer.bindPopup(
`${properties.name}<br>
${properties.altitude} ft<br>
${properties.speed} kts<br>
CNSTR:
${properties.altitudeConstraint ?? ''}
${properties.speedConstraint ?? ''}<br>`
);
}
}}
filter={(feature) => feature.geometry.type === 'Point'}
/>
</Fragment>
))}
</MapContainer>
);
};
-8
View File
@@ -1,8 +0,0 @@
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
-1
View File
@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import 'leaflet/dist/leaflet.css';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
+1 -1
View File
@@ -20,7 +20,7 @@ export const TerminatorsFC = (
const line: LineSegment[] = [[previousFix.longitude, previousFix.latitude]];
if (previousFix.isFlyOver) {
let crsIntoEndpoint = trackIntoEndpoint;
const crsIntoEndpoint = trackIntoEndpoint;
// Check if there even is an arc
if (!crsIntoEndpoint.equal(lastCourse)) {
+1
View File
@@ -0,0 +1 @@
@import 'tailwindcss';
+1 -1
View File
@@ -1,4 +1,4 @@
//eslint-disable-next-line @typescript-eslint/no-unused-vars
import 'leaflet';
declare module 'leaflet' {
+2 -1
View File
@@ -1,7 +1,8 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [tailwindcss(), react()],
});