map style

This commit is contained in:
Kilian Hofmann 2025-07-14 05:02:22 +02:00
parent af7ac30926
commit 0d797519cb
21 changed files with 174 additions and 109 deletions

25
.gitignore vendored
View File

@ -1,3 +1,24 @@
node_modules/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
output.json
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
browser/.gitignore vendored
View File

@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,23 +1,26 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { globalIgnores } from "eslint/config";
export default tseslint.config([
globalIgnores(['dist']),
globalIgnores(["dist"]),
{
files: ['**/*.{ts,tsx}'],
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
rules: {
"@typescript-eslint/no-shadow": "error",
},
},
])
]);

View File

@ -14,25 +14,27 @@
"geojson": "^0.5.0",
"geolib": "^3.3.4",
"leaflet": "^1.9.4",
"leaflet-svg-shape-markers": "^1.4.0",
"magvar": "^2.0.0",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-leaflet": "5.0.0-rc.2"
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-leaflet": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@eslint/js": "^9.31.0",
"@types/leaflet": "^1.9.20",
"@types/node": "^24.0.13",
"@types/object-hash": "^3.0.6",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react-swc": "^3.10.2",
"eslint": "^9.30.1",
"eslint": "^9.31.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"object-hash": "^3.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"typescript-eslint": "^8.36.0",
"vite": "^7.0.4"
}
}

94
browser/pnpm-lock.yaml generated
View File

@ -17,25 +17,31 @@ importers:
leaflet:
specifier: ^1.9.4
version: 1.9.4
leaflet-svg-shape-markers:
specifier: ^1.4.0
version: 1.4.0
magvar:
specifier: ^2.0.0
version: 2.0.0
react:
specifier: 19.0.0-rc.1
version: 19.0.0-rc.1
specifier: ^19.1.0
version: 19.1.0
react-dom:
specifier: 19.0.0-rc.1
version: 19.0.0-rc.1(react@19.0.0-rc.1)
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
react-leaflet:
specifier: 5.0.0-rc.2
version: 5.0.0-rc.2(leaflet@1.9.4)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)
specifier: ^5.0.0
version: 5.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
devDependencies:
'@eslint/js':
specifier: ^9.30.1
specifier: ^9.31.0
version: 9.31.0
'@types/leaflet':
specifier: ^1.9.20
version: 1.9.20
'@types/node':
specifier: ^24.0.13
version: 24.0.13
'@types/object-hash':
specifier: ^3.0.6
version: 3.0.6
@ -47,9 +53,9 @@ importers:
version: 19.1.6(@types/react@19.1.8)
'@vitejs/plugin-react-swc':
specifier: ^3.10.2
version: 3.10.2(vite@7.0.4)
version: 3.10.2(vite@7.0.4(@types/node@24.0.13))
eslint:
specifier: ^9.30.1
specifier: ^9.31.0
version: 9.31.0
eslint-plugin-react-hooks:
specifier: ^5.2.0
@ -67,11 +73,11 @@ importers:
specifier: ~5.8.3
version: 5.8.3
typescript-eslint:
specifier: ^8.35.1
specifier: ^8.36.0
version: 8.36.0(eslint@9.31.0)(typescript@5.8.3)
vite:
specifier: ^7.0.4
version: 7.0.4
version: 7.0.4(@types/node@24.0.13)
packages:
@ -498,6 +504,9 @@ packages:
'@types/leaflet@1.9.20':
resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==}
'@types/node@24.0.13':
resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==}
'@types/object-hash@3.0.6':
resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==}
@ -830,6 +839,9 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
leaflet-svg-shape-markers@1.4.0:
resolution: {integrity: sha512-vUBwso51+4ZVGcLZbhdBGxz+xrbul5jDYxool2yTKbIjAC6rvOMLjr8YBTQbLaa1LBRBQIaWUbmCafdXm17pxw==}
leaflet@1.9.4:
resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==}
@ -927,20 +939,20 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-dom@19.0.0-rc.1:
resolution: {integrity: sha512-k8MfDX+4G+eaa1cXXI9QF4d+pQtYol3nx8vauqRWUEOPqC7NQn2qmEqUsLoSd28rrZUL+R3T2VC+kZ2Hyx1geQ==}
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies:
react: 19.0.0-rc.1
react: ^19.1.0
react-leaflet@5.0.0-rc.2:
resolution: {integrity: sha512-1xQGYG9mEIW+nfkQhqgHImwUuB1UDlnzYFSzv6PrBFDBeYrFmv0BbpwpNAFdJg/UQ2yz5UZSL7ZwlUxjwb8MZw==}
react-leaflet@5.0.0:
resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==}
peerDependencies:
leaflet: ^1.9.0
react: ^19.0.0
react-dom: ^19.0.0
react@19.0.0-rc.1:
resolution: {integrity: sha512-NZKln+uyPuyHchzP07I6GGYFxdAoaKhehgpCa3ltJGzwE31OYumLeshGaitA1R/fS5d9D2qpZVwTFAr6zCLM9w==}
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
resolve-from@4.0.0:
@ -959,8 +971,8 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
scheduler@0.25.0-rc.1:
resolution: {integrity: sha512-fVinv2lXqYpKConAMdergOl5owd0rY1O4P/QTe0aWKCqGtu7VsCt1iqQFxSJtqK4Lci/upVSBpGwVC7eWcuS9Q==}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
@ -1017,6 +1029,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -1222,11 +1237,11 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)':
'@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
leaflet: 1.9.4
react: 19.0.0-rc.1
react-dom: 19.0.0-rc.1(react@19.0.0-rc.1)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@rolldown/pluginutils@1.0.0-beta.11': {}
@ -1352,6 +1367,10 @@ snapshots:
dependencies:
'@types/geojson': 7946.0.16
'@types/node@24.0.13':
dependencies:
undici-types: 7.8.0
'@types/object-hash@3.0.6': {}
'@types/react-dom@19.1.6(@types/react@19.1.8)':
@ -1454,11 +1473,11 @@ snapshots:
'@typescript-eslint/types': 8.36.0
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-react-swc@3.10.2(vite@7.0.4)':
'@vitejs/plugin-react-swc@3.10.2(vite@7.0.4(@types/node@24.0.13))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.11
'@swc/core': 1.12.11
vite: 7.0.4
vite: 7.0.4(@types/node@24.0.13)
transitivePeerDependencies:
- '@swc/helpers'
@ -1731,6 +1750,8 @@ snapshots:
dependencies:
json-buffer: 3.0.1
leaflet-svg-shape-markers@1.4.0: {}
leaflet@1.9.4: {}
levn@0.4.1:
@ -1812,19 +1833,19 @@ snapshots:
queue-microtask@1.2.3: {}
react-dom@19.0.0-rc.1(react@19.0.0-rc.1):
react-dom@19.1.0(react@19.1.0):
dependencies:
react: 19.0.0-rc.1
scheduler: 0.25.0-rc.1
react: 19.1.0
scheduler: 0.26.0
react-leaflet@5.0.0-rc.2(leaflet@1.9.4)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1):
react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)
'@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
leaflet: 1.9.4
react: 19.0.0-rc.1
react-dom: 19.0.0-rc.1(react@19.0.0-rc.1)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react@19.0.0-rc.1: {}
react@19.1.0: {}
resolve-from@4.0.0: {}
@ -1860,7 +1881,7 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
scheduler@0.25.0-rc.1: {}
scheduler@0.26.0: {}
semver@7.7.2: {}
@ -1907,11 +1928,13 @@ snapshots:
typescript@5.8.3: {}
undici-types@7.8.0: {}
uri-js@4.4.1:
dependencies:
punycode: 2.3.1
vite@7.0.4:
vite@7.0.4(@types/node@24.0.13):
dependencies:
esbuild: 0.25.6
fdir: 6.4.6(picomatch@4.0.2)
@ -1920,6 +1943,7 @@ snapshots:
rollup: 4.45.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 24.0.13
fsevents: 2.3.3
which@2.0.2:

View File

@ -3,6 +3,7 @@ import Parser from "./parser/parser";
import { createRef, useEffect, useState } from "react";
import hash from "object-hash";
import Leaflet from "leaflet";
import "leaflet-svg-shape-markers";
import L from "leaflet";
const parser = await Parser.instance();
@ -11,7 +12,7 @@ const terminals = [10394, 10395, 10475, 10480, 10482, 10485, 10653];
function App() {
const [selectedTerminal, setSelectedTerminal] = useState(terminals[0]);
const [procedure, setProcedure] = useState<string>();
const [procedure, setProcedure] = useState<object>();
const mapRef = createRef<Leaflet.Map>();
const layerRef = createRef<Leaflet.GeoJSON>();
@ -49,26 +50,40 @@ function App() {
<GeoJSON
key={hash(procedure ?? "") + "lines"}
data={procedure}
style={{
color: "#00ffff",
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={(feature) => ({
color: feature.properties["marker-color"],
stroke: false,
style={{
color: "black",
fill: true,
fillOpacity: 1,
})}
pointToLayer={(_, latlng) => {
return L.circleMarker(latlng, { radius: 5 });
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") {

View File

@ -2,12 +2,13 @@ import Parser from "./parser.ts";
// mutate fetch to be local
// @ts-ignore
// @ts-expect-error Global override
// eslint-disable-next-line no-global-assign
fetch = async (path: string) => {
// @ts-ignore
const fs = await import("fs");
return {
json: () => JSON.parse(fs.readFileSync(`public/${path}`)),
json: () =>
JSON.parse(fs.readFileSync(`public/${path}`) as unknown as string),
};
};

View File

@ -35,9 +35,9 @@ class Parser {
public static instance = async () => {
if (!Parser._instance) {
const waypoints = await (await fetch("navdata/Waypoints.json")).json();
const runways = await (await fetch("navdata/Runways.json")).json();
const terminals = await (await fetch("navdata/Terminals.json")).json();
const waypoints = await (await fetch("NavData/Waypoints.json")).json();
const runways = await (await fetch("NavData/Runways.json")).json();
const terminals = await (await fetch("NavData/Terminals.json")).json();
Parser._instance = new Parser(waypoints, runways, terminals);
}
@ -71,12 +71,12 @@ class Parser {
if (!runway) throw new Error("Procedure links to non existent Runway");
// Load procedure
const procedure = (await (
await fetch(`navdata/TermID_${terminalID}.json`)
await fetch(`NavData/TermID_${terminalID}.json`)
).json()) as TerminalEntry[];
// Output variables
const navFixes: NavFix[] = [];
const lineSegments: { line: LineSegment[] }[] = [];
const lineSegments: { line: LineSegment[]; [x: string]: unknown }[] = [];
// Initials
navFixes.push({
@ -167,7 +167,7 @@ class Parser {
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
lineSegments.push({ line: lineToAdd, isManual: true });
updateLastCourse(lineToAdd);
}
break;
@ -256,7 +256,7 @@ class Parser {
);
if (fixToAdd) navFixes.push(fixToAdd);
if (lineToAdd) {
lineSegments.push({ line: lineToAdd });
lineSegments.push({ line: lineToAdd, isManual: true });
updateLastCourse(lineToAdd);
}
break;

View File

@ -11,7 +11,6 @@ export const TerminatorsAF = (
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -11,7 +11,6 @@ export const TerminatorsCF = (
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -19,7 +19,6 @@ export const TerminatorsCI = (
nextFix,
crs
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -16,7 +16,6 @@ export const TerminatorsCR = (
{ latitude: leg.NavLat, longitude: leg.NavLon },
leg.NavBear.toTrue({ latitude: leg.NavLat, longitude: leg.NavLon })
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -13,7 +13,6 @@ export const TerminatorsRF = (
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -12,7 +12,6 @@ export const TerminatorsTF = (
latitude: leg.WptLat,
longitude: leg.WptLon,
name: waypoint?.Ident ?? undefined,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -19,7 +19,6 @@ export const TerminatorsVA = (
leg.Course.toTrue(previousFix)
),
name: leg.Alt,
"marker-color": "#ff0000",
isFlyOver: true,
altitude: leg.Alt.parseAltitude(),
speed: leg.SpeedLimit

View File

@ -15,7 +15,6 @@ export const TerminatorsVD = (
leg.Course.toTrue(previousFix)
),
name: leg.Distance.toString(),
"marker-color": "#ff0000",
isFlyOver: true,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -20,7 +20,6 @@ export const TerminatorsVI = (
nextFix,
crs
)!,
"marker-color": leg.IsFlyOver ? "#ff0000" : undefined,
isFlyOver: leg.IsFlyOver,
altitude: leg.Alt ? leg.Alt.parseAltitude() : previousFix.altitude,
speed: leg.SpeedLimit

View File

@ -85,5 +85,11 @@ export const computeIntersection = (
const lat = φ3.toDegrees();
const lon = λ3.toDegrees();
return { ...p1, latitude: lat, longitude: lon, name: "INTC" };
return {
...p1,
latitude: lat,
longitude: lon,
name: "INTC",
isIntersection: true,
};
};

View File

@ -1,3 +1,3 @@
declare module "geojson" {
export const parse: (data: any, format: any) => any;
export const parse: (data: object, format: object) => object;
}

24
browser/src/types/leaflet.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
//eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as L from "leaflet";
declare module "leaflet" {
export function shapeMarker(
latlng: LatLngExpression,
options?: PathOptions & {
shape?:
| "diamond"
| "square"
| "triangle"
| "triangle-up"
| "triangle-down"
| "arrowhead"
| "arrowhead-up"
| "arrowhead-down"
| "circle"
| "x"
| string;
radius?: number;
rotation?: number;
}
): Path;
}

View File

@ -85,6 +85,8 @@ export declare global {
"marker-color"?: string;
altitudeConstraint?: string;
speedConstraint?: number;
// For map
isIntersection?: boolean;
};
type LineSegment = [number, number];