This commit is contained in:
Kilian Hofmann 2025-07-15 12:19:41 +02:00
parent 13bea68195
commit e4ed55cac9
15 changed files with 1089 additions and 820 deletions

3
browser/.prettierignore Normal file
View File

@ -0,0 +1,3 @@
public
pnpm-lock.yaml
node_modules

9
browser/.prettierrc Normal file
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"]
}

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"],
};

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>

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"
}
}

1359
browser/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

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}`;

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>
);
};

View File

@ -1,8 +0,0 @@
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}

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>

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
browser/src/style.css Normal file
View File

@ -0,0 +1 @@
@import 'tailwindcss';

View File

@ -1,4 +1,4 @@
//eslint-disable-next-line @typescript-eslint/no-unused-vars
import 'leaflet';
declare module 'leaflet' {

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()],
});