diff --git a/.gitignore b/.gitignore index 5d7a737b..c7494aea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ tsconfig.tsbuildinfo .vs examples/aircraft/PackageSources/html_ui/Pages/VCockpit/Instruments/Navigraph/NavigationDataInterfaceSample examples/aircraft/PackageSources/SimObjects/Airplanes/Navigraph_Navigation_Data_Interface_Aircraft/panel/msfs_navigation_data_interface.wasm +examples/aircraft/PackageSources/bundled-navigation-data +!examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1 +!examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2 + out # Rust diff --git a/Cargo.toml b/Cargo.toml index b400d88f..b85aa25d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,4 @@ lto = true strip = true [patch.crates-io] -rusqlite = { git = "https://github.com/navigraph/rusqlite", rev = "7921774" } +rusqlite = { git = "https://github.com/navigraph/rusqlite", rev = "7921774" } \ No newline at end of file diff --git a/docs/RFC 001.md b/docs/RFC 001.md index 233c976f..ae2034eb 100644 --- a/docs/RFC 001.md +++ b/docs/RFC 001.md @@ -50,6 +50,7 @@ This interface is also not designed for use outside of Microsoft Flight Simulato - Do: `Vhf` - Dont: `NDB` - Latitudes should be encoded to as `lat` and Longitudes should be encoded to as `long`, and should wherever they are used in conjunction with each other, be part of a `Coordinates` data structure +- For enum types, Unknown shall be an enum variant if neccessary, and for all other types such as numbers or strings, an unknown value will be indicated by being undefined. This applies for fields which are only supported by certain database sources, if there is an output field that some database does not provide, that value shall be set to unknown, whatever that means for the said field. --- diff --git a/docs/RFC 002.md b/docs/RFC 002.md new file mode 100644 index 00000000..86c33fc2 --- /dev/null +++ b/docs/RFC 002.md @@ -0,0 +1,126 @@ +# Specification for loading and persistence of Navigation Data packages in Microsoft Flight Simulator + +To be Reviewed By: Katlyn Courtade, Jack Lavigne, Markus Hamburger + +Authors: Alex Cutforth + +Status: In Progress + +## Definition of Terms + +- `developers` refers to any third party aircraft developer wishing to use Navigraph's in sim navigation data package loader +- `sim`/`the sim` refers to Microsoft Flight Simulator specifically +- `wasm-interface` refers to the WASM bundle that is run in sim by aircraft wishing to download or load Navigraph's navigation data. Aircraft developers can interface with this bundle through CommBus calls + +## Problem + +Shipping navigation data to aircraft is traditionally done by an external program, while the simulator is not running. This is inconvinent especially when users forget to update their navigation data before starting the simulator. This RFC will outline a system for storing navigation data packages in sim persistently, and outline a system for automatically loading bundled navigation data. + +# Solution + +## Storage + +Navigation Data packages shall be stored within a folder in the simulator `work` folder called `navigation-data`, so `/work/navigation-data`. Each package should be a folder containing all the data and metadata for that package. These folders should be given uuids as names to prevent collisions. The contents of these folders should match the contents of the ZIP folder provided from the Navigraph packages API, that is, the .zip is essentially transformed into a file system folder with a uuid name. + +The UUID of each folder should be seeded based on the [uniqeness properties](#package-uniqueness) of the `cycle.json` so that folder names can be used to check if two packages are the same without reading both `cycle.json`s. This also ensures that two packages that are the 'same' are not installed at the same time. + +Every package which is downloaded must contain exactly one `cycle.json` file placed at the root. This file shall follow the following structure: + +```ts +{ + cycle: string, // E.g.: "2311" Represents the AIRAC cycle number of this package + revision: string, // E.g.: "2" + name: string, // E.g.: "avionics_v1" (this is an arbitrary name that generally represents what/who this package is meant for) + format: 'dfd' | 'dfdv2' | 'custom', // Represents the format of the data present. Note that further format types may be added if they are supported with custom wrappers in the `wasm-interface` + validityPeriod: string, // E.g.: "2024-10-03/2024-10-30" Represents the time period through which this package is valid (generally matches the AIRAC cycle period) + + // Required for dfd_v1 and dfd_v2 + databasePath?: string, // E.g.: "/e_dfd_2311.s3db" Provides the path to the dfd database file from the root of the folder + + // May contain any other neccessary metadata such as paths to certain files/folders +} +``` + +Any folder within the `navigation-data` folder which does not contain a `cycle.json` at the root, or contains more than one `cycle.json` will be regarded as an invalid package, and will not be recognised by the wasm-interface. + +### Example file structure: + +``` +work +| navigation-data +| | bac9657d-36b8-4ffb-8052-7d88b13f6ff8 +| | | cycle.json +| | | e_dfd_2311.s3db +| | | ... +| | +| | 27b1642c-7572-468a-b11a-be1b944c5e43 +| | | cycle.json +| | | Config +| | | | .DS_Store +| | | | +| | | | NavData +| | | | | airports.dat +| | | | | apNavAPT.txt +| | | | | ... +| | | | +| | | | SidStars +| | | | | NZCH.txt +| | | | | KJFK.txt +| | | | | ... + +``` + +## Package Uniqueness + +The `cycle.json` properties: `cycle`, `revision`, `name` and `format` shall be used to differentiate packages from one another. That is to say, the `navigation-data` folder shouldn't have multiple packages with the same set of said properties. + +## Bundled data + +**It is important to note that the package folder name is unrelated to the `name` field in `cycle.json`** + +Aircraft devlopers may bundle navigation data packages with their aircraft by placing them in `/PackageSources/bundled-navigation-data` in the same way packages are stored in `\work\navigation-data`. On initialisation of the wasm-interface, all packages in `bundled-navigation-data` that are not already in `/work/navigation-data` (see [Package Uniqueness](#package-uniqueness) for details on how to check if two packages are the same) shall be copied to `/work/navigation-data`. The packages in `bundled-navigation-data` may have any folder name, so when copying a package to `/work/navigation-data` the folder shall be renamed to the seeded uuid. If this was not the case, an aircraft update may bundle a newer cycle version package which would then have the same folder name as the previous package in `/work/navigation-data`, so to avoid having to check for clashes and delete the previous package, the package folder will be given seeded uuid name. This is to ensure developers to properly check that their desired package and format is present before tring to load it (This can be done using the function outlined in [Package Selection](#package-selection)). Packages which are copied over from `bundled-navigation-data` should not be deleted from `bundled-navigation-data`. + +## Download + +Navigation data can be downloaded using Navigraph's packages API. The wasm-interface shall provide a function `DownloadNavigationData` which will take in a download URL, and download it to the `\work\navigation-data`. The wasm-interface will unzip the contents of the download into a folder with a seeded uuid name in order to match the [required file structure](#example-file-structure). The wasm-interface should also provide a function `SetDownloadOptions` which allows the developer to specify the maximum file extraction/deletion rate to maintain sim performance. + +Note that the packages API may provide packages which are not valid navigation data packages, do not attempt to download these as they will not be recognised by the `wasm-interface` once installed. + +The DownloadNavigationData function shall provide an optional parameter to **explicitly** enable automatic selection of the package once it has been downloaded. + +## Package deletion + +The wasm-interface shall provide a function `DeletePackage` which shall delete a package from `/work/navigation-data` based on its uuid. + +## Package Cleanup + +The wasm-interface shall provide a function `CleanPackages` which shall delete all package from `/work/navigation-data` which do not have the same format as the currently active database. It shall accept an optional parameter `count` which will limit the number of matching format packages to retain. Any packages which are also present in the `bundled-navigation-data` folder will be retained regardless of `count` or format. + +## Package Selection + +The wasm-interface shall provide a function `ListAvailablePackages` which returns a list of valid packages present in the `/work/navigation-data` folder. + +### `ListAvailablePackages` Result type + +```ts +[ + { + uuid: string, // E.g.: "bac9657d-36b8-4ffb-8052-7d88b13f6ff8" Provides the seeded uuid of the package (same as the folder name) + path: string, // E.g. "/work/navigation-data/bac9657d-36b8-4ffb-8052-7d88b13f6ff8" Provides the absolute path in the wasm file system to the package folder. + is_bundled: boolean, + cycle: { + // Provides all the data from the cycle.json + ... + } + } + ... +] +``` + +### Active package + +A package is said to be "selected" or "active" by having its folder renamed to `active`. This enables persistent selection and assurance that only one package is active at any one time. Developers can then find the currently selected package in `/work/navigation-data/active/`. + +The wasm-interface shall provide a function `SetActivePackage`. This function shall take in the uuid of the package to be selected, and that package folder shall then be renamed to `active`. If the active package `cycle.json` format field indicates the package is of a format that can be read by the `wasm-interface` database functionality, it will be automatically selected for use. When a package is "de-selected", that is, a different package is selected, it's folder shall be renamed to its seeded uuid. + +The wasm-interface shall also provide a function `GetActivePackage`. This function shall return information about the package which is currently active, in the same format as `ListAvailablePackages`. If no package is active, the function shall return null. \ No newline at end of file diff --git a/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml b/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml index df4e18f5..619e8f49 100644 --- a/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml +++ b/examples/aircraft/PackageDefinitions/navigraph-aircraft-navigation-data-interface-sample.xml @@ -27,14 +27,6 @@ PackageSources\Data\ Data\ - - Copy - - false - - PackageSources\NavigationData\ - NavigationData\ - SimObject @@ -43,6 +35,14 @@ PackageSources\SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\ SimObjects\Airplanes\Navigraph_Navigation_Data_Interface_Aircraft\ + + Copy + + false + + PackageSources\bundled-navigation-data\ + bundled-navigation-data\ + Copy diff --git a/examples/aircraft/PackageSources/NavigationData/cycle.json b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/cycle.json similarity index 100% rename from examples/aircraft/PackageSources/NavigationData/cycle.json rename to examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/cycle.json diff --git a/examples/aircraft/PackageSources/NavigationData/e_dfd_2101.s3db b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/e_dfd_2101.s3db similarity index 100% rename from examples/aircraft/PackageSources/NavigationData/e_dfd_2101.s3db rename to examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/e_dfd_2101.s3db diff --git a/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/foo.txt b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v1/foo.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/cycle.json b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/cycle.json new file mode 100644 index 00000000..87442977 --- /dev/null +++ b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/cycle.json @@ -0,0 +1 @@ +{"cycle":"2401","revision":"1","name":"Navigraph Avionics", "format": "dfdv2", "validityPeriod": "2024-01-25/2024-02-21"} \ No newline at end of file diff --git a/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/foo.txt b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/foo.txt new file mode 100644 index 00000000..e69de29b diff --git a/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/ng_jeppesen_fwdfd_2401.s3db b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/ng_jeppesen_fwdfd_2401.s3db new file mode 100644 index 00000000..768eaaa9 Binary files /dev/null and b/examples/aircraft/PackageSources/bundled-navigation-data/sample-data-v2/ng_jeppesen_fwdfd_2401.s3db differ diff --git a/examples/gauge/Components/Input.tsx b/examples/gauge/Components/Input.tsx index f88a1601..b727222c 100644 --- a/examples/gauge/Components/Input.tsx +++ b/examples/gauge/Components/Input.tsx @@ -1,7 +1,18 @@ -import { ComponentProps, DisplayComponent, FSComponent, Subscribable, UUID, VNode } from "@microsoft/msfs-sdk" +import { + ComponentProps, + DisplayComponent, + FSComponent, + Subscribable, + SubscribableUtils, + UUID, + VNode, +} from "@microsoft/msfs-sdk" +import { InterfaceNavbarItemV2 } from "./Utils" interface InputProps extends ComponentProps { - value?: string + value: Subscribable + setValue: (value: string) => void + default?: Subscribable | string class?: string | Subscribable textarea?: boolean } @@ -10,13 +21,16 @@ export class Input extends DisplayComponent { private readonly inputId = UUID.GenerateUuid() private readonly inputRef = FSComponent.createRef() - get value() { - return this.inputRef.instance.value - } - onAfterRender(node: VNode): void { super.onAfterRender(node) + this.props.value.map(val => (this.inputRef.instance.value = val)) + SubscribableUtils.toSubscribable(this.props.default ?? "", true).map(val => { + this.inputRef.instance.placeholder = val + }) + + this.inputRef.instance.addEventListener("input", () => this.props.setValue(this.inputRef.instance.value ?? "")) + this.inputRef.instance.onfocus = this.onInputFocus this.inputRef.instance.onblur = this.onInputBlur } @@ -54,3 +68,34 @@ export class Input extends DisplayComponent { return } } + +interface CheckboxProps extends ComponentProps { + value: Subscribable + setValue: (value: string) => void + default?: Subscribable | string + class?: string +} + +export class Checkbox extends DisplayComponent { + private readonly isActive = this.props.value.map(val => (val == "true" ? true : false)) + + private onClick = () => { + this.props.setValue(this.isActive.get() ? "false" : "true") + } + + render(): VNode { + return ( + this.onClick()} + > + {this.isActive.map(val => (val ? "✔" : "X"))} + + ) + } +} diff --git a/examples/gauge/Components/InterfaceSample.css b/examples/gauge/Components/InterfaceSample.css index cdce9217..8fb425ec 100644 --- a/examples/gauge/Components/InterfaceSample.css +++ b/examples/gauge/Components/InterfaceSample.css @@ -45,6 +45,13 @@ padding: 2rem; } +.horizontal-no-pad { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-start; +} + .vertical { display: flex; flex-direction: column; @@ -52,6 +59,11 @@ align-items: center; } +.scrollable { + overflow: scroll; + height: 400px; +} + .text-field { width: fit-content; height: 50px; diff --git a/examples/gauge/Components/InterfaceSample.tsx b/examples/gauge/Components/InterfaceSample.tsx index 71c9cbaa..ce51ff55 100644 --- a/examples/gauge/Components/InterfaceSample.tsx +++ b/examples/gauge/Components/InterfaceSample.tsx @@ -1,47 +1,33 @@ import { + ArraySubject, ComponentProps, DisplayComponent, EventBus, FSComponent, - MappedSubject, Subject, VNode, } from "@microsoft/msfs-sdk" -import { - DownloadProgressPhase, - NavigraphEventType, - NavigraphNavigationDataInterface, -} from "@navigraph/msfs-navigation-data-interface" -import { NavigationDataStatus } from "@navigraph/msfs-navigation-data-interface/types/meta" -import { CancelToken } from "navigraph/auth" -import { packages } from "../Lib/navigraph" -import { AuthService } from "../Services/AuthService" -import { Dropdown } from "./Dropdown" -import { Input } from "./Input" +import { NavigraphNavigationDataInterface, PackageInfo } from "@navigraph/msfs-navigation-data-interface" import "./InterfaceSample.css" +import { AuthPage } from "./Pages/Auth/Auth" +import { Dashboard } from "./Pages/Dashboard/Dashboard" +import { TestPage } from "./Pages/Test/Test" +import { InterfaceNavbar, InterfaceSwitch } from "./Utils" interface InterfaceSampleProps extends ComponentProps { bus: EventBus } export class InterfaceSample extends DisplayComponent { - private readonly textRef = FSComponent.createRef() - private readonly navigationDataTextRef = FSComponent.createRef() - private readonly loginButtonRef = FSComponent.createRef() - private readonly qrCodeRef = FSComponent.createRef() - private readonly dropdownRef = FSComponent.createRef() - private readonly downloadButtonRef = FSComponent.createRef() - private readonly icaoInputRef = FSComponent.createRef() - private readonly executeIcaoButtonRef = FSComponent.createRef() - private readonly sqlInputRef = FSComponent.createRef() - private readonly executeSqlButtonRef = FSComponent.createRef() - private readonly outputRef = FSComponent.createRef() private readonly loadingRef = FSComponent.createRef() private readonly authContainerRef = FSComponent.createRef() - private readonly navigationDataStatus = Subject.create(null) - - private cancelSource = CancelToken.source() + private readonly activeDatabase = Subject.create(null) + private readonly databases = ArraySubject.create([]) + private readonly resetPackageList = Subject.create(false) + private readonly mainPageIndex = Subject.create(0) + private readonly selectedDatabaseIndex = Subject.create(0) + private readonly selectedDatabase = Subject.create(null) private navigationDataInterface: NavigraphNavigationDataInterface @@ -49,49 +35,6 @@ export class InterfaceSample extends DisplayComponent { super(props) this.navigationDataInterface = new NavigraphNavigationDataInterface() - - this.navigationDataInterface.onEvent(NavigraphEventType.DownloadProgress, data => { - switch (data.phase) { - case DownloadProgressPhase.Downloading: - this.displayMessage("Downloading navigation data...") - break - case DownloadProgressPhase.Cleaning: - if (!data.deleted) return - this.displayMessage(`Cleaning destination directory. ${data.deleted} files deleted so far`) - break - case DownloadProgressPhase.Extracting: { - // Ensure non-null - if (!data.unzipped || !data.total_to_unzip) return - const percent = Math.round((data.unzipped / data.total_to_unzip) * 100) - this.displayMessage(`Unzipping files... ${percent}% complete`) - break - } - } - }) - } - - public renderDatabaseStatus(): VNode | void { - return ( - <> -
{ - return status ? "vertical" : "hidden" - }, this.navigationDataStatus)} - > -
{this.navigationDataStatus.map(s => `Install method: ${s?.status}`)}
-
- {this.navigationDataStatus.map( - s => `Installed format: ${s?.installedFormat} revision ${s?.installedRevision}`, - )} -
-
{this.navigationDataStatus.map(s => `Installed path: ${s?.installedPath}`)}
-
{this.navigationDataStatus.map(s => `Installed cycle: ${s?.installedCycle}`)}
-
{this.navigationDataStatus.map(s => `Latest cycle: ${s?.latestCycle}`)}
-
{this.navigationDataStatus.map(s => `Validity period: ${s?.validityPeriod}`)}
-
-
(status ? "hidden" : "visible"))}>Loading status...
- - ) } public render(): VNode { @@ -102,45 +45,46 @@ export class InterfaceSample extends DisplayComponent { minutes
-
-
-

Step 1 - Sign in

-
Loading
-
-
- -
-
-

Step 2 - Select Database

- -
- Download -
- {this.renderDatabaseStatus()} -
-
- -

Step 3 - Query the database

-
-
- -
- Fetch Airport -
-
- +
+ this.mainPageIndex.set(pageNumber)} + active={this.mainPageIndex} /> -
- Execute SQL -
-
-              The output of the query will show up here
-            
+ this.selectedDatabase.set(database)} + setSelectedDatabaseIndex={index => this.selectedDatabaseIndex.set(index)} + interface={this.navigationDataInterface} + />, + ], + [1, ], + [ + 2, + this.activeDatabase.set(database)} + navigationDataInterface={this.navigationDataInterface} + />, + ], + ]} + />
@@ -151,129 +95,40 @@ export class InterfaceSample extends DisplayComponent { super.onAfterRender(node) // Populate status when ready - this.navigationDataInterface.onReady(() => { - this.navigationDataInterface - .get_navigation_data_install_status() - .then(status => this.navigationDataStatus.set(status)) - .catch(e => console.error(e)) + this.navigationDataInterface.onReady(async () => { + const pkgs = await this.navigationDataInterface.list_available_packages(true) - // show the auth container - this.authContainerRef.instance.style.display = "block" - this.loadingRef.instance.style.display = "none" - }) + this.databases.set(pkgs) - this.loginButtonRef.instance.addEventListener("click", () => this.handleClick()) - this.downloadButtonRef.instance.addEventListener("click", () => this.handleDownloadClick()) + const activePackage = await this.navigationDataInterface.get_active_package() - this.executeIcaoButtonRef.instance.addEventListener("click", () => { - console.time("query") - this.navigationDataInterface - .get_airport(this.icaoInputRef.instance.value) - .then(airport => { - console.info(airport) - this.outputRef.instance.textContent = JSON.stringify(airport, null, 2) - }) - .catch(e => console.error(e)) - .finally(() => console.timeEnd("query")) - }) + this.activeDatabase.set(activePackage) + this.selectedDatabase.set(activePackage) + if (activePackage !== null) { + this.selectedDatabaseIndex.set(pkgs.findIndex(pkg => pkg.uuid === activePackage.uuid)) + } - this.executeSqlButtonRef.instance.addEventListener("click", () => { - console.time("query") - this.navigationDataInterface - .execute_sql(this.sqlInputRef.instance.value, []) - .then(result => { - console.info(result) - this.outputRef.instance.textContent = JSON.stringify(result, null, 2) - }) - .catch(e => console.error(e)) - .finally(() => console.timeEnd("query")) + // show the auth container + this.authContainerRef.instance.style.display = "block" + this.loadingRef.instance.style.display = "none" }) - AuthService.user.sub(user => { - if (user) { - this.qrCodeRef.instance.src = "" - this.qrCodeRef.instance.style.display = "none" - this.loginButtonRef.instance.textContent = "Log out" - this.textRef.instance.textContent = `Welcome, ${user.preferred_username}` - this.displayMessage("") - - this.handleLogin() - } else { - this.loginButtonRef.instance.textContent = "Sign in" - this.textRef.instance.textContent = "Not logged in" + this.resetPackageList.map(async val => { + if (!val) { + return } - }, true) - } - private async handleClick() { - try { - if (AuthService.getUser()) { - await AuthService.signOut() - } else { - this.cancelSource = CancelToken.source() // Reset any previous cancellations - this.displayMessage("Authenticating.. Scan code (or click it) to sign in") - await AuthService.signIn(p => { - if (p) { - this.qrCodeRef.instance.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${p.verification_uri_complete}` - this.qrCodeRef.instance.style.display = "block" - this.qrCodeRef.instance.onclick = () => { - OpenBrowser(p.verification_uri_complete) - } - } - }, this.cancelSource.token) - } - } catch (err) { - this.qrCodeRef.instance.style.display = "none" - if (err instanceof Error) this.displayError(err.message) - else this.displayError(`Unknown error: ${String(err)}`) - } - } + const pkgs = await this.navigationDataInterface.list_available_packages(true) - private handleLogin() { - // Let's display all of our packages - packages - .listPackages() - .then(pkgs => { - for (const pkg of pkgs) { - this.dropdownRef.instance.addDropdownItem(pkg.format, pkg.format) - } - }) - .catch(e => console.error(e)) - } - - private async handleDownloadClick() { - try { - if (!this.navigationDataInterface.getIsInitialized()) throw new Error("Navigation data interface not initialized") - - const format = this.dropdownRef.instance.getNavigationDataFormat() - if (!format) throw new Error("Unable to fetch package: No navigation data format has been selected") + const activePackage = await this.navigationDataInterface.get_active_package() - // Get default package for client - const pkg = await packages.getPackage(format) + this.activeDatabase.set(activePackage) + this.selectedDatabase.set(activePackage) + this.selectedDatabaseIndex.set(pkgs.findIndex(pkg => pkg.uuid === activePackage?.uuid)) - // Download navigation data to work dir - await this.navigationDataInterface.download_navigation_data(pkg.file.url) + this.databases.set(pkgs) - // Update navigation data status - this.navigationDataInterface - .get_navigation_data_install_status() - .then(status => this.navigationDataStatus.set(status)) - .catch(e => console.error(e)) - - this.displayMessage("Navigation data downloaded") - } catch (err) { - if (err instanceof Error) this.displayError(err.message) - else this.displayError(`Unknown error: ${String(err)}`) - } - } - - private displayMessage(message: string) { - this.navigationDataTextRef.instance.textContent = message - this.navigationDataTextRef.instance.style.color = "white" - } - - private displayError(error: string) { - this.navigationDataTextRef.instance.textContent = error - this.navigationDataTextRef.instance.style.color = "red" + this.resetPackageList.set(false) + }) } } diff --git a/examples/gauge/Components/List.tsx b/examples/gauge/Components/List.tsx new file mode 100644 index 00000000..897b3199 --- /dev/null +++ b/examples/gauge/Components/List.tsx @@ -0,0 +1,135 @@ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* + Mostly taken directly from https://github.com/microsoft/msfs-avionics-mirror/blob/main/src/workingtitle-instruments-g1000/html_ui/Pages/VCockpit/Instruments/NavSystems/WTG1000/Shared/UI/List.tsx + I'm not reinventing the wheel. +*/ + +import { + ComponentProps, + DisplayComponent, + FSComponent, + SubscribableArray, + SubscribableArrayEventType, + VNode, +} from "@microsoft/msfs-sdk" + +/** The properties for the List component. */ +interface ListProps extends ComponentProps { + /** + * The data for this list. + * @type {any[]} + */ + data: SubscribableArray + + /** A function defining how to render each list item. */ + renderItem: (data: any, index: number) => VNode + + /** CSS class(es) to add to the root of the list component. */ + class?: string +} + +/** The List component. */ +export class List extends DisplayComponent { + private readonly _listContainer = FSComponent.createRef() + + /** @inheritdoc */ + public onAfterRender(): void { + this.renderList() + this.props.data.sub(this.onDataChanged.bind(this)) + } + + /** + * A callback fired when the array subject data changes. + * @param index The index of the change. + * @param type The type of change. + * @param item The item that was changed. + */ + private onDataChanged(index: number, type: SubscribableArrayEventType, item: any | any[]): void { + switch (type) { + case SubscribableArrayEventType.Added: + { + const el = this._listContainer.instance.children.item(index) + if (Array.isArray(item)) { + for (let i = 0; i < item.length; i++) { + this.addDomNode(item[i], index + i, el) + } + } else { + this.addDomNode(item, index, el) + } + } + break + case SubscribableArrayEventType.Removed: + { + if (Array.isArray(item)) { + for (let i = 0; i < item.length; i++) { + this.removeDomNode(index) + } + } else { + this.removeDomNode(index) + } + } + break + case SubscribableArrayEventType.Cleared: + this._listContainer.instance.innerHTML = "" + break + } + } + + /** + * Removes a dom node from the collection at the specified index. + * @param index The index to remove. + */ + private removeDomNode(index: number): void { + const child = this._listContainer.instance.childNodes.item(index) + this._listContainer.instance.removeChild(child) + } + + /** + * Adds a list rendered dom node to the collection. + * @param item Item to render and add. + * @param index The index to add at. + * @param el The element to add to. + */ + private addDomNode(item: any, index: number, el: Element | null): void { + const node = this.renderListItem(item, index) + if (el !== null) { + node && el && FSComponent.renderBefore(node, el as any) + } else { + el = this._listContainer.instance + node && el && FSComponent.render(node, el as any) + } + } + + /** + * Renders a list item + * @param dataItem The data item to render. + * @param index The index to render at. + * @returns list item vnode + * @throws error when the resulting vnode is not a scrollable control + */ + private renderListItem(dataItem: any, index: number): VNode { + return this.props.renderItem(dataItem, index) + } + /** Renders the list of data items. */ + private renderList(): void { + // clear all items + this._listContainer.instance.textContent = "" + + // render items + const dataLen = this.props.data.length + for (let i = 0; i < dataLen; i++) { + const vnode = this.renderListItem(this.props.data.get(i), i) + if (vnode !== undefined) { + FSComponent.render(vnode, this._listContainer.instance) + } + } + } + + /** @inheritdoc */ + render(): VNode { + return
+ } +} diff --git a/examples/gauge/Components/Pages/Auth/Auth.tsx b/examples/gauge/Components/Pages/Auth/Auth.tsx new file mode 100644 index 00000000..bf323932 --- /dev/null +++ b/examples/gauge/Components/Pages/Auth/Auth.tsx @@ -0,0 +1,167 @@ +import { ComponentProps, DisplayComponent, FSComponent, Subscribable, VNode } from "@microsoft/msfs-sdk" +import { + DownloadProgressPhase, + NavigraphEventType, + NavigraphNavigationDataInterface, + PackageInfo, +} from "@navigraph/msfs-navigation-data-interface" +import { CancelToken } from "navigraph/auth" +import { packages } from "../../../Lib/navigraph" +import { AuthService } from "../../../Services/AuthService" +import { Dropdown } from "../../Dropdown" + +interface AuthPageProps extends ComponentProps { + navigationDataInterface: NavigraphNavigationDataInterface + activeDatabase: Subscribable + setActiveDatabase: (database: PackageInfo | null) => void +} + +export class AuthPage extends DisplayComponent { + private readonly textRef = FSComponent.createRef() + private readonly loginButtonRef = FSComponent.createRef() + private readonly navigationDataTextRef = FSComponent.createRef() + private readonly qrCodeRef = FSComponent.createRef() + private readonly dropdownRef = FSComponent.createRef() + private readonly downloadButtonRef = FSComponent.createRef() + + private cancelSource = CancelToken.source() + + constructor(props: AuthPageProps) { + super(props) + + this.props.navigationDataInterface.onEvent(NavigraphEventType.DownloadProgress, data => { + switch (data.phase) { + case DownloadProgressPhase.Downloading: + this.displayMessage("Downloading navigation data...") + break + case DownloadProgressPhase.Cleaning: + if (!data.deleted) return + this.displayMessage(`Cleaning destination directory. ${data.deleted} files deleted so far`) + break + case DownloadProgressPhase.Extracting: { + // Ensure non-null + if (!data.unzipped || !data.total_to_unzip) return + const percent = Math.round((data.unzipped / data.total_to_unzip) * 100) + this.displayMessage(`Unzipping files... ${percent}% complete`) + break + } + } + }) + } + + onAfterRender(node: VNode): void { + super.onAfterRender(node) + + this.loginButtonRef.instance.addEventListener("click", () => this.handleClick()) + this.downloadButtonRef.instance.addEventListener("click", () => this.handleDownloadClick()) + + AuthService.user.sub(user => { + if (user) { + this.qrCodeRef.instance.src = "" + this.qrCodeRef.instance.style.display = "none" + this.loginButtonRef.instance.textContent = "Log out" + this.textRef.instance.textContent = `Welcome, ${user.preferred_username}` + this.displayMessage("") + + this.handleLogin() + } else { + this.loginButtonRef.instance.textContent = "Sign in" + this.textRef.instance.textContent = "Not logged in" + } + }, true) + } + + private async handleClick() { + try { + if (AuthService.getUser()) { + await AuthService.signOut() + } else { + this.cancelSource = CancelToken.source() // Reset any previous cancellations + this.displayMessage("Authenticating.. Scan code (or click it) to sign in") + await AuthService.signIn(p => { + if (p) { + this.qrCodeRef.instance.src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${p.verification_uri_complete}` + this.qrCodeRef.instance.style.display = "block" + this.qrCodeRef.instance.onclick = () => { + OpenBrowser(p.verification_uri_complete) + } + } + }, this.cancelSource.token) + } + } catch (err) { + this.qrCodeRef.instance.style.display = "none" + if (err instanceof Error) this.displayError(err.message) + else this.displayError(`Unknown error: ${String(err)}`) + } + } + + private handleLogin() { + // Let's display all of our packages + packages + .listPackages() + .then(pkgs => { + for (const pkg of pkgs) { + this.dropdownRef.instance.addDropdownItem(pkg.format, pkg.format) + } + }) + .catch(e => console.error(e)) + } + + private async handleDownloadClick() { + try { + if (!this.props.navigationDataInterface.getIsInitialized()) + throw new Error("Navigation data interface not initialized") + + const format = this.dropdownRef.instance.getNavigationDataFormat() + if (!format) throw new Error("Unable to fetch package: No navigation data format has been selected") + + // Get default package for client + const pkg = await packages.getPackage(format) + + // Download navigation data to work dir and set active + await this.props.navigationDataInterface.download_navigation_data(pkg.file.url, true) + + // Update navigation data status + this.props.setActiveDatabase(await this.props.navigationDataInterface.get_active_package()) + + this.displayMessage("Navigation data downloaded") + } catch (err) { + if (err instanceof Error) this.displayError(err.message) + else this.displayError(`Unknown error: ${String(err)}`) + } + } + + private displayMessage(message: string) { + this.navigationDataTextRef.instance.textContent = message + this.navigationDataTextRef.instance.style.color = "white" + } + + private displayError(error: string) { + this.navigationDataTextRef.instance.textContent = error + this.navigationDataTextRef.instance.style.color = "red" + } + + render(): VNode { + return ( +
+

Authentication

+
+
+

Step 1 - Sign in

+
Loading
+
+
+ +
+
+

Step 2 - Select Database

+ +
+ Download +
+
+
+
+ ) + } +} diff --git a/examples/gauge/Components/Pages/Dashboard/Dashboard.tsx b/examples/gauge/Components/Pages/Dashboard/Dashboard.tsx new file mode 100644 index 00000000..bca93e19 --- /dev/null +++ b/examples/gauge/Components/Pages/Dashboard/Dashboard.tsx @@ -0,0 +1,192 @@ +import { + ComponentProps, + DisplayComponent, + FSComponent, + MappedSubject, + MappedSubscribable, + MutableSubscribable, + Subscribable, + SubscribableArray, + VNode, +} from "@microsoft/msfs-sdk" +import { NavigraphNavigationDataInterface, PackageInfo } from "@navigraph/msfs-navigation-data-interface" +import { List } from "../../List" +import { Button, InterfaceNavbarItemV2, InterfaceSwitch } from "../../Utils" + +interface DashboardProps extends ComponentProps { + databases: SubscribableArray + reloadPackageList: MutableSubscribable + selectedDatabase: Subscribable + selectedDatabaseIndex: Subscribable + setSelectedDatabase: (database: PackageInfo) => void + setSelectedDatabaseIndex: (index: number) => void + activeDatabase: Subscribable + interface: NavigraphNavigationDataInterface +} + +export class Dashboard extends DisplayComponent { + private readonly _selectedCallback = this.props.selectedDatabaseIndex.map(val => { + if (this.props.databases.length !== 0) { + this.props.setSelectedDatabase(this.props.databases.get(val)) + } + }) + private readonly showActiveDatabase = MappedSubject.create( + ([selectedDatabase, activeDatabase]) => selectedDatabase?.uuid === activeDatabase?.uuid, + this.props.selectedDatabase, + this.props.activeDatabase, + ) + + private displayItems(data: PackageInfo, index: number): VNode { + return ( + val === index)} + setActive={() => this.props.setSelectedDatabaseIndex(index)} + > +

+ {data.cycle.cycle} - {data.cycle.format} +

+
+ ) + } + + private setDatabase() { + const selectedDatabase = this.props.selectedDatabase.get() + + if (selectedDatabase === null) { + return + } + + this.props.interface + .set_active_package(selectedDatabase.uuid) + .then(_res => {}) + .catch(err => console.error(err)) + + this.props.reloadPackageList.set(true) + } + + private deleteSelected() { + const prevSelectedDatabase = this.props.selectedDatabase.get() + + if (prevSelectedDatabase === null || this.props.databases.length <= 1) { + return + } + + let pkg + + try { + if (this.props.selectedDatabaseIndex.get() === 0) { + pkg = this.props.databases.get(1) + } else { + pkg = this.props.databases.get(0) + } + } catch { + return + } + + if (this.showActiveDatabase.get()) { + this.props.interface + .set_active_package(pkg.uuid) + .then(_res => {}) + .catch(err => console.error(err)) + } + + this.props.interface + .delete_package(prevSelectedDatabase.uuid) + .then(_res => {}) + .catch(err => console.error(err)) + + this.props.reloadPackageList.set(true) + } + + render(): VNode { + return ( +
+

Dashboard

+
+
+

Databases

+
+
+ this.displayItems(data as PackageInfo, index)} + /> +
+
+
+ + +
+
+ +
+
+ ) + } +} + +interface ActiveDatabaseProps extends ComponentProps { + selectedDatabase: Subscribable + showActiveDatabase: MappedSubscribable +} + +class ActiveDatabase extends DisplayComponent { + private readonly isActive = this.props.showActiveDatabase.map(val => (val ? 0 : 1)) + + render(): VNode { + return ( +
+ Active Database

], + [1,

Selected Database

], + ]} + /> +
+
+
+ UUID: + {this.props.selectedDatabase.map(s => s?.uuid)} +
+
+

Bundled

+

{this.props.selectedDatabase.map(s => s?.is_bundled)}

+
+
+

Installed format

+

+ {this.props.selectedDatabase.map(s => `${s?.cycle.format} revision ${s?.cycle.revision}`)} +

+
+
+

Active path

+

{this.props.selectedDatabase.map(s => s?.path)}

+
+
+

Active cycle

+

{this.props.selectedDatabase.map(s => s?.cycle.cycle)}

+
+
+

Validity period

+

{this.props.selectedDatabase.map(s => s?.cycle.validityPeriod)}

+
+
+
+
+ ) + } +} diff --git a/examples/gauge/Components/Pages/Test/Test.tsx b/examples/gauge/Components/Pages/Test/Test.tsx new file mode 100644 index 00000000..6027c8be --- /dev/null +++ b/examples/gauge/Components/Pages/Test/Test.tsx @@ -0,0 +1,448 @@ +import { + ComponentProps, + DisplayComponent, + FSComponent, + MappedSubject, + ObjectSubject, + Subject, + VNode, +} from "@microsoft/msfs-sdk" +import { Coordinates, NavigraphNavigationDataInterface } from "@navigraph/msfs-navigation-data-interface" +import { Checkbox, Input } from "../../Input" +import { Button, InterfaceNavbarItemV2, InterfaceSwitch } from "../../Utils" + +interface TestPageProps extends ComponentProps { + interface: NavigraphNavigationDataInterface +} + +interface FunctionDescriptor { + index: number + arguments: string[] + name: string + functionCallback: (input?: string, inputAlt?: string) => Promise +} + +interface InputState { + active: boolean + type: InputStateType + hint: string +} + +enum InputStateType { + String, + Bool, +} + +export class TestPage extends DisplayComponent { + private readonly functionList: FunctionDescriptor[] = [ + { + index: 0, + arguments: ["url: string", "activate: bool"], + name: "DownloadNavigationData", + functionCallback: input => this.props.interface.download_navigation_data(input ?? ""), + }, + { + index: 1, + arguments: [], + name: "GetActivePackage", + functionCallback: () => this.props.interface.get_active_package(), + }, + { + index: 2, + arguments: ["sort: bool", "filter: bool"], + name: "ListAvailablePackages", + functionCallback: (input, inputAlt) => + this.props.interface.list_available_packages(this.strToBool(input), this.strToBool(inputAlt)), + }, + { + index: 3, + arguments: ["uuid: string"], + name: "SetActivePackage", + functionCallback: input => this.props.interface.download_navigation_data(input ?? ""), + }, + { + index: 4, + arguments: ["uuid: string"], + name: "DeletePackage", + functionCallback: input => this.props.interface.delete_package(input ?? ""), + }, + { + index: 5, + arguments: ["count?: string"], + name: "CleanPackages", + functionCallback: input => this.props.interface.clean_packages(Number(input)), + }, + { + index: 6, + arguments: [], + name: "GetDatabaseInfo", + functionCallback: () => this.props.interface.get_database_info(), + }, + { + index: 7, + arguments: ["ident: string"], + name: "GetAirport", + functionCallback: input => this.props.interface.get_airport(input ?? ""), + }, + { + index: 8, + arguments: ["ident: string"], + name: "GetWaypoints", + functionCallback: input => this.props.interface.get_waypoints(input ?? ""), + }, + { + index: 9, + arguments: ["ident: string"], + name: "GetVhfNavaids", + functionCallback: input => this.props.interface.get_vhf_navaids(input ?? ""), + }, + { + index: 10, + arguments: ["ident: string"], + name: "GetNdbNavaids", + functionCallback: input => this.props.interface.get_ndb_navaids(input ?? ""), + }, + { + index: 11, + arguments: ["ident: string"], + name: "GetAirways", + functionCallback: input => this.props.interface.get_airways(input ?? ""), + }, + { + index: 12, + arguments: ["fixIdent: string", "fixIcao: string"], + name: "GetAirwaysAtFix", + functionCallback: (input, inputAlt) => this.props.interface.get_airways_at_fix(input ?? "", inputAlt ?? ""), + }, + { + index: 13, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetAirportsInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_airports_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 14, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetWaypointsInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_waypoints_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 15, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetVhfNavaidsInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_vhf_navaids_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 16, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetNdbNavaidsInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_ndb_navaids_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 17, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetAirwaysInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_airways_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 18, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetControlledAirspacesInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_controlled_airspaces_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 19, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetRestrictiveAirspacesInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_restrictive_airspaces_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 20, + arguments: ["center: (lat, long)", "range: nm"], + name: "GetCommunicationsInRange", + functionCallback: (input, inputAlt) => + this.props.interface.get_communications_in_range(this.strToCoords(input), Number(inputAlt ?? 0)), + }, + { + index: 21, + arguments: ["airportIdent: string"], + name: "GetRunwaysAtAirport", + functionCallback: input => this.props.interface.get_runways_at_airport(input ?? ""), + }, + { + index: 22, + arguments: ["airportIdent: string"], + name: "GetDeparturesAtAirport", + functionCallback: input => this.props.interface.get_departures_at_airport(input ?? ""), + }, + { + index: 23, + arguments: ["airportIdent: string"], + name: "GetArrivalsAtAirport", + functionCallback: input => this.props.interface.get_arrivals_at_airport(input ?? ""), + }, + { + index: 24, + arguments: ["airportIdent: string"], + name: "GetApproachesAtAirport", + functionCallback: input => this.props.interface.get_approaches_at_airport(input ?? ""), + }, + { + index: 25, + arguments: ["airportIdent: string"], + name: "GetWaypointsAtAirport", + functionCallback: input => this.props.interface.get_waypoints_at_airport(input ?? ""), + }, + { + index: 26, + arguments: ["airportIdent: string"], + name: "GetNdbNavaidsAtAirport", + functionCallback: input => this.props.interface.get_ndb_navaids_at_airport(input ?? ""), + }, + { + index: 27, + arguments: ["airportIdent: string"], + name: "GetGatesAtAirport", + functionCallback: input => this.props.interface.get_gates_at_airport(input ?? ""), + }, + { + index: 28, + arguments: ["airportIdent: string"], + name: "GetCommunicationsAtAirport", + functionCallback: input => this.props.interface.get_communications_at_airport(input ?? ""), + }, + { + index: 29, + arguments: ["airportIdent: string"], + name: "GetGlsNavaidsAtAirport", + functionCallback: input => this.props.interface.get_gls_navaids_at_airport(input ?? ""), + }, + { + index: 30, + arguments: ["airportIdent: string"], + name: "GetPathPointsAtAirport", + functionCallback: input => this.props.interface.get_path_points_at_airport(input ?? ""), + }, + ] + + private readonly input1 = Subject.create("") + private readonly input2 = Subject.create("") + private readonly output = Subject.create("") + private readonly selectedFunction = Subject.create(0) + private readonly selectedFunctionObj = this.selectedFunction.map(index => this.functionList[index]) + private readonly input1State = ObjectSubject.create({ + active: false, + type: InputStateType.String, + hint: this.functionList[this.selectedFunction.get()].arguments[0] ?? "", + }) + private readonly input2State = ObjectSubject.create({ + active: false, + type: InputStateType.String, + hint: this.functionList[this.selectedFunction.get()].arguments[1] ?? "", + }) + + private doubleInputCss = MappedSubject.create( + ([input1, input2]) => + `flex flex-row h-16 bg-ng-background-500 items-center p-2 ${input1.active && input2.active ? "space-x-2" : ""}`, + this.input1State, + this.input2State, + ) + + private strToBool(input?: string): boolean { + return input == "true" ? true : false + } + + private strToCoords(input?: string): Coordinates { + const splitInput = (input ?? "").replace(/[(){}\s]/g, "").split(",") + + const coords: Coordinates = { + lat: Number(splitInput[0] ?? 0.0), + long: Number(splitInput[1] ?? 0.0), + } + + return coords + } + + private handleFunction = () => { + const functionObj = this.selectedFunctionObj.get() + const input1 = this.input1.get() + const input2 = this.input2.get() + + functionObj + .functionCallback(input1, input2) + .then(obj => this.output.set(JSON.stringify(obj, null, 2))) + .catch(err => this.output.set(JSON.stringify(err, null, 2))) + } + + onAfterRender(node: VNode): void { + super.onAfterRender(node) + + this.selectedFunctionObj.map(functionObj => { + const functionArgCount = functionObj.arguments.length + + switch (functionArgCount) { + case 1: { + this.input1State.set("active", true) + this.input2State.set("active", false) + break + } + case 2: { + this.input1State.set("active", true) + this.input2State.set("active", true) + break + } + default: { + this.input1State.set("active", false) + this.input2State.set("active", false) + break + } + } + + this.input1.set("") + this.input2.set("") + + functionObj.arguments.forEach((value, index) => { + const argumentType = value.includes("bool") ? InputStateType.Bool : InputStateType.String + + switch (index) { + case 1: { + this.input2State.set("type", argumentType) + this.input2State.set("hint", functionObj.arguments[1]) + if (argumentType === InputStateType.Bool) { + this.input2.set("false") + } + break + } + default: { + this.input1State.set("type", argumentType) + this.input1State.set("hint", functionObj.arguments[0]) + if (argumentType === InputStateType.Bool) { + this.input1.set("false") + } + break + } + } + }) + }) + } + + render(): VNode { + return ( +
+

Test

+
+
+
+ {this.functionList.map(obj => ( + index === obj.index)} + setActive={() => this.selectedFunction.set(obj.index)} + > +

{obj.name}

+

({obj.arguments.join(", ")})

+
+ ))} +
+
+ (obj.active ? (obj.type === InputStateType.String ? 0 : 1) : 2))} + pages={[ + [ + 0, + this.input1.set(value)} + default={this.input1State.map(obj => obj.hint)} + />, + ], + [ + 1, +
+

{this.input1State.map(obj => obj.hint.split(":")[0] + ":")}

+ this.input1.set(value)} /> +
, + ], + [ + 2, +
+ No Inputs +
, + ], + ]} + /> + (obj.active ? "h-full w-1/2 flex content-center" : ""))} + intoClass="flex-grow flex content-center" + active={this.input2State.map(obj => (obj.active ? (obj.type === InputStateType.String ? 0 : 1) : 2))} + pages={[ + [ + 0, + this.input2.set(value)} + default={this.input2State.map(obj => obj.hint)} + />, + ], + [ + 1, +
+

{this.input2State.map(obj => obj.hint.split(":")[0] + ":")}

+ this.input2.set(value)} /> +
, + ], + [2, <>], + ]} + /> +
+
+
+
+

{this.output}

+
+ +
+
+
+ ) + } +} diff --git a/examples/gauge/Components/Utils.tsx b/examples/gauge/Components/Utils.tsx new file mode 100644 index 00000000..f94419cf --- /dev/null +++ b/examples/gauge/Components/Utils.tsx @@ -0,0 +1,153 @@ +import { + ComponentProps, + DisplayComponent, + FSComponent, + Subscribable, + SubscribableUtils, + VNode, +} from "@microsoft/msfs-sdk" + +type Page = [number, VNode] + +interface InterfaceSwitchProps extends ComponentProps { + class?: string | Subscribable + intoClass?: string + active: Subscribable + pages: Page[] + noTheming?: boolean + intoNoTheming?: boolean + hideLast?: boolean +} + +interface InterfaceSwitchPageProps extends ComponentProps { + class?: string + active: Subscribable + noTheming: boolean +} + +export class InterfaceSwitch extends DisplayComponent { + private readonly activeClass = SubscribableUtils.toSubscribable(this.props.class ?? "", true).map( + val => `${this.props.noTheming ? "" : "size-full"} ${val ?? "bg-inherit"}`, + ) + + private readonly visibility = (pageNumber: number) => + this.props.active.map(val => + this.props.hideLast ? val === pageNumber && val !== this.props.pages.length - 1 : val === pageNumber, + ) + + render(): VNode { + return ( +
+ {this.props.pages.map(([pageNumber, page]) => ( + + {page} + + ))} +
+ ) + } +} + +class InterfaceSwitchPage extends DisplayComponent { + private readonly activeCss = this.props.active.map( + val => `${val ? "block" : "!hidden"} ${this.props.noTheming ? "" : "size-full p-6"} ${this.props.class ?? ""}`, + ) + + render(): VNode { + return
{this.props.children}
+ } +} + +interface InterfaceNavbarProps extends ComponentProps { + tabs: [number, string][] + setActive: (pageNumber: number) => void + active: Subscribable + class?: string + intoClass?: string + activeClass?: string +} + +interface InterfaceNavbarItemProps extends ComponentProps { + content: string + active: Subscribable + setActive: () => void + class?: string + activeClass?: string +} + +export class InterfaceNavbar extends DisplayComponent { + render(): VNode { + return ( +
+ {this.props.tabs.map(([pageNumber, content]) => ( + val === pageNumber)} + setActive={() => this.props.setActive(pageNumber)} + class="p-4 bg-inherit hover:text-blue-25 text-2xl text-center align-middle" + activeClass="text-blue-25" + content={content} + /> + ))} +
+ ) + } +} + +export class InterfaceNavbarItem extends DisplayComponent { + private readonly activeCss = this.props.active.map( + val => `${this.props.class ?? "size-full"} ${val ? this.props.activeClass ?? "" : ""}`, + ) + + render(): VNode { + return ( + + ) + } +} + +export class InterfaceNavbarItemV2 extends DisplayComponent { + private readonly activeCss = this.props.active.map( + val => `${this.props.class ?? "size-full"} ${val ? this.props.activeClass ?? "" : ""}`, + ) + + render(): VNode { + return ( + + ) + } +} + +interface ButtonProps extends ComponentProps { + class?: Subscribable | string + onClick: () => void +} + +export class Button extends DisplayComponent { + private readonly buttonRef = FSComponent.createRef() + + private readonly class = SubscribableUtils.toSubscribable(this.props.class ?? "", true).map( + val => val ?? "text-inherit", + ) + + onAfterRender(node: VNode): void { + super.onAfterRender(node) + + this.buttonRef.instance.addEventListener("click", this.props.onClick) + } + + render(): VNode { + return ( +
+ {this.props.children} +
+ ) + } +} diff --git a/examples/gauge/MyInstrument.css b/examples/gauge/MyInstrument.css index 04062fe6..efb0f989 100644 --- a/examples/gauge/MyInstrument.css +++ b/examples/gauge/MyInstrument.css @@ -1,4 +1,25 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + #InstrumentContent { width: 100%; height: 100%; } + +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background: #1d2838; +} + +::-webkit-scrollbar-thumb { + background: #7f868f; + border-radius: 2rem; +} + +::-webkit-scrollbar-corner { + background: #1d2838; +} diff --git a/examples/gauge/package.json b/examples/gauge/package.json index f079e0a5..6a8b59c3 100644 --- a/examples/gauge/package.json +++ b/examples/gauge/package.json @@ -14,13 +14,17 @@ "@microsoft/msfs-sdk": "^0.6.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.5", + "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "dotenv": "^16.3.1", "esbuild": "^0.19.5", + "postcss": "^8.4.47", "rollup": "^2.79.1", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-esbuild": "^6.1.0", - "rollup-plugin-import-css": "^3.3.5" + "rollup-plugin-import-css": "^3.3.5", + "rollup-plugin-postcss": "^4.0.2", + "tailwindcss": "^3.4.14" }, "dependencies": { "@navigraph/msfs-navigation-data-interface": "*", diff --git a/examples/gauge/postcss.config.js b/examples/gauge/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/examples/gauge/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/gauge/rollup.config.js b/examples/gauge/rollup.config.js index 93973ee8..bced2e38 100644 --- a/examples/gauge/rollup.config.js +++ b/examples/gauge/rollup.config.js @@ -3,10 +3,12 @@ import replace from "@rollup/plugin-replace" import dotenv from "dotenv" import copy from "rollup-plugin-copy" import esbuild from "rollup-plugin-esbuild" -import css from "rollup-plugin-import-css" +// import css from "rollup-plugin-import-css" +import postcss from "rollup-plugin-postcss" dotenv.config() +// eslint-disable-next-line no-undef const DEBUG = process.env.DEBUG === "true" let outputDest = "../aircraft/PackageSources" @@ -21,14 +23,20 @@ export default { format: "es", }, plugins: [ - css({ output: "MyInstrument.css" }), resolve({ extensions: [".js", ".jsx", ".ts", ".tsx"] }), - esbuild({ target: "es2017" }), + esbuild({ target: "es6" }), replace({ + // eslint-disable-next-line no-undef "process.env.NG_CLIENT_ID": JSON.stringify(process.env.NG_CLIENT_ID), + // eslint-disable-next-line no-undef "process.env.NG_CLIENT_SECRET": JSON.stringify(process.env.NG_CLIENT_SECRET), preventAssignment: true, }), + postcss({ + extract: true, + minimize: true, + output: "MyInstrument.css", + }), copy({ targets: [ { diff --git a/examples/gauge/tailwind.config.js b/examples/gauge/tailwind.config.js new file mode 100644 index 00000000..a0665036 --- /dev/null +++ b/examples/gauge/tailwind.config.js @@ -0,0 +1,178 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./MyInstrument.html", "./MyInstrument.tsx", "./Components/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + fontFamily: { + inter: ["Inter", "sans-serif"], + }, + colors: { + "ng-background": { + 900: "#0E131B", + 800: "#111721", + 700: "#151D29", + 600: "#192230", + 500: "#1D2838", + 400: "#222F42", + 300: "#27364D", + 200: "#2E3F59", + 100: "#324562", + 50: "#374C6C", + 25: "#3D5476", + }, + blue: { + 900: "#113355", + 800: "#133B62", + 700: "#16426F", + 600: "#184A7B", + 500: "#1B5288", + 400: "#1D5995", + 300: "#1F5F9E", + 200: "#2064A6", + 100: "#2369AF", + 50: "#2571BB", + 25: "#287BCC", + 5: "#3788D7", + }, + teal: { + 900: "#115555", + 800: "#136262", + 700: "#166F6F", + 600: "#187B7B", + 500: "#1B8888", + 400: "#1D9595", + 300: "#1F9E9E", + 200: "#22AFAF", + 100: "#24B7B7", + }, + purple: { + 900: "#1B0E21", + 800: "#25132E", + 700: "#30193B", + 600: "#3A1E47", + 500: "#442454", + 400: "#4F2961", + 300: "#552C68", + 200: "#5B2F6F", + 100: "#633479", + 50: "#6C3984", + 25: "#753D8F", + 5: "#7E429A", + }, + "blue-gray": { + 900: "#27313F", + 800: "#2B3645", + 700: "#2F3B4B", + 600: "#333F52", + 500: "#374458", + 400: "#3B495E", + 300: "#3F4E64", + 200: "#43536B", + 100: "#475871", + 50: "#4D5F7A", + }, + gray: { + 900: "#5A6068", + 800: "#666D75", + 700: "#727982", + 600: "#7F868F", + 500: "#8E949C", + 400: "#9AA0A7", + 300: "#A8ADB3", + 200: "#B6BABF", + 100: "#C3C7CB", + 50: "#D1D3D7", + 25: "#DEE0E2", + 5: "#ECEDEE", + }, + sid: { + 900: "#BA3476", + 800: "#C34080", + 700: "#CB4C8B", + 600: "#D45895", + 500: "#DC649F", + 400: "#E075A9", + 300: "#E485B4", + 200: "#E796BE", + 100: "#E99EC3", + }, + star: { + 900: "#6CA550", + 800: "#78B15A", + 700: "#82BA67", + 600: "#8DC273", + 500: "#98CA7F", + 400: "#A4D08D", + 300: "#AFD69C", + 200: "#BBDCAA", + 100: "#C1DFB1", + }, + app: { + 900: "#EC7B2C", + 800: "#EE8842", + 700: "#F09354", + 600: "#F19F67", + 500: "#F3AC7A", + 400: "#F5B78D", + 300: "#F6C3A0", + 200: "#F8CEB2", + 100: "#F9D4BC", + }, + red: { + 900: "#EC7B2C", + 800: "#EE8842", + 700: "#F09354", + 600: "#F19F67", + 500: "#F3AC7A", + 400: "#F5B78D", + 300: "#F6C3A0", + 200: "#F8CEB2", + 100: "#F9D4BC", + }, + orange: { + 900: "#794006", + 800: "#8D4A07", + 700: "#A05408", + 600: "#B35E09", + 500: "#C7690A", + 400: "#D8720B", + 300: "#ED7D0C", + 200: "#F3871B", + 100: "#F4912F", + 50: "#F59C42", + }, + yellow: { + 900: "#A48B0A", + 800: "#B79B0B", + 700: "#CAAB0C", + 600: "#DEBC0D", + 500: "#F1CC0E", + 400: "#F2D021", + 300: "#F3D435", + 200: "#F4D848", + 100: "#F5DC5B", + }, + rwy: { + 900: "#1AADEC", + 800: "#22B1EF", + 700: "#32B9F3", + 600: "#41C1F8", + 500: "#51C9FD", + 400: "#65CFFD", + 300: "#79D5FD", + 200: "#8EDCFE", + 100: "#98DFFE", + }, + }, + }, + }, + corePlugins: { + backdropOpacity: false, + backgroundOpacity: false, + borderOpacity: false, + divideOpacity: false, + ringOpacity: false, + textOpacity: false, + }, + plugins: [], +} diff --git a/package-lock.json b/package-lock.json index fc1a28eb..4a80c174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", - "bigint-buffer": "^1.1.5", "dotenv": "^16.3.1", "eslint": "^8.52.0", "eslint-config-prettier": "^9.0.0", @@ -41,13 +40,17 @@ "@microsoft/msfs-sdk": "^0.6.0", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.5", + "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "dotenv": "^16.3.1", "esbuild": "^0.19.5", + "postcss": "^8.4.47", "rollup": "^2.79.1", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-esbuild": "^6.1.0", - "rollup-plugin-import-css": "^3.3.5" + "rollup-plugin-import-css": "^3.3.5", + "rollup-plugin-postcss": "^4.0.2", + "tailwindcss": "^3.4.14" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -59,6 +62,18 @@ "node": ">=0.10.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -2199,9 +2214,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", - "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -2212,9 +2227,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", - "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -2225,9 +2240,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", - "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -2238,9 +2253,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", - "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -2251,9 +2266,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", - "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -2264,9 +2292,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", - "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -2277,9 +2305,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", - "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -2289,10 +2317,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", - "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -2302,10 +2343,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", - "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -2316,9 +2370,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", - "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -2329,9 +2383,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", - "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -2342,9 +2396,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", - "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -2355,9 +2409,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", - "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -2391,6 +2445,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -2457,9 +2520,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/fs-extra": { @@ -2902,6 +2965,43 @@ "node": ">=8" } }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -3094,19 +3194,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/bigint-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", - "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.3.0" - }, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3116,14 +3203,11 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true }, "node_modules/brace-expansion": { "version": "2.0.1", @@ -3135,21 +3219,21 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -3166,10 +3250,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -3259,10 +3343,31 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001572", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", - "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==", + "version": "1.0.30001677", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", + "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", "dev": true, "funding": [ { @@ -3407,6 +3512,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -3428,6 +3539,15 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -3550,10 +3670,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3563,6 +3684,177 @@ "node": ">= 8" } }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3618,6 +3910,12 @@ "node": ">=8" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -3648,6 +3946,12 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3660,6 +3964,61 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", @@ -3679,9 +4038,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.616", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", - "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", + "version": "1.5.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", + "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==", "dev": true }, "node_modules/emittery": { @@ -3702,6 +4061,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3756,9 +4124,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" @@ -4107,6 +4475,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -4237,16 +4611,10 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true - }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4319,6 +4687,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -4366,6 +4747,15 @@ "resolved": "examples/gauge", "link": true }, + "node_modules/generic-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz", + "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==", + "dev": true, + "dependencies": { + "loader-utils": "^3.2.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4548,6 +4938,24 @@ "node": ">=10.17.0" } }, + "node_modules/icss-replace-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", + "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", + "dev": true + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -4557,13 +4965,25 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", "dev": true, "dependencies": { - "parent-module": "^1.0.0", + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { @@ -4573,6 +4993,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -6401,6 +6842,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -6549,6 +6999,15 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6564,6 +7023,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -6582,6 +7047,12 @@ "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "dev": true }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6633,6 +7104,12 @@ "tmpl": "1.0.5" } }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6649,12 +7126,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -6711,6 +7188,24 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6735,9 +7230,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/normalize-path": { @@ -6749,6 +7244,27 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -6761,6 +7277,18 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6770,6 +7298,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6811,6 +7348,15 @@ "node": ">= 0.8.0" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6841,6 +7387,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -6948,9 +7522,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -6965,6 +7539,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -7038,155 +7621,755 @@ "node": ">=8" } }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 14" + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" }, "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } + "postcss": "^8.2.2" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">= 0.8.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/prettier": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", - "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=14" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, "engines": { - "node": ">=6.0.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "engines": { + "node": "^10 || ^12 || >=14.0" }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", "dev": true, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" }, "engines": { - "node": ">= 6" + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" } }, - "node_modules/pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, { "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ] - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" + "url": "https://opencollective.com/postcss/" }, { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz", + "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==", + "dev": true, + "dependencies": { + "generic-names": "^4.0.0", + "icss-replace-symbols": "^1.1.0", + "lodash.camelcase": "^4.3.0", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "string-hash": "^1.1.1" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise.series": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz", + "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, { "type": "consulting", "url": "https://feross.org/support" @@ -7199,6 +8382,15 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7311,9 +8503,9 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -7394,6 +8586,177 @@ "rollup": "^2.x.x || ^3.x.x || ^4.x.x" } }, + "node_modules/rollup-plugin-postcss": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz", + "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "concat-with-sourcemaps": "^1.1.0", + "cssnano": "^5.0.1", + "import-cwd": "^3.0.0", + "p-queue": "^6.6.2", + "pify": "^5.0.0", + "postcss-load-config": "^3.0.0", + "postcss-modules": "^4.0.0", + "promise.series": "^0.2.0", + "resolve": "^1.19.0", + "rollup-pluginutils": "^2.8.2", + "safe-identifier": "^0.4.2", + "style-inject": "^0.3.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "8.x" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/rollup-plugin-postcss/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-postcss/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup-plugin-postcss/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7417,6 +8780,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", + "dev": true + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -7501,6 +8870,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -7517,6 +8895,13 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7538,6 +8923,12 @@ "node": ">=8" } }, + "node_modules/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==", + "dev": true + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7635,6 +9026,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-inject": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", + "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==", + "dev": true + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -7703,6 +9116,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", @@ -7719,6 +9162,58 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tailwindcss": { + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7996,12 +9491,12 @@ } }, "node_modules/tsup/node_modules/rollup": { - "version": "4.9.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", - "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -8011,19 +9506,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.6", - "@rollup/rollup-android-arm64": "4.9.6", - "@rollup/rollup-darwin-arm64": "4.9.6", - "@rollup/rollup-darwin-x64": "4.9.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", - "@rollup/rollup-linux-arm64-gnu": "4.9.6", - "@rollup/rollup-linux-arm64-musl": "4.9.6", - "@rollup/rollup-linux-riscv64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-gnu": "4.9.6", - "@rollup/rollup-linux-x64-musl": "4.9.6", - "@rollup/rollup-win32-arm64-msvc": "4.9.6", - "@rollup/rollup-win32-ia32-msvc": "4.9.6", - "@rollup/rollup-win32-x64-msvc": "4.9.6", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -8101,9 +9599,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -8120,8 +9618,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -8139,6 +9637,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/package.json b/package.json index 166002c2..5290f631 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "src/js/*" ], "scripts": { + "docker": ".\\scripts\\run_docker_cmd.bat", "format": "prettier --write .", "lint:js": "eslint \"src/js/**/*.ts\"", + "build:gauge": ".\\scripts\\build_gauge.bat", + "build:js": ".\\scripts\\build_js.bat", "build:wasm": ".\\scripts\\build.bat", "build:wasm-workflow": "./scripts/run_docker_cmd.sh ./scripts/build.sh", - "jest": "jest --verbose", + "jest": "jest --verbose --runInBand", "test": ".\\scripts\\test.bat", "test-workflow": "./scripts/test.sh" }, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d5ce77ec..86e1c453 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] profile = "default" channel = "1.79.0" +targets = ["wasm32-wasi"] diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index db20c017..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1,34 +0,0 @@ -# Use command `cargo +nightly fmt`, nightly formatter is required for some features - -binop_separator = "Front" -blank_lines_lower_bound = 0 -blank_lines_upper_bound = 1 -combine_control_expr = true -comment_width = 120 -condense_wildcard_suffixes = true -empty_item_single_line = true -fn_params_layout = "Compressed" -fn_single_line = true -force_explicit_abi = true -format_code_in_doc_comments = true -format_macro_matchers = true -format_macro_bodies = true -format_strings = true -imports_indent = "Block" -# hard_tabs = true -imports_granularity = "Crate" -imports_layout = "HorizontalVertical" -indent_style = "Block" -match_block_trailing_comma = true -max_width = 120 -merge_derives = true -newline_style = "Native" -normalize_comments = true -normalize_doc_attributes = true -reorder_impl_items = true -reorder_imports = true -group_imports = "StdExternalCrate" -trailing_semicolon = true -use_field_init_shorthand = true -use_try_shorthand = true -wrap_comments = true diff --git a/scripts/build_gauge.bat b/scripts/build_gauge.bat new file mode 100644 index 00000000..6a536ded --- /dev/null +++ b/scripts/build_gauge.bat @@ -0,0 +1,7 @@ +@echo off + +cd .\examples\gauge + +call npm run build + +cd %~dp0 \ No newline at end of file diff --git a/scripts/build_js.bat b/scripts/build_js.bat new file mode 100644 index 00000000..5c3c0414 --- /dev/null +++ b/scripts/build_js.bat @@ -0,0 +1,7 @@ +@echo off + +cd .\src\js + +call npm run build + +cd %~dp0 diff --git a/scripts/test.bat b/scripts/test.bat index 33229d18..e213bd23 100644 --- a/scripts/test.bat +++ b/scripts/test.bat @@ -4,5 +4,6 @@ cd %~dp0 rmdir /s /q ..\test_work mkdir ..\test_work +mkdir ..\test_work\navigraph-test call .\run_docker_cmd.bat npm run jest diff --git a/scripts/test.sh b/scripts/test.sh index d2e850c6..4003b9bc 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,6 +2,7 @@ rm -rf test_work mkdir test_work +mkdir test_work/navigraph-test -source "${BASH_SOURCE%/*}/run_docker_cmd.sh" npm ci -npm run jest \ No newline at end of file +bash ./scripts/run_docker_cmd.sh npm ci +bash ./scripts/run_docker_cmd.sh npm run jest \ No newline at end of file diff --git a/src/database/Cargo.toml b/src/database/Cargo.toml index ba291072..f400fa45 100644 --- a/src/database/Cargo.toml +++ b/src/database/Cargo.toml @@ -12,3 +12,4 @@ serde = { version = "1.0.190", features = ["derive"] } serde_json = "1.0.108" serde_with = "3.4.0" regex = "1.10.2" +enum_dispatch = "0.3.13" diff --git a/src/database/src/database.rs b/src/database/src/database.rs index fd29a511..a4270212 100644 --- a/src/database/src/database.rs +++ b/src/database/src/database.rs @@ -1,565 +1,570 @@ -use std::{ - error::Error, - fmt::{Display, Formatter}, -}; - -use rusqlite::{params, params_from_iter, types::ValueRef, Connection, OpenFlags, Result}; -use serde_json::{Number, Value}; - -use super::output::{airport::Airport, airway::map_airways, procedure::departure::map_departures}; -use crate::{ - math::{Coordinates, NauticalMiles}, - output::{ - airspace::{map_controlled_airspaces, map_restrictive_airspaces, ControlledAirspace, RestrictiveAirspace}, - airway::Airway, - communication::Communication, - database_info::DatabaseInfo, - gate::Gate, - gls_navaid::GlsNavaid, - ndb_navaid::NdbNavaid, - path_point::PathPoint, - procedure::{ - approach::{map_approaches, Approach}, - arrival::{map_arrivals, Arrival}, - departure::Departure, - }, - runway::RunwayThreshold, - vhf_navaid::VhfNavaid, - waypoint::Waypoint, - }, - sql_structs, util, -}; - -pub struct Database { - database: Option, - pub path: Option, -} - -#[derive(Debug)] -struct NoDatabaseOpen; - -impl Display for NoDatabaseOpen { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "No database open") - } -} - -impl Error for NoDatabaseOpen {} - -impl Database { - pub fn new() -> Self { - Database { - database: None, - path: None, - } - } - - fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> { - self.database.as_ref().ok_or(NoDatabaseOpen) - } - - pub fn set_active_database(&mut self, path: String) -> Result<(), Box> { - let path = match util::find_sqlite_file(&path) { - Ok(new_path) => new_path, - Err(_) => path, - }; - println!("[NAVIGRAPH] Setting active database to {}", path); - self.close_connection(); - if util::is_sqlite_file(&path)? { - self.open_connection(path.clone())?; - } - self.path = Some(path); - - Ok(()) - } - - pub fn open_connection(&mut self, path: String) -> Result<(), Box> { - // We have to open with flags because the SQLITE_OPEN_CREATE flag with the default open causes the file to - // be overwritten - let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX; - let conn = Connection::open_with_flags(path, flags)?; - self.database = Some(conn); - - Ok(()) - } - - pub fn execute_sql_query(&self, sql: String, params: Vec) -> Result> { - // Execute query - let conn = self.get_database()?; - let mut stmt = conn.prepare(&sql)?; - let names = stmt - .column_names() - .into_iter() - .map(|n| n.to_string()) - .collect::>(); - - // Collect data to be returned - let data_iter = stmt.query_map(params_from_iter(params), |row| { - let mut map = serde_json::Map::new(); - for (i, name) in names.iter().enumerate() { - let value = match row.get_ref(i)? { - ValueRef::Text(text) => Some(Value::String(String::from_utf8(text.into()).unwrap())), - ValueRef::Integer(int) => Some(Value::Number(Number::from(int))), - ValueRef::Real(real) => Some(Value::Number(Number::from_f64(real).unwrap())), - ValueRef::Null => None, - ValueRef::Blob(_) => panic!("Unexpected value type Blob"), - }; - - if let Some(value) = value { - map.insert(name.to_string(), value); - } - } - Ok(Value::Object(map)) - })?; - - let mut data = Vec::new(); - for row in data_iter { - data.push(row?); - } - - let json = Value::Array(data); - - Ok(json) - } - - pub fn get_database_info(&self) -> Result> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_header")?; - - let header_data = Database::fetch_row::(&mut stmt, params![])?; - - Ok(DatabaseInfo::from(header_data)) - } - - pub fn get_airport(&self, ident: String) -> Result> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_airports WHERE airport_identifier = (?1)")?; - - let airport_data = Database::fetch_row::(&mut stmt, params![ident])?; - - Ok(Airport::from(airport_data)) - } - - pub fn get_waypoints(&self, ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut enroute_stmt = conn.prepare("SELECT * FROM tbl_enroute_waypoints WHERE waypoint_identifier = (?1)")?; - let mut terminal_stmt = - conn.prepare("SELECT * FROM tbl_terminal_waypoints WHERE waypoint_identifier = (?1)")?; - - let enroute_data = Database::fetch_rows::(&mut enroute_stmt, params![ident])?; - let terminal_data = Database::fetch_rows::(&mut terminal_stmt, params![ident])?; - - Ok(enroute_data - .into_iter() - .chain(terminal_data.into_iter()) - .map(Waypoint::from) - .collect()) - } - - pub fn get_vhf_navaids(&self, ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_vhfnavaids WHERE vor_identifier = (?1)")?; - - let navaids_data = Database::fetch_rows::(&mut stmt, params![ident])?; - - Ok(navaids_data.into_iter().map(VhfNavaid::from).collect()) - } - - pub fn get_ndb_navaids(&self, ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut enroute_stmt = conn.prepare("SELECT * FROM tbl_enroute_ndbnavaids WHERE ndb_identifier = (?1)")?; - let mut terminal_stmt = conn.prepare("SELECT * FROM tbl_terminal_ndbnavaids WHERE ndb_identifier = (?1)")?; - - let enroute_data = Database::fetch_rows::(&mut enroute_stmt, params![ident])?; - let terminal_data = Database::fetch_rows::(&mut terminal_stmt, params![ident])?; - - Ok(enroute_data - .into_iter() - .chain(terminal_data.into_iter()) - .map(NdbNavaid::from) - .collect()) - } - - pub fn get_airways(&self, ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_enroute_airways WHERE route_identifier = (?1)")?; - - let airways_data = Database::fetch_rows::(&mut stmt, params![ident])?; - - Ok(map_airways(airways_data)) - } - - pub fn get_airways_at_fix(&self, fix_ident: String, fix_icao_code: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt: rusqlite::Statement<'_> = conn.prepare( - "SELECT * FROM tbl_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ - tbl_enroute_airways WHERE waypoint_identifier = (?1) AND icao_code = (?2))", - )?; - let all_airways = - Database::fetch_rows::(&mut stmt, params![fix_ident, fix_icao_code])?; - - Ok(map_airways(all_airways) - .into_iter() - .filter(|airway| { - airway - .fixes - .iter() - .any(|fix| fix.ident == fix_ident && fix.icao_code == fix_icao_code) - }) - .collect()) - } - - pub fn get_airports_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, "airport_ref"); - - let mut stmt = conn.prepare(format!("SELECT * FROM tbl_airports WHERE {where_string}").as_str())?; - - let airports_data = Database::fetch_rows::(&mut stmt, [])?; - - // Filter into a circle of range - Ok(airports_data - .into_iter() - .map(Airport::from) - .filter(|airport| airport.location.distance_to(¢er) <= range) - .collect()) - } - - pub fn get_waypoints_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, "waypoint"); - - let mut enroute_stmt = - conn.prepare(format!("SELECT * FROM tbl_enroute_waypoints WHERE {where_string}").as_str())?; - let mut terminal_stmt = - conn.prepare(format!("SELECT * FROM tbl_terminal_waypoints WHERE {where_string}").as_str())?; - - let enroute_data = Database::fetch_rows::(&mut enroute_stmt, [])?; - let terminal_data = Database::fetch_rows::(&mut terminal_stmt, [])?; - - // Filter into a circle of range - Ok(enroute_data - .into_iter() - .chain(terminal_data.into_iter()) - .map(Waypoint::from) - .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) - .collect()) - } - - pub fn get_ndb_navaids_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, "ndb"); - - let mut enroute_stmt = - conn.prepare(format!("SELECT * FROM tbl_enroute_ndbnavaids WHERE {where_string}").as_str())?; - let mut terminal_stmt = - conn.prepare(format!("SELECT * FROM tbl_terminal_ndbnavaids WHERE {where_string}").as_str())?; - - let enroute_data = Database::fetch_rows::(&mut enroute_stmt, [])?; - let terminal_data = Database::fetch_rows::(&mut terminal_stmt, [])?; - - // Filter into a circle of range - Ok(enroute_data - .into_iter() - .chain(terminal_data.into_iter()) - .map(NdbNavaid::from) - .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) - .collect()) - } - - pub fn get_vhf_navaids_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, "vor"); - - let mut stmt = conn.prepare(format!("SELECT * FROM tbl_vhfnavaids WHERE {where_string}").as_str())?; - - let navaids_data = Database::fetch_rows::(&mut stmt, [])?; - - // Filter into a circle of range - Ok(navaids_data - .into_iter() - .map(VhfNavaid::from) - .filter(|navaid| navaid.location.distance_to(¢er) <= range) - .collect()) - } - - pub fn get_airways_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, "waypoint"); - - let mut stmt = conn.prepare( - format!( - "SELECT * FROM tbl_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ - tbl_enroute_airways WHERE {where_string})" - ) - .as_str(), - )?; - - let airways_data = Database::fetch_rows::(&mut stmt, [])?; - - Ok(map_airways(airways_data) - .into_iter() - .filter(|airway| { - airway - .fixes - .iter() - .any(|fix| fix.location.distance_to(¢er) <= range) - }) - .collect()) - } - - pub fn get_controlled_airspaces_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, ""); - let arc_where_string = Self::range_query_where(center, range, "arc_origin"); - - let range_query = format!( - "SELECT airspace_center, multiple_code FROM tbl_controlled_airspace WHERE {where_string} OR \ - {arc_where_string}" - ); - - let mut stmt = conn.prepare( - format!("SELECT * FROM tbl_controlled_airspace WHERE (airspace_center, multiple_code) IN ({range_query})") - .as_str(), - )?; - - let airspaces_data = Database::fetch_rows::(&mut stmt, [])?; - - Ok(map_controlled_airspaces(airspaces_data)) - } - - pub fn get_restrictive_airspaces_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, ""); - let arc_where_string = Self::range_query_where(center, range, "arc_origin"); - - let range_query: String = format!( - "SELECT restrictive_airspace_designation, icao_code FROM tbl_restrictive_airspace WHERE {where_string} OR \ - {arc_where_string}" - ); - - let mut stmt = conn.prepare( - format!( - "SELECT * FROM tbl_restrictive_airspace WHERE (restrictive_airspace_designation, icao_code) IN \ - ({range_query})" - ) - .as_str(), - )?; - - let airspaces_data = Database::fetch_rows::(&mut stmt, [])?; - - Ok(map_restrictive_airspaces(airspaces_data)) - } - - pub fn get_communications_in_range( - &self, center: Coordinates, range: NauticalMiles, - ) -> Result, Box> { - let conn = self.get_database()?; - - let where_string = Self::range_query_where(center, range, ""); - - let mut enroute_stmt = - conn.prepare(format!("SELECT * FROM tbl_enroute_communication WHERE {where_string}").as_str())?; - - let mut terminal_stmt = - conn.prepare(format!("SELECT * FROM tbl_airport_communication WHERE {where_string}").as_str())?; - - let enroute_data = Database::fetch_rows::(&mut enroute_stmt, [])?; - let terminal_data = Database::fetch_rows::(&mut terminal_stmt, [])?; - - Ok(enroute_data - .into_iter() - .map(Communication::from) - .chain(terminal_data.into_iter().map(Communication::from)) - .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) - .collect()) - } - - pub fn get_runways_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; - - let runways_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(runways_data.into_iter().map(Into::into).collect()) - } - - pub fn get_departures_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut departures_stmt = conn.prepare("SELECT * FROM tbl_sids WHERE airport_identifier = (?1)")?; - - let mut runways_stmt = conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; - - let departures_data = - Database::fetch_rows::(&mut departures_stmt, params![airport_ident])?; - let runways_data = Database::fetch_rows::(&mut runways_stmt, params![airport_ident])?; - - Ok(map_departures(departures_data, runways_data)) - } - - pub fn get_arrivals_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut arrivals_stmt = conn.prepare("SELECT * FROM tbl_stars WHERE airport_identifier = (?1)")?; - - let mut runways_stmt = conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; - - let arrivals_data = - Database::fetch_rows::(&mut arrivals_stmt, params![airport_ident])?; - let runways_data = Database::fetch_rows::(&mut runways_stmt, params![airport_ident])?; - - Ok(map_arrivals(arrivals_data, runways_data)) - } - - pub fn get_approaches_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut approachs_stmt = conn.prepare("SELECT * FROM tbl_iaps WHERE airport_identifier = (?1)")?; - - let approaches_data = - Database::fetch_rows::(&mut approachs_stmt, params![airport_ident])?; - - Ok(map_approaches(approaches_data)) - } - - pub fn get_waypoints_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_terminal_waypoints WHERE region_code = (?1)")?; - - let waypoints_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(waypoints_data.into_iter().map(Waypoint::from).collect()) - } - - pub fn get_ndb_navaids_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_terminal_ndbnavaids WHERE airport_identifier = (?1)")?; - - let waypoints_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(waypoints_data.into_iter().map(NdbNavaid::from).collect()) - } - - pub fn get_gates_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_gate WHERE airport_identifier = (?1)")?; - - let gates_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(gates_data.into_iter().map(Gate::from).collect()) - } - - pub fn get_communications_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_airport_communication WHERE airport_identifier = (?1)")?; - - let gates_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(gates_data.into_iter().map(Communication::from).collect()) - } - - pub fn get_gls_navaids_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_gls WHERE airport_identifier = (?1)")?; - - let gates_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(gates_data.into_iter().map(GlsNavaid::from).collect()) - } - - pub fn get_path_points_at_airport(&self, airport_ident: String) -> Result, Box> { - let conn = self.get_database()?; - - let mut stmt = conn.prepare("SELECT * FROM tbl_pathpoints WHERE airport_identifier = (?1)")?; - - let gates_data = Database::fetch_rows::(&mut stmt, params![airport_ident])?; - - Ok(gates_data.into_iter().map(PathPoint::from).collect()) - } - - fn range_query_where(center: Coordinates, range: NauticalMiles, prefix: &str) -> String { - let (bottom_left, top_right) = center.distance_bounds(range); - - let prefix = if prefix.is_empty() { - String::new() - } else { - format!("{prefix}_") - }; - - if bottom_left.long > top_right.long { - format!( - "{prefix}latitude BETWEEN {} AND {} AND ({prefix}longitude >= {} OR {prefix}longitude <= {})", - bottom_left.lat, top_right.lat, bottom_left.long, top_right.long - ) - } else if bottom_left.lat.max(top_right.lat) > 80.0 { - format!("{prefix}latitude >= {}", bottom_left.lat.min(top_right.lat)) - } else if bottom_left.lat.min(top_right.lat) < -80.0 { - format!("{prefix}latitude <= {}", bottom_left.lat.max(top_right.lat)) - } else { - format!( - "{prefix}latitude BETWEEN {} AND {} AND {prefix}longitude BETWEEN {} AND {}", - bottom_left.lat, top_right.lat, bottom_left.long, top_right.long - ) - } - } - - fn fetch_row(stmt: &mut rusqlite::Statement, params: impl rusqlite::Params) -> Result> - where - T: for<'r> serde::Deserialize<'r>, - { - let mut rows = stmt.query_and_then(params, |r| serde_rusqlite::from_row::(r))?; - let row = rows.next().ok_or("No row found")??; - Ok(row) - } - - fn fetch_rows(stmt: &mut rusqlite::Statement, params: impl rusqlite::Params) -> Result, Box> - where - T: for<'r> serde::Deserialize<'r>, - { - let mut rows = stmt.query_and_then(params, |r| serde_rusqlite::from_row::(r))?; - let mut data = Vec::new(); - while let Some(row) = rows.next() { - data.push(row.map_err(|e| e.to_string())?); - } - Ok(data) - } - - pub fn close_connection(&mut self) { - self.database = None; - } -} +use std::{error::Error, path::Path}; + +use rusqlite::{params, Connection, OpenFlags, Result}; + +use crate::{ + enums::InterfaceFormat, + math::{Coordinates, NauticalMiles}, + output::{ + airport::Airport, + airspace::{ + map_controlled_airspaces, map_restrictive_airspaces, ControlledAirspace, + RestrictiveAirspace, + }, + airway::{map_airways, Airway}, + communication::Communication, + database_info::DatabaseInfo, + gate::Gate, + gls_navaid::GlsNavaid, + ndb_navaid::NdbNavaid, + path_point::PathPoint, + procedure::{ + approach::{map_approaches, Approach}, + arrival::{map_arrivals, Arrival}, + departure::{map_departures, Departure}, + }, + runway::RunwayThreshold, + vhf_navaid::VhfNavaid, + waypoint::Waypoint, + }, + sql_structs, + traits::{NoDatabaseOpen, *}, + util, +}; + +#[derive(Default)] +pub struct DatabaseV1 { + connection: Option, + pub path: Option, +} + +impl DatabaseTrait for DatabaseV1 { + fn get_database_type(&self) -> InterfaceFormat { + InterfaceFormat::DFDv1 + } + + fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> { + self.connection.as_ref().ok_or(NoDatabaseOpen) + } + + fn setup(&self) -> Result> { + // Nothing goes here preferrably + Ok(String::from("Setup Complete")) + } + + fn enable_cycle(&mut self, package: &PackageInfo) -> Result> { + let db_path = match package.cycle.database_path { + Some(ref path) => Path::new("").join(&package.path).join(path), + None => Path::new("") + .join(&package.path) + .join(format!("e_dfd_{}.s3db", package.cycle.cycle)), + }; + + println!("[NAVIGRAPH]: Setting active database to {:?}", db_path); + + if self.connection.is_some() { + self.disable_cycle()?; + } + + let flags = OpenFlags::SQLITE_OPEN_READ_ONLY + | OpenFlags::SQLITE_OPEN_URI + | OpenFlags::SQLITE_OPEN_NO_MUTEX; + let conn = Connection::open_with_flags(db_path.clone(), flags)?; + + self.connection = Some(conn); + self.path = Some(String::from(db_path.to_string_lossy())); + + println!("[NAVIGRAPH]: Set active database to {:?}", db_path); + + Ok(true) + } + + fn disable_cycle(&mut self) -> Result> { + println!("[NAVIGRAPH]: Disabling active database"); + self.connection = None; + Ok(true) + } + + fn get_database_info(&self) -> Result> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_header")?; + + let header_data = util::fetch_row::(&mut stmt, params![])?; + + Ok(DatabaseInfo::from(header_data)) + } + + fn get_airport(&self, ident: String) -> Result> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_airports WHERE airport_identifier = (?1)")?; + + let airport_data = util::fetch_row::(&mut stmt, params![ident])?; + + Ok(Airport::from(airport_data)) + } + + fn get_waypoints(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut enroute_stmt = + conn.prepare("SELECT * FROM tbl_enroute_waypoints WHERE waypoint_identifier = (?1)")?; + let mut terminal_stmt = + conn.prepare("SELECT * FROM tbl_terminal_waypoints WHERE waypoint_identifier = (?1)")?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, params![ident])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, params![ident])?; + + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(Waypoint::from) + .collect()) + } + + fn get_vhf_navaids(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_vhfnavaids WHERE vor_identifier = (?1)")?; + + let navaids_data = util::fetch_rows::(&mut stmt, params![ident])?; + + Ok(navaids_data.into_iter().map(VhfNavaid::from).collect()) + } + + fn get_ndb_navaids(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut enroute_stmt = + conn.prepare("SELECT * FROM tbl_enroute_ndbnavaids WHERE ndb_identifier = (?1)")?; + let mut terminal_stmt = + conn.prepare("SELECT * FROM tbl_terminal_ndbnavaids WHERE ndb_identifier = (?1)")?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, params![ident])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, params![ident])?; + + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(NdbNavaid::from) + .collect()) + } + + fn get_airways(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_enroute_airways WHERE route_identifier = (?1)")?; + + let airways_data = + util::fetch_rows::(&mut stmt, params![ident])?; + + Ok(map_airways(airways_data)) + } + + fn get_airways_at_fix( + &self, + fix_ident: String, + fix_icao_code: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt: rusqlite::Statement<'_> = conn.prepare( + "SELECT * FROM tbl_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ + tbl_enroute_airways WHERE waypoint_identifier = (?1) AND icao_code = (?2))", + )?; + let all_airways = util::fetch_rows::( + &mut stmt, + params![fix_ident, fix_icao_code], + )?; + + Ok(map_airways(all_airways) + .into_iter() + .filter(|airway| { + airway + .fixes + .iter() + .any(|fix| fix.ident == fix_ident && fix.icao_code == fix_icao_code) + }) + .collect()) + } + + fn get_airports_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "airport_ref"); + + let mut stmt = + conn.prepare(format!("SELECT * FROM tbl_airports WHERE {where_string}").as_str())?; + + let airports_data = util::fetch_rows::(&mut stmt, [])?; + + // Filter into a circle of range + Ok(airports_data + .into_iter() + .map(Airport::from) + .filter(|airport| airport.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_waypoints_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "waypoint"); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_enroute_waypoints WHERE {where_string}").as_str(), + )?; + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_terminal_waypoints WHERE {where_string}").as_str(), + )?; + + let enroute_data = util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = util::fetch_rows::(&mut terminal_stmt, [])?; + + // Filter into a circle of range + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(Waypoint::from) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_ndb_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "ndb"); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_enroute_ndbnavaids WHERE {where_string}").as_str(), + )?; + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_terminal_ndbnavaids WHERE {where_string}").as_str(), + )?; + + let enroute_data = util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = util::fetch_rows::(&mut terminal_stmt, [])?; + + // Filter into a circle of range + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(NdbNavaid::from) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_vhf_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "vor"); + + let mut stmt = + conn.prepare(format!("SELECT * FROM tbl_vhfnavaids WHERE {where_string}").as_str())?; + + let navaids_data = util::fetch_rows::(&mut stmt, [])?; + + // Filter into a circle of range + Ok(navaids_data + .into_iter() + .map(VhfNavaid::from) + .filter(|navaid| navaid.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_airways_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "waypoint"); + + let mut stmt = conn.prepare( + format!( + "SELECT * FROM tbl_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ + tbl_enroute_airways WHERE {where_string})" + ) + .as_str(), + )?; + + let airways_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_airways(airways_data) + .into_iter() + .filter(|airway| { + airway + .fixes + .iter() + .any(|fix| fix.location.distance_to(¢er) <= range) + }) + .collect()) + } + + fn get_controlled_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + let arc_where_string = util::range_query_where(center, range, "arc_origin"); + + let range_query = format!( + "SELECT airspace_center, multiple_code FROM tbl_controlled_airspace WHERE {where_string} OR \ + {arc_where_string}" + ); + + let mut stmt = conn.prepare( + format!("SELECT * FROM tbl_controlled_airspace WHERE (airspace_center, multiple_code) IN ({range_query})") + .as_str(), + )?; + + let airspaces_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_controlled_airspaces(airspaces_data)) + } + + fn get_restrictive_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + let arc_where_string = util::range_query_where(center, range, "arc_origin"); + + let range_query: String = format!( + "SELECT restrictive_airspace_designation, icao_code FROM tbl_restrictive_airspace WHERE {where_string} OR \ + {arc_where_string}" + ); + + let mut stmt = conn.prepare( + format!( + "SELECT * FROM tbl_restrictive_airspace WHERE (restrictive_airspace_designation, icao_code) IN \ + ({range_query})" + ) + .as_str(), + )?; + + let airspaces_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_restrictive_airspaces(airspaces_data)) + } + + fn get_communications_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_enroute_communication WHERE {where_string}").as_str(), + )?; + + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_airport_communication WHERE {where_string}").as_str(), + )?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, [])?; + + Ok(enroute_data + .into_iter() + .map(Communication::from) + .chain(terminal_data.into_iter().map(Communication::from)) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_runways_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; + + let runways_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(runways_data.into_iter().map(Into::into).collect()) + } + + fn get_departures_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut departures_stmt = + conn.prepare("SELECT * FROM tbl_sids WHERE airport_identifier = (?1)")?; + + let mut runways_stmt = + conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; + + let departures_data = util::fetch_rows::( + &mut departures_stmt, + params![airport_ident], + )?; + let runways_data = + util::fetch_rows::(&mut runways_stmt, params![airport_ident])?; + + Ok(map_departures(departures_data, runways_data)) + } + + fn get_arrivals_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut arrivals_stmt = + conn.prepare("SELECT * FROM tbl_stars WHERE airport_identifier = (?1)")?; + + let mut runways_stmt = + conn.prepare("SELECT * FROM tbl_runways WHERE airport_identifier = (?1)")?; + + let arrivals_data = util::fetch_rows::( + &mut arrivals_stmt, + params![airport_ident], + )?; + let runways_data = + util::fetch_rows::(&mut runways_stmt, params![airport_ident])?; + + Ok(map_arrivals(arrivals_data, runways_data)) + } + + fn get_approaches_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut approachs_stmt = + conn.prepare("SELECT * FROM tbl_iaps WHERE airport_identifier = (?1)")?; + + let approaches_data = util::fetch_rows::( + &mut approachs_stmt, + params![airport_ident], + )?; + + Ok(map_approaches(approaches_data)) + } + + fn get_waypoints_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_terminal_waypoints WHERE region_code = (?1)")?; + + let waypoints_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(waypoints_data.into_iter().map(Waypoint::from).collect()) + } + + fn get_ndb_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_terminal_ndbnavaids WHERE airport_identifier = (?1)")?; + + let waypoints_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(waypoints_data.into_iter().map(NdbNavaid::from).collect()) + } + + fn get_gates_at_airport(&self, airport_ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_gate WHERE airport_identifier = (?1)")?; + + let gates_data = util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(Gate::from).collect()) + } + + fn get_communications_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn + .prepare("SELECT * FROM tbl_airport_communication WHERE airport_identifier = (?1)")?; + + let gates_data = util::fetch_rows::( + &mut stmt, + params![airport_ident], + )?; + + Ok(gates_data.into_iter().map(Communication::from).collect()) + } + + fn get_gls_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_gls WHERE airport_identifier = (?1)")?; + + let gates_data = util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(GlsNavaid::from).collect()) + } + + fn get_path_points_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pathpoints WHERE airport_identifier = (?1)")?; + + let gates_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(PathPoint::from).collect()) + } +} diff --git a/src/database/src/enums.rs b/src/database/src/enums.rs index 65dcbc46..aebe0822 100644 --- a/src/database/src/enums.rs +++ b/src/database/src/enums.rs @@ -1,9 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub enum IfrCapability { #[serde(rename = "Y")] Yes, + // Never used, for linting + #[default] #[serde(rename = "N")] No, } @@ -217,7 +219,7 @@ pub enum RestrictiveAirspaceType { Unknown, } -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)] pub enum CommunicationType { #[serde(rename = "ACC")] AreaControlCenter, @@ -301,13 +303,15 @@ pub enum CommunicationType { Tower, #[serde(rename = "UAC")] UpperAreaControl, + // Never used, for linting + #[default] #[serde(rename = "UNI")] Unicom, #[serde(rename = "VOL")] Volmet, } -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)] pub enum FrequencyUnits { #[serde(rename = "H")] High, @@ -315,15 +319,80 @@ pub enum FrequencyUnits { VeryHigh, #[serde(rename = "U")] UltraHigh, + // Never used, for linting + #[default] #[serde(rename = "C")] /// Communication channel for 8.33 kHz spacing CommChannel, } -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)] pub enum ApproachTypeIdentifier { #[serde(rename = "LPV")] LocalizerPerformanceVerticalGuidance, + // Never used, for linting + #[default] #[serde(rename = "LP")] LocalizerPerformance, } + +#[derive(Debug)] +pub enum InterfaceFormat { + DFDv1, + DFDv2, + Custom, +} + +impl InterfaceFormat { + pub fn as_str(&self) -> &'static str { + match self { + Self::DFDv1 => "dfd", + Self::DFDv2 => "dfdv2", + Self::Custom => "custom", + } + } +} + +impl From<&String> for InterfaceFormat { + fn from(value: &String) -> Self { + match value.as_str() { + "dfd" => Self::DFDv1, + "dfdv2" => Self::DFDv2, + _ => Self::Custom, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum TrafficPattern { + #[serde(rename = "L")] + Left, + #[serde(rename = "R")] + Right, +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum RunwayLights { + #[serde(rename = "Y")] + Yes, + #[serde(rename = "N")] + No, +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub enum RunwaySurface { + #[serde(rename = "ASPH")] + Asphalt, + #[serde(rename = "TURF")] + Turf, + #[serde(rename = "GRVL")] + Gravel, + #[serde(rename = "CONC")] + Concrete, + #[serde(rename = "WATE")] + Water, + #[serde(rename = "BITU")] + Bitumen, + #[serde(rename = "UNPV")] + Unpaved, +} diff --git a/src/database/src/lib.rs b/src/database/src/lib.rs index 9accadfd..5d95ea21 100644 --- a/src/database/src/lib.rs +++ b/src/database/src/lib.rs @@ -1,6 +1,9 @@ pub mod database; pub mod enums; +pub mod manual; pub mod math; pub mod output; mod sql_structs; +pub mod traits; pub mod util; +pub mod v2; diff --git a/src/database/src/manual/database.rs b/src/database/src/manual/database.rs new file mode 100644 index 00000000..465322aa --- /dev/null +++ b/src/database/src/manual/database.rs @@ -0,0 +1,62 @@ +use std::{error::Error, fs, path::Path}; + +use rusqlite::{Connection, Result}; + +use crate::{ + enums::InterfaceFormat, + output::database_info::DatabaseInfo, + traits::{DatabaseTrait, InstalledNavigationDataCycleInfo, NoDatabaseOpen, PackageInfo}, +}; + +/// Used for manual connections, only handles setting packages as active +#[derive(Default)] +pub struct DatabaseManual { + path: String, +} + +impl DatabaseTrait for DatabaseManual { + fn get_database_type(&self) -> InterfaceFormat { + InterfaceFormat::Custom + } + + fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> { + Err(NoDatabaseOpen) + } + + fn setup(&self) -> Result> { + // Nothing goes here preferrably + Ok(String::from("Setup Complete")) + } + + fn enable_cycle(&mut self, package: &PackageInfo) -> Result> { + println!("[NAVIGRAPH]: Set active database to {:?}", &package.path); + + self.path.clone_from(&package.path); + + Ok(true) + } + + fn disable_cycle(&mut self) -> Result> { + println!("[NAVIGRAPH]: Disabling active database"); + Ok(true) + } + + fn get_database_info(&self) -> Result> { + let cycle_path = Path::new(&self.path).join("cycle.json"); + + let cycle: InstalledNavigationDataCycleInfo = + serde_json::from_reader(fs::File::open(cycle_path).unwrap()).unwrap(); + + let mut validity = cycle.validity_period.split('/').map(|f| f.to_string()); + + let header_data = DatabaseInfo::new( + cycle.cycle, + validity.nth(0).unwrap_or_default(), + validity.next().unwrap_or_default(), + None, + None, + ); + + Ok(header_data) + } +} diff --git a/src/database/src/manual/mod.rs b/src/database/src/manual/mod.rs new file mode 100644 index 00000000..8fd0a6be --- /dev/null +++ b/src/database/src/manual/mod.rs @@ -0,0 +1 @@ +pub mod database; diff --git a/src/database/src/math.rs b/src/database/src/math.rs index 24a98847..218c80f7 100644 --- a/src/database/src/math.rs +++ b/src/database/src/math.rs @@ -1,86 +1,86 @@ -use serde::{Deserialize, Serialize}; - -pub type NauticalMiles = f64; -pub type Degrees = f64; -pub type Radians = f64; -pub type Feet = f64; -pub type Meters = f64; -pub type Knots = f64; -pub type Minutes = f64; -pub type KiloHertz = f64; -pub type MegaHertz = f64; - -pub(crate) fn feet_to_meters(metres: Meters) -> Feet { - metres / 3.28084 -} - -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] -pub struct Coordinates { - pub lat: Degrees, - pub long: Degrees, -} - -const EARTH_RADIUS: NauticalMiles = 3443.92; -const MIN_LAT: Degrees = -90.0; -const MAX_LAT: Degrees = 90.0; -const MIN_LONG: Degrees = -180.0; -const MAX_LONG: Degrees = 180.0; - -impl Coordinates { - /// Returns the Southwest and Northeast corner of a box around coordinates with a minimum `distance` - pub fn distance_bounds(&self, distance: NauticalMiles) -> (Coordinates, Coordinates) { - let radial_distance: Radians = distance / EARTH_RADIUS; - - let mut low_lat = self.lat - radial_distance.to_degrees(); - let mut high_lat = self.lat + radial_distance.to_degrees(); - - let mut low_long; - let mut high_long; - - if low_lat > MIN_LAT && high_lat < MAX_LAT { - let delta_long = (radial_distance.sin() / self.lat.to_radians().cos()) - .asin() - .to_degrees(); - low_long = self.long - delta_long; - - if low_long < MIN_LONG { - low_long += 360.0; - } - - high_long = self.long + delta_long; - - if high_long > MAX_LONG { - high_long -= 360.0; - } - } else { - low_lat = low_lat.max(MIN_LAT); - high_lat = high_lat.max(MAX_LAT); - - low_long = MIN_LONG; - high_long = MIN_LONG; - } - - ( - Coordinates { - lat: low_lat, - long: low_long, - }, - Coordinates { - lat: high_lat, - long: high_long, - }, - ) - } - - pub fn distance_to(&self, other: &Coordinates) -> NauticalMiles { - let delta_lat: Radians = (other.lat - self.lat).to_radians(); - let delta_long: Degrees = (other.long - self.long).to_radians(); - - let a = - (delta_lat / 2.0).sin().powi(2) + self.lat.to_radians().cos().powi(2) * (delta_long / 2.0).sin().powi(2); - - let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt()); - - EARTH_RADIUS * c - } -} +use serde::{Deserialize, Serialize}; + +pub type NauticalMiles = f64; +pub type Degrees = f64; +pub type Radians = f64; +pub type Feet = f64; +pub type Meters = f64; +pub type Knots = f64; +pub type Minutes = f64; +pub type KiloHertz = f64; +pub type MegaHertz = f64; + +pub(crate) fn feet_to_meters(metres: Meters) -> Feet { + metres / 3.28084 +} + +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Default)] +pub struct Coordinates { + pub lat: Degrees, + pub long: Degrees, +} + +const EARTH_RADIUS: NauticalMiles = 3443.92; +const MIN_LAT: Degrees = -90.0; +const MAX_LAT: Degrees = 90.0; +const MIN_LONG: Degrees = -180.0; +const MAX_LONG: Degrees = 180.0; + +impl Coordinates { + /// Returns the Southwest and Northeast corner of a box around coordinates with a minimum `distance` + pub fn distance_bounds(&self, distance: NauticalMiles) -> (Coordinates, Coordinates) { + let radial_distance: Radians = distance / EARTH_RADIUS; + + let mut low_lat = self.lat - radial_distance.to_degrees(); + let mut high_lat = self.lat + radial_distance.to_degrees(); + + let mut low_long; + let mut high_long; + + if low_lat > MIN_LAT && high_lat < MAX_LAT { + let delta_long = (radial_distance.sin() / self.lat.to_radians().cos()) + .asin() + .to_degrees(); + low_long = self.long - delta_long; + + if low_long < MIN_LONG { + low_long += 360.0; + } + + high_long = self.long + delta_long; + + if high_long > MAX_LONG { + high_long -= 360.0; + } + } else { + low_lat = low_lat.max(MIN_LAT); + high_lat = high_lat.max(MAX_LAT); + + low_long = MIN_LONG; + high_long = MIN_LONG; + } + + ( + Coordinates { + lat: low_lat, + long: low_long, + }, + Coordinates { + lat: high_lat, + long: high_long, + }, + ) + } + + pub fn distance_to(&self, other: &Coordinates) -> NauticalMiles { + let delta_lat: Radians = (other.lat - self.lat).to_radians(); + let delta_long: Degrees = (other.long - self.long).to_radians(); + + let a = (delta_lat / 2.0).sin().powi(2) + + self.lat.to_radians().cos().powi(2) * (delta_long / 2.0).sin().powi(2); + + let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt()); + + EARTH_RADIUS * c + } +} diff --git a/src/database/src/output/airport.rs b/src/database/src/output/airport.rs index 5c49c207..b3222879 100644 --- a/src/database/src/output/airport.rs +++ b/src/database/src/output/airport.rs @@ -1,70 +1,114 @@ -use serde::Serialize; - -use crate::{ - enums::{IfrCapability, RunwaySurfaceCode}, - math::{Coordinates, Feet}, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize)] -pub struct Airport { - /// The unique identifier of the airport, such as `KLAX` or `EGLL` - pub ident: String, - /// Represents the geographic region of the world where this airport is located. - pub area_code: String, - /// Represents the icao prefix of the region that this airport is in. - /// - /// For most airports, this will be the same as the first two letters of the `ident`, such as `EG` for `EGLL`, or - /// `LF` for `LFPG`. - /// - /// The notable exceptions to this are airports in the US, Canada, and Australia. - pub icao_code: String, - /// The geographic location of the airport's reference point - pub location: Coordinates, - /// The formal name of the airport such as `KENNEDY INTL` for `KJFK` or `HEATHROW` for `EGLL` - pub name: String, - pub ifr_capability: IfrCapability, - /// The surface type of the longest runway at this airport. - pub longest_runway_surface_code: Option, - /// The elevation in feet of the airport's reference point - pub elevation: Feet, - /// The altitude in feet where aircraft transition from `QNH/QFE` to `STD` barometer settings - /// - /// This field will usually be smaller than `transition_level` to define the lower bound of the transition band - pub transition_altitude: Option, - /// The flight level in feet where aircraft transition from `QNH/QFE` to `STD` barometer settings - /// - /// This field will usually be larger than `transition_altitude` to define the upper bound of the transition band - pub transition_level: Option, - /// The speed limit in knots that aircraft should not exceed while they are below `speed_limit_altitude` around - /// this airport - pub speed_limit: Option, - /// The altitude in feet that aircraft below which must stay below the `speed_limit` of this airport while nearby. - pub speed_limit_altitude: Option, - /// The IATA identifier of this airport, such as `LHR` for `EGLL` or `JFK` for `KJFK` - pub iata_ident: Option, -} - -impl From for Airport { - fn from(airport: sql_structs::Airports) -> Self { - Self { - ident: airport.airport_identifier, - area_code: airport.area_code, - icao_code: airport.icao_code, - location: Coordinates { - lat: airport.airport_ref_latitude, - long: airport.airport_ref_longitude, - }, - name: airport.airport_name, - ifr_capability: airport.ifr_capability, - longest_runway_surface_code: airport.longest_runway_surface_code, - elevation: airport.elevation, - transition_altitude: airport.transition_altitude, - transition_level: airport.transition_level, - speed_limit: airport.speed_limit, - speed_limit_altitude: airport.speed_limit_altitude, - iata_ident: airport.iata_ata_designator, - } - } -} +use serde::Serialize; + +use crate::{ + enums::{IfrCapability, RunwaySurfaceCode}, + math::{Coordinates, Degrees, Feet}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct Airport { + /// The unique identifier of the airport, such as `KLAX` or `EGLL` + pub ident: String, + /// Represents the geographic region of the world where this airport is located. + pub area_code: String, + /// Represents the icao prefix of the region that this airport is in. + /// + /// For most airports, this will be the same as the first two letters of the `ident`, such as `EG` for `EGLL`, or + /// `LF` for `LFPG`. + /// Airport type (see Appendix 3.38) (v2 only) + /// The notable exceptions to this are airports in the US, Canada, and Australia. + pub icao_code: String, + pub airport_type: Option, + /// The geographic location of the airport's reference point + pub location: Coordinates, + /// The airport's general area (v2 only) + pub city: Option, + pub continent: Option, + pub country: Option, + pub country_3letter: Option, + pub state: Option, + pub state_2letter: Option, + /// The formal name of the airport such as `KENNEDY INTL` for `KJFK` or `HEATHROW` for `EGLL` + pub name: String, + pub ifr_capability: IfrCapability, + /// The surface type of the longest runway at this airport. + pub longest_runway_surface_code: Option, + /// The elevation in feet of the airport's reference point + pub elevation: Feet, + /// Magnetic north in Degrees (v2 only) + pub magnetic_variation: Option, + /// The altitude in feet where aircraft transition from `QNH/QFE` to `STD` barometer settings + /// + /// This field will usually be smaller than `transition_level` to define the lower bound of the transition band + pub transition_altitude: Option, + /// The flight level in feet where aircraft transition from `QNH/QFE` to `STD` barometer settings + /// + /// This field will usually be larger than `transition_altitude` to define the upper bound of the transition band + pub transition_level: Option, + /// The speed limit in knots that aircraft should not exceed while they are below `speed_limit_altitude` around + /// this airport + pub speed_limit: Option, + /// The altitude in feet that aircraft below which must stay below the `speed_limit` of this airport while nearby. + pub speed_limit_altitude: Option, + /// The IATA identifier of this airport, such as `LHR` for `EGLL` or `JFK` for `KJFK` + pub iata_ident: Option, +} + +impl From for Airport { + fn from(airport: sql_structs::Airports) -> Self { + Self { + ident: airport.airport_identifier, + area_code: airport.area_code, + icao_code: airport.icao_code, + location: Coordinates { + lat: airport.airport_ref_latitude, + long: airport.airport_ref_longitude, + }, + name: airport.airport_name, + ifr_capability: airport.ifr_capability, + longest_runway_surface_code: airport.longest_runway_surface_code, + elevation: airport.elevation, + transition_altitude: airport.transition_altitude, + transition_level: airport.transition_level, + speed_limit: airport.speed_limit, + speed_limit_altitude: airport.speed_limit_altitude, + iata_ident: airport.iata_ata_designator, + ..Default::default() + } + } +} + +impl From for Airport { + fn from(airport: v2::sql_structs::Airports) -> Self { + Self { + ident: airport.airport_identifier, + name: airport.airport_name, + location: Coordinates { + lat: airport.airport_ref_latitude, + long: airport.airport_ref_longitude, + }, + airport_type: Some(airport.airport_type), + area_code: airport.area_code, + iata_ident: airport.ata_iata_code, + city: airport.city, + continent: airport.continent, + country: airport.country, + country_3letter: airport.country_3letter, + elevation: airport.elevation, + icao_code: airport.icao_code, + ifr_capability: airport.ifr_capability.unwrap_or(IfrCapability::No), + longest_runway_surface_code: Some(airport.longest_runway_surface_code), + magnetic_variation: airport.magnetic_variation, + transition_altitude: airport.transition_altitude, + transition_level: airport.transition_level, + speed_limit: airport.speed_limit, + speed_limit_altitude: airport + .speed_limit_altitude + .and_then(|val| val.parse::().ok()), + state: airport.state, + state_2letter: airport.state_2letter, + } + } +} diff --git a/src/database/src/output/airspace.rs b/src/database/src/output/airspace.rs index e36087ba..e17a7e2b 100644 --- a/src/database/src/output/airspace.rs +++ b/src/database/src/output/airspace.rs @@ -1,185 +1,194 @@ -use serde::Serialize; - -use crate::{ - enums::{ControlledAirspaceType, RestrictiveAirspaceType, TurnDirection}, - math::{Coordinates, Degrees, NauticalMiles}, - sql_structs, -}; - -#[derive(Serialize, Debug)] -pub struct Arc { - pub origin: Coordinates, - pub distance: NauticalMiles, - pub bearing: Degrees, - pub direction: TurnDirection, -} - -#[derive(Serialize, Debug, Copy, Clone)] -pub enum PathType { - #[serde(rename = "C")] - Circle, - #[serde(rename = "G")] - GreatCircle, - #[serde(rename = "R")] - RhumbLine, - #[serde(rename = "A")] - Arc, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Debug)] -pub struct Path { - pub location: Coordinates, - pub arc: Option, - pub path_type: PathType, -} - -impl Path { - fn from_data( - latitude: Option, longitude: Option, arc_latitude: Option, arc_longitude: Option, - arc_distance: Option, arc_bearing: Option, boundary_via: String, - ) -> Self { - let boundary_char = boundary_via.chars().nth(0).unwrap(); - match boundary_char { - 'C' => Self { - location: Coordinates { - lat: arc_latitude.unwrap(), - long: arc_longitude.unwrap(), - }, - arc: None, - path_type: PathType::Circle, - }, - 'G' | 'H' => Self { - location: Coordinates { - lat: latitude.unwrap(), - long: longitude.unwrap(), - }, - arc: None, - path_type: match boundary_char { - 'G' => PathType::GreatCircle, - 'H' | _ => PathType::RhumbLine, - }, - }, - 'L' | 'R' => Self { - location: Coordinates { - lat: latitude.unwrap(), - long: longitude.unwrap(), - }, - arc: Some(Arc { - origin: Coordinates { - lat: arc_latitude.unwrap(), - long: arc_longitude.unwrap(), - }, - distance: arc_distance.unwrap(), - bearing: arc_bearing.unwrap(), - direction: match boundary_char { - 'R' => TurnDirection::Right, - 'L' | _ => TurnDirection::Left, - }, - }), - path_type: PathType::Arc, - }, - _ => panic!("Invalid path type"), - } - } -} - -#[derive(Serialize, Debug)] -pub struct ControlledAirspace { - pub area_code: String, - pub icao_code: String, - pub airspace_center: String, - pub name: String, - pub airspace_type: ControlledAirspaceType, - pub boundary_paths: Vec, -} - -#[derive(Serialize, Debug)] -pub struct RestrictiveAirspace { - pub area_code: String, - pub icao_code: String, - pub designation: String, - pub name: String, - pub airspace_type: RestrictiveAirspaceType, - pub boundary_paths: Vec, -} - -pub(crate) fn map_controlled_airspaces(data: Vec) -> Vec { - let mut airspace_complete = false; - - data.into_iter().fold(Vec::new(), |mut airspaces, row| { - if airspaces.len() == 0 || airspace_complete { - airspaces.push(ControlledAirspace { - area_code: row.area_code.clone(), - icao_code: row.icao_code.clone(), - airspace_center: row.airspace_center.clone(), - name: row - .controlled_airspace_name - .clone() - .expect("First row of an airspace data must have a name"), - airspace_type: row.airspace_type.clone(), - boundary_paths: Vec::new(), - }); - - airspace_complete = false; - } - - if row.boundary_via.chars().nth(1) == Some('E') { - airspace_complete = true; - } - - let target_airspace = airspaces.last_mut().unwrap(); - - target_airspace.boundary_paths.push(Path::from_data( - row.latitude, - row.longitude, - row.arc_origin_latitude, - row.arc_origin_longitude, - row.arc_distance, - row.arc_bearing, - row.boundary_via, - )); - - airspaces - }) -} - -pub(crate) fn map_restrictive_airspaces(data: Vec) -> Vec { - let mut airspace_complete = false; - - data.into_iter().fold(Vec::new(), |mut airspaces, row| { - if airspaces.len() == 0 || airspace_complete { - airspaces.push(RestrictiveAirspace { - area_code: row.area_code.clone(), - icao_code: row.icao_code.clone(), - designation: row.restrictive_airspace_designation.clone(), - name: row - .restrictive_airspace_name - .clone() - .expect("First row of an airspace data must have a name"), - airspace_type: row.restrictive_type.clone(), - boundary_paths: Vec::new(), - }); - - airspace_complete = false; - } - - if row.boundary_via.chars().nth(1) == Some('E') { - airspace_complete = true; - } - - let target_airspace = airspaces.last_mut().unwrap(); - - target_airspace.boundary_paths.push(Path::from_data( - row.latitude, - row.longitude, - row.arc_origin_latitude, - row.arc_origin_longitude, - row.arc_distance, - row.arc_bearing, - row.boundary_via, - )); - - airspaces - }) -} +use serde::Serialize; + +use crate::{ + enums::{ControlledAirspaceType, RestrictiveAirspaceType, TurnDirection}, + math::{Coordinates, Degrees, NauticalMiles}, + sql_structs, +}; + +#[derive(Serialize, Debug)] +pub struct Arc { + pub origin: Coordinates, + pub distance: NauticalMiles, + pub bearing: Degrees, + pub direction: TurnDirection, +} + +#[derive(Serialize, Debug, Copy, Clone)] +pub enum PathType { + #[serde(rename = "C")] + Circle, + #[serde(rename = "G")] + GreatCircle, + #[serde(rename = "R")] + RhumbLine, + #[serde(rename = "A")] + Arc, +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug)] +pub struct Path { + pub location: Coordinates, + pub arc: Option, + pub path_type: PathType, +} + +impl Path { + fn from_data( + latitude: Option, + longitude: Option, + arc_latitude: Option, + arc_longitude: Option, + arc_distance: Option, + arc_bearing: Option, + boundary_via: String, + ) -> Self { + let boundary_char = boundary_via.chars().nth(0).unwrap(); + match boundary_char { + 'C' => Self { + location: Coordinates { + lat: arc_latitude.unwrap(), + long: arc_longitude.unwrap(), + }, + arc: None, + path_type: PathType::Circle, + }, + 'G' | 'H' => Self { + location: Coordinates { + lat: latitude.unwrap(), + long: longitude.unwrap(), + }, + arc: None, + path_type: match boundary_char { + 'G' => PathType::GreatCircle, + _ => PathType::RhumbLine, // Also covers 'H' + }, + }, + 'L' | 'R' => Self { + location: Coordinates { + lat: latitude.unwrap(), + long: longitude.unwrap(), + }, + arc: Some(Arc { + origin: Coordinates { + lat: arc_latitude.unwrap(), + long: arc_longitude.unwrap(), + }, + distance: arc_distance.unwrap(), + bearing: arc_bearing.unwrap(), + direction: match boundary_char { + 'R' => TurnDirection::Right, + _ => TurnDirection::Left, // Also covers 'L' + }, + }), + path_type: PathType::Arc, + }, + _ => panic!("Invalid path type"), + } + } +} + +#[derive(Serialize, Debug)] +pub struct ControlledAirspace { + pub area_code: String, + pub icao_code: String, + pub airspace_center: String, + pub name: String, + pub airspace_type: ControlledAirspaceType, + pub boundary_paths: Vec, +} + +#[derive(Serialize, Debug)] +pub struct RestrictiveAirspace { + pub area_code: String, + pub icao_code: String, + pub designation: String, + pub name: String, + pub airspace_type: RestrictiveAirspaceType, + pub boundary_paths: Vec, +} + +pub(crate) fn map_controlled_airspaces( + data: Vec, +) -> Vec { + let mut airspace_complete = false; + + data.into_iter().fold(Vec::new(), |mut airspaces, row| { + if airspaces.is_empty() || airspace_complete { + airspaces.push(ControlledAirspace { + area_code: row.area_code.clone(), + icao_code: row.icao_code.clone(), + airspace_center: row.airspace_center.clone(), + name: row + .controlled_airspace_name + .clone() + .expect("First row of an airspace data must have a name"), + airspace_type: row.airspace_type, + boundary_paths: Vec::new(), + }); + + airspace_complete = false; + } + + if row.boundary_via.chars().nth(1) == Some('E') { + airspace_complete = true; + } + + let target_airspace = airspaces.last_mut().unwrap(); + + target_airspace.boundary_paths.push(Path::from_data( + row.latitude, + row.longitude, + row.arc_origin_latitude, + row.arc_origin_longitude, + row.arc_distance, + row.arc_bearing, + row.boundary_via, + )); + + airspaces + }) +} + +pub(crate) fn map_restrictive_airspaces( + data: Vec, +) -> Vec { + let mut airspace_complete = false; + + data.into_iter().fold(Vec::new(), |mut airspaces, row| { + if airspaces.is_empty() || airspace_complete { + airspaces.push(RestrictiveAirspace { + area_code: row.area_code.clone(), + icao_code: row.icao_code.clone(), + designation: row.restrictive_airspace_designation.clone(), + name: row + .restrictive_airspace_name + .clone() + .expect("First row of an airspace data must have a name"), + airspace_type: row.restrictive_type, + boundary_paths: Vec::new(), + }); + + airspace_complete = false; + } + + if row.boundary_via.chars().nth(1) == Some('E') { + airspace_complete = true; + } + + let target_airspace = airspaces.last_mut().unwrap(); + + target_airspace.boundary_paths.push(Path::from_data( + row.latitude, + row.longitude, + row.arc_origin_latitude, + row.arc_origin_longitude, + row.arc_distance, + row.arc_bearing, + row.boundary_via, + )); + + airspaces + }) +} diff --git a/src/database/src/output/airway.rs b/src/database/src/output/airway.rs index 0d979344..538c8e1a 100644 --- a/src/database/src/output/airway.rs +++ b/src/database/src/output/airway.rs @@ -1,72 +1,118 @@ -use serde::Serialize; - -use super::fix::Fix; -use crate::{ - enums::{AirwayDirection, AirwayLevel, AirwayRouteType}, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize)] -pub struct Airway { - /// Identifier of the airway (not unique), such as `A1` or `Y175` - pub ident: String, - /// A list of fixes which make up the airway - pub fixes: Vec, - /// The type of airway - pub route_type: AirwayRouteType, - /// Represents the altitude band which this aircraft is part of - /// - /// Can be: - /// - High - /// - Low - /// - Both - pub level: AirwayLevel, - /// Represents a directional restriction on this airway - /// - /// If it is `AirwayDirection::Forward`, this airway must only be flown in the order that fixes are listed in the - /// `fixes` field. - /// - /// If it is `AirwayDirection::Backward`, this airway must only be flown in the reverse order - /// that fixes are listed in the `fixes` field - pub direction: Option, -} - -/// Takes a vector of EnrouteAirway rows from the database and collects them into Airway structs -/// -/// This function requires complete airway data, so it is expected that the provided data comes from a query by -/// route_identifier, to ensure that full airways will be present. -/// -/// When querying airways by location always be sure to query all airways with route_identifiers which appear within the -/// query area. This is icao_code can change along one airway so it should not be used to group airways. There is no way -/// to way to identify distinct airways other than iterating through them to find an end of airway flag -pub(crate) fn map_airways(data: Vec) -> Vec { - let mut airway_complete = false; - data.into_iter().fold(Vec::new(), |mut airways, airway_row| { - if airways.len() == 0 || airway_complete { - airways.push(Airway { - ident: airway_row.route_identifier, - fixes: Vec::new(), - route_type: airway_row.route_type, - level: airway_row.flightlevel, - direction: airway_row.direction_restriction, - }); - - airway_complete = false; - } - - let target_airway = airways.last_mut().unwrap(); - - target_airway.fixes.push(Fix::from_row_data( - airway_row.waypoint_latitude, - airway_row.waypoint_longitude, - airway_row.id, - )); - - if airway_row.waypoint_description_code.chars().nth(1) == Some('E') { - airway_complete = true; - } - - airways - }) -} +use serde::Serialize; + +use super::fix::Fix; +use crate::{ + enums::{AirwayDirection, AirwayLevel, AirwayRouteType}, + sql_structs, + v2::{self}, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize)] +pub struct Airway { + /// Identifier of the airway (not unique), such as `A1` or `Y175` + pub ident: String, + /// A list of fixes which make up the airway + pub fixes: Vec, + /// The type of airway + pub route_type: AirwayRouteType, + /// Represents the altitude band which this aircraft is part of + /// + /// Can be: + /// - High + /// - Low + /// - Both + pub level: AirwayLevel, + /// Represents a directional restriction on this airway + /// + /// If it is `AirwayDirection::Forward`, this airway must only be flown in the order that fixes are listed in the + /// `fixes` field. + /// + /// If it is `AirwayDirection::Backward`, this airway must only be flown in the reverse order + /// that fixes are listed in the `fixes` field + pub direction: Option, +} + +/// Takes a vector of EnrouteAirway rows from the database and collects them into Airway structs +/// +/// This function requires complete airway data, so it is expected that the provided data comes from a query by +/// route_identifier, to ensure that full airways will be present. +/// +/// When querying airways by location always be sure to query all airways with route_identifiers which appear within the +/// query area. This is icao_code can change along one airway so it should not be used to group airways. There is no way +/// to way to identify distinct airways other than iterating through them to find an end of airway flag +pub(crate) fn map_airways(data: Vec) -> Vec { + let mut airway_complete = false; + data.into_iter() + .fold(Vec::new(), |mut airways, airway_row| { + if airways.is_empty() || airway_complete { + airways.push(Airway { + ident: airway_row.route_identifier, + fixes: Vec::new(), + route_type: airway_row.route_type, + level: airway_row.flightlevel, + direction: airway_row.direction_restriction, + }); + + airway_complete = false; + } + + let target_airway = airways.last_mut().unwrap(); + + target_airway.fixes.push(Fix::from_row_data( + airway_row.waypoint_latitude, + airway_row.waypoint_longitude, + airway_row.id, + )); + + if airway_row.waypoint_description_code.chars().nth(1) == Some('E') { + airway_complete = true; + } + + airways + }) +} + +// TODO: Implement error propigation, need to rewrite logic (maybe out of scope) +pub(crate) fn map_airways_v2(data: Vec) -> Vec { + let mut airway_complete = false; + data.into_iter() + .fold(Vec::new(), |mut airways, airway_row| { + if airways.is_empty() || airway_complete { + airways.push(Airway { + ident: airway_row.route_identifier.unwrap_or("ERROR".to_string()), + fixes: Vec::new(), + route_type: airway_row + .route_type + .unwrap_or(AirwayRouteType::UndesignatedAtsRoute), + level: airway_row.flightlevel.unwrap_or(AirwayLevel::Both), + direction: airway_row.direction_restriction, + }); + + airway_complete = false; + } + + let target_airway = airways.last_mut().unwrap(); + + target_airway.fixes.push(Fix::from_row_data_v2( + airway_row.waypoint_latitude.unwrap_or(0.), + airway_row.waypoint_longitude.unwrap_or(0.), + airway_row.waypoint_identifier.unwrap_or("NULL".to_string()), + airway_row.icao_code.unwrap_or("NULL".to_string()), + None, + airway_row.waypoint_ref_table, + )); + + if airway_row + .waypoint_description_code + .unwrap_or(" ".to_string()) + .chars() + .nth(1) + == Some('E') + { + airway_complete = true; + } + + airways + }) +} diff --git a/src/database/src/output/communication.rs b/src/database/src/output/communication.rs index a78e6d48..c8e9b7b5 100644 --- a/src/database/src/output/communication.rs +++ b/src/database/src/output/communication.rs @@ -1,69 +1,125 @@ -use serde::Serialize; - -use crate::{ - enums::{CommunicationType, FrequencyUnits}, - math::Coordinates, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Debug)] -/// Represents a communication station at an airport or in an enroute fir -pub struct Communication { - /// The Geographic region where this communication is - pub area_code: String, - /// The type of communication - pub communication_type: CommunicationType, - /// The identifier of the airport which this communication is at, if this an airport communication - pub airport_ident: Option, - /// The identifier of the FIR which this communication is in, if this is an enroute communication - pub fir_rdo_ident: Option, - /// The frequency of this communication - pub frequency: f64, - /// The units of the frequency of this communication - pub frequency_units: FrequencyUnits, - /// The callsign of this communication - pub callsign: Option, - /// The name of this communication (only defined for enroute communications) - pub name: Option, - /// The location of this communication - pub location: Coordinates, -} - -impl From for Communication { - fn from(row: sql_structs::AirportCommunication) -> Self { - Self { - area_code: row.area_code, - communication_type: row.communication_type, - airport_ident: Some(row.airport_identifier), - fir_rdo_ident: None, - frequency: row.communication_frequency, - frequency_units: row.frequency_units, - callsign: row.callsign, - name: None, - location: Coordinates { - lat: row.latitude, - long: row.longitude, - }, - } - } -} - -impl From for Communication { - fn from(row: sql_structs::EnrouteCommunication) -> Self { - Self { - area_code: row.area_code, - communication_type: row.communication_type, - airport_ident: None, - fir_rdo_ident: Some(row.fir_rdo_ident), - frequency: row.communication_frequency, - frequency_units: row.frequency_units, - callsign: row.callsign, - name: None, - location: Coordinates { - lat: row.latitude, - long: row.longitude, - }, - } - } -} +use serde::Serialize; + +use crate::{ + enums::{CommunicationType, FrequencyUnits}, + math::Coordinates, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Debug, Default)] +/// Represents a communication station at an airport or in an enroute fir +pub struct Communication { + /// The Geographic region where this communication is + pub area_code: String, + /// The type of communication + pub communication_type: CommunicationType, + /// The identifier of the airport which this communication is at, if this an airport communication + pub airport_ident: Option, + /// The identifier of the FIR which this communication is in, if this is an enroute communication + pub fir_rdo_ident: Option, + /// The frequency of this communication + pub frequency: f64, + /// The units of the frequency of this communication + pub frequency_units: FrequencyUnits, + /// The callsign of this communication + pub callsign: Option, + /// The name of this communication (only defined for enroute communications) + pub name: Option, + /// The location of this communication + pub location: Coordinates, + /// Facility in which an RCO will be transmitting through + pub remote_facility: Option, // new + pub remote_facility_icao_code: Option, // new + /// Sector associated with the communication + pub sector_facility: Option, // new + pub sector_facility_icao_code: Option, // new + /// Bearings from the sector facility is applicable to the communication + pub sectorization: Option, // new +} + +impl From for Communication { + fn from(row: sql_structs::AirportCommunication) -> Self { + Self { + area_code: row.area_code, + communication_type: row.communication_type, + airport_ident: Some(row.airport_identifier), + fir_rdo_ident: None, + frequency: row.communication_frequency, + frequency_units: row.frequency_units, + callsign: row.callsign, + name: None, + location: Coordinates { + lat: row.latitude, + long: row.longitude, + }, + ..Default::default() + } + } +} + +impl From for Communication { + fn from(row: sql_structs::EnrouteCommunication) -> Self { + Self { + area_code: row.area_code, + communication_type: row.communication_type, + airport_ident: None, + fir_rdo_ident: Some(row.fir_rdo_ident), + frequency: row.communication_frequency, + frequency_units: row.frequency_units, + callsign: row.callsign, + name: None, + location: Coordinates { + lat: row.latitude, + long: row.longitude, + }, + ..Default::default() + } + } +} + +impl From for Communication { + fn from(row: v2::sql_structs::AirportCommunication) -> Self { + Self { + area_code: row.area_code, + communication_type: row.communication_type, + airport_ident: Some(row.airport_identifier), + fir_rdo_ident: None, + frequency: row.communication_frequency, + frequency_units: row.frequency_units, + callsign: row.callsign, + name: None, + location: Coordinates { + lat: row.latitude, + long: row.longitude, + }, + remote_facility: row.remote_facility, + remote_facility_icao_code: row.remote_facility_icao_code, + sector_facility: row.sector_facility, + sector_facility_icao_code: row.sector_facility_icao_code, + sectorization: row.sectorization, + } + } +} + +impl From for Communication { + fn from(row: v2::sql_structs::EnrouteCommunication) -> Self { + Self { + area_code: row.area_code, + communication_type: row.communication_type, + airport_ident: None, + fir_rdo_ident: Some(row.fir_rdo_ident), + frequency: row.communication_frequency, + frequency_units: row.frequency_units, + callsign: row.callsign, + name: None, + location: Coordinates { + lat: row.latitude, + long: row.longitude, + }, + remote_facility: row.remote_facility, + remote_facility_icao_code: row.remote_facility_icao_code, + ..Default::default() + } + } +} diff --git a/src/database/src/output/database_info.rs b/src/database/src/output/database_info.rs index 9d4f59de..99312498 100644 --- a/src/database/src/output/database_info.rs +++ b/src/database/src/output/database_info.rs @@ -1,45 +1,78 @@ -use std::str::FromStr; - -use serde::Serialize; - -use crate::sql_structs; - -#[derive(Serialize)] -pub struct DatabaseInfo { - /// The AIRAC cycle that this database is. - /// - /// e.g. `2313` or `2107` - airac_cycle: String, - /// The effective date range of this AIRAC cycle. - effective_from_to: (String, String), - /// The effective date range of the previous AIRAC cycle - previous_from_to: (String, String), -} - -/// Converts a string of the format `DDMMDDMMYY` into a tuple of two strings of the format `DD-MM-YYYY`. -/// -/// If the previous month is greater than the current month, the previous year is decremented by 1. -fn parse_from_to(data: String) -> Result<(String, String), ::Err> { - let from_day = data[0..2].parse::()?; - let from_month = data[2..4].parse::()?; - let to_day = data[4..6].parse::()?; - let to_month = data[6..8].parse::()?; - let to_year = data[8..10].parse::()?; - - let from_year = if to_month < from_month { to_year - 1 } else { to_year }; - - Ok(( - format!("{from_day:0>2}-{from_month:0>2}-20{from_year:0>2}"), - format!("{to_day:0>2}-{to_month:0>2}-20{to_year:0>2}"), - )) -} - -impl From for DatabaseInfo { - fn from(header: sql_structs::Header) -> Self { - Self { - airac_cycle: header.current_airac, - effective_from_to: parse_from_to(header.effective_fromto).unwrap(), - previous_from_to: parse_from_to(header.previous_fromto).unwrap(), - } - } -} +use std::str::FromStr; + +use serde::Serialize; + +use crate::{sql_structs, v2}; + +#[derive(Serialize)] +pub struct DatabaseInfo { + /// The AIRAC cycle that this database is. + /// + /// e.g. `2313` or `2107` + pub airac_cycle: String, + /// The effective date range of this AIRAC cycle. + pub effective_from_to: (String, String), + /// The effective date range of the previous AIRAC cycle + pub previous_from_to: (String, String), +} + +impl DatabaseInfo { + pub fn new( + cycle: String, + effective_from: String, + effective_to: String, + previous_from: Option, + previous_to: Option, + ) -> Self { + DatabaseInfo { + airac_cycle: cycle, + effective_from_to: (effective_from, effective_to), + previous_from_to: ( + previous_from.unwrap_or("depricated".to_string()), + previous_to.unwrap_or("depricated".to_string()), + ), + } + } +} + +/// Converts a string of the format `DDMMDDMMYY` into a tuple of two strings of the format `DD-MM-YYYY`. +/// +/// If the previous month is greater than the current month, the previous year is decremented by 1. +fn parse_from_to(data: String) -> Result<(String, String), ::Err> { + let from_day = data[0..2].parse::()?; + let from_month = data[2..4].parse::()?; + let to_day = data[4..6].parse::()?; + let to_month = data[6..8].parse::()?; + let to_year = data[8..10].parse::()?; + + let from_year = if to_month < from_month { + to_year - 1 + } else { + to_year + }; + + Ok(( + format!("{from_day:0>2}-{from_month:0>2}-20{from_year:0>2}"), + format!("{to_day:0>2}-{to_month:0>2}-20{to_year:0>2}"), + )) +} + +impl From for DatabaseInfo { + fn from(header: sql_structs::Header) -> Self { + Self { + airac_cycle: header.current_airac, + effective_from_to: parse_from_to(header.effective_fromto).unwrap(), + previous_from_to: parse_from_to(header.previous_fromto).unwrap(), + } + } +} + +impl From for DatabaseInfo { + fn from(header: v2::sql_structs::Header) -> Self { + Self { + airac_cycle: header.cycle, + effective_from_to: parse_from_to(header.effective_fromto).unwrap(), + previous_from_to: ("depricated".to_string(), "depricated".to_string()), + } + } +} diff --git a/src/database/src/output/fix.rs b/src/database/src/output/fix.rs index 407a6e16..706f2a65 100644 --- a/src/database/src/output/fix.rs +++ b/src/database/src/output/fix.rs @@ -1,82 +1,113 @@ -use serde::Serialize; - -use crate::math::Coordinates; - -#[derive(Serialize, Copy, Clone)] -pub enum FixType { - #[serde(rename = "A")] - Airport, - #[serde(rename = "N")] - NdbNavaid, - #[serde(rename = "R")] - RunwayThreshold, - #[serde(rename = "G")] - GlsNavaid, - #[serde(rename = "I")] - IlsNavaid, - #[serde(rename = "V")] - VhfNavaid, - #[serde(rename = "W")] - Waypoint, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone)] -/// Represents a fix which was used as a reference in a procedure or an airway. -/// -/// Every `Fix` will have a full data entry as one of these structs somewhere in the database with the same `ident` and -/// `icao_code`: -/// - `Airport` -/// - `NdbNavaid` -/// - `RunwayThreshold` -/// - `GlsNavaid` -/// - `IlsNavaid` -/// - `VhfNavaid` -/// - `Waypoint` -pub struct Fix { - /// The type of fix - pub fix_type: FixType, - /// The identifier of this fix (not unique), such as `KLAX` or `BI` or `RW17L` or `G07J` or `ISYK` or `YXM` or - /// `GLENN` - pub ident: String, - /// The icao prefix of the region that this fix is in. - pub icao_code: String, - /// The geographic location of this fix - pub location: Coordinates, - /// The identifier of the airport that this fix is associated with, if any - pub airport_ident: Option, -} - -impl Fix { - /// Creates a `Fix` by using the latitude and longitude fields, and by parsing the linked id field from a procedure - /// or airway row. - pub fn from_row_data(lat: f64, long: f64, id: String) -> Self { - let table = id.split("|").nth(0).unwrap(); - let id = id.split("|").nth(1).unwrap(); - let (airport_identifier, icao_code, ident) = - if table.starts_with("tbl_terminal") || table == "tbl_localizers_glideslopes" || table == "tbl_gls" { - (Some(&id[0..4]), &id[4..6], &id[6..]) - } else { - (None, &id[0..2], &id[2..]) - }; - - let fix_type = match table { - "tbl_airports" => FixType::Airport, - "tbl_terminal_ndbnavaids" | "tbl_enroute_ndbnavaids" => FixType::NdbNavaid, - "tbl_runways" => FixType::RunwayThreshold, - "tbl_gls" => FixType::GlsNavaid, - "tbl_localizers_glideslopes" => FixType::IlsNavaid, - "tbl_vhfnavaids" => FixType::VhfNavaid, - "tbl_enroute_waypoints" | "tbl_terminal_waypoints" => FixType::Waypoint, - x => panic!("Unexpected table: '{x}'"), - }; - - Self { - fix_type, - ident: ident.to_string(), - icao_code: icao_code.to_string(), - location: Coordinates { lat, long }, - airport_ident: airport_identifier.map(|s| s.to_string()), - } - } -} +use serde::{Deserialize, Serialize}; + +use crate::math::Coordinates; + +#[derive(Serialize, Deserialize, Copy, Clone, Debug)] +pub enum FixType { + #[serde(rename = "A")] + Airport, + #[serde(rename = "N")] + NdbNavaid, + #[serde(rename = "R")] + RunwayThreshold, + #[serde(rename = "G")] + GlsNavaid, + #[serde(rename = "I")] + IlsNavaid, + #[serde(rename = "V")] + VhfNavaid, + #[serde(rename = "W")] + Waypoint, +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone)] +/// Represents a fix which was used as a reference in a procedure or an airway. +/// +/// Every `Fix` will have a full data entry as one of these structs somewhere in the database with the same `ident` and +/// `icao_code`: +/// - `Airport` +/// - `NdbNavaid` +/// - `RunwayThreshold` +/// - `GlsNavaid` +/// - `IlsNavaid` +/// - `VhfNavaid` +/// - `Waypoint` +#[derive(Debug)] +pub struct Fix { + /// The type of fix + pub fix_type: FixType, + /// The identifier of this fix (not unique), such as `KLAX` or `BI` or `RW17L` or `G07J` or `ISYK` or `YXM` or + /// `GLENN` + pub ident: String, + /// The icao prefix of the region that this fix is in. + pub icao_code: String, + /// The geographic location of this fix (this does not exist on v2) + pub location: Coordinates, + /// The identifier of the airport that this fix is associated with, if any + pub airport_ident: Option, +} + +impl Fix { + /// Creates a `Fix` by using the latitude and longitude fields, and by parsing the linked id field from a procedure + /// or airway row. + pub fn from_row_data(lat: f64, long: f64, id_raw: String) -> Self { + let table = id_raw.split('|').nth(0).unwrap(); + let id = id_raw.split('|').nth(1).unwrap(); + let (airport_identifier, icao_code, ident) = if table.starts_with("tbl_terminal") + || table == "tbl_localizers_glideslopes" + || table == "tbl_gls" + { + (Some(&id[0..4]), &id[4..6], &id[6..]) + } else { + (None, &id[0..2], &id[2..]) + }; + + let fix_type = match table { + "tbl_airports" => FixType::Airport, + "tbl_terminal_ndbnavaids" | "tbl_enroute_ndbnavaids" => FixType::NdbNavaid, + "tbl_runways" => FixType::RunwayThreshold, + "tbl_gls" => FixType::GlsNavaid, + "tbl_localizers_glideslopes" => FixType::IlsNavaid, + "tbl_vhfnavaids" => FixType::VhfNavaid, + "tbl_enroute_waypoints" | "tbl_terminal_waypoints" => FixType::Waypoint, + x => panic!("Unexpected table: '{x}'"), + }; + + Self { + fix_type, + ident: ident.to_string(), + icao_code: icao_code.to_string(), + location: Coordinates { lat, long }, + airport_ident: airport_identifier.map(|s| s.to_string()), + } + } + + pub fn from_row_data_v2( + lat: f64, + long: f64, + ident: String, + icao_code: String, + airport_ident: Option, + ref_table: String, + ) -> Self { + let fix_type = match ref_table.as_str() { + "PA" => FixType::Airport, + "PN" | "DB" => FixType::NdbNavaid, + "PG" => FixType::RunwayThreshold, + "PT" => FixType::GlsNavaid, + "PI" => FixType::IlsNavaid, + "D " => FixType::VhfNavaid, + "EA" | "PC" => FixType::Waypoint, + x => panic!("Unexpected table: '{x}'"), + }; + + Self { + fix_type, + ident, + icao_code, + location: Coordinates { lat, long }, + airport_ident, + } + } +} diff --git a/src/database/src/output/gate.rs b/src/database/src/output/gate.rs index 7a5cce49..902fa99b 100644 --- a/src/database/src/output/gate.rs +++ b/src/database/src/output/gate.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use crate::{math::Coordinates, sql_structs}; +use crate::{math::Coordinates, sql_structs, v2}; #[derive(Serialize)] /// Represents a gate at an airport @@ -31,3 +31,18 @@ impl From for Gate { } } } + +impl From for Gate { + fn from(row: v2::sql_structs::Gate) -> Self { + Self { + area_code: row.area_code, + icao_code: row.icao_code, + ident: row.gate_identifier, + location: Coordinates { + lat: row.gate_latitude, + long: row.gate_longitude, + }, + name: row.name.unwrap_or(String::from("N/A")), + } + } +} diff --git a/src/database/src/output/gls_navaid.rs b/src/database/src/output/gls_navaid.rs index 255db4d1..f329f9c4 100644 --- a/src/database/src/output/gls_navaid.rs +++ b/src/database/src/output/gls_navaid.rs @@ -1,57 +1,79 @@ -use serde::Serialize; - -use crate::{ - math::{Coordinates, Degrees, Feet}, - sql_structs, -}; - -#[derive(Serialize)] -pub struct GlsNavaid { - /// The Geographic region where this navaid is - pub area_code: String, - /// The identifier of the airport which this navaid serves - pub airport_ident: String, - /// The icao prefix of the region this navaid is in - pub icao_code: String, - /// The identifier of this navaid, such as `G03P` or `A34A` - pub ident: String, - /// The category of this navaid, Technically can be multiple values, but the database only contains `1` as the - /// value for this field - pub category: String, - /// The channel of this navaid - pub channel: f64, - /// The identifier of the runway this navaid serves - pub runway_ident: String, - /// The magnetic bearing of the approach to this navaid - pub magnetic_approach_bearing: Degrees, - /// The location of this navaid - pub location: Coordinates, - /// The angle of the approach to this navaid - pub approach_angle: Degrees, - /// The magnetic variation at this navaid - pub magnetic_variation: f64, - /// The elevation of this navaid - pub elevation: Feet, -} - -impl From for GlsNavaid { - fn from(gls: sql_structs::Gls) -> Self { - Self { - area_code: gls.area_code, - airport_ident: gls.airport_identifier, - icao_code: gls.icao_code, - ident: gls.gls_ref_path_identifier, - category: gls.gls_category, - runway_ident: gls.runway_identifier, - channel: gls.gls_channel, - magnetic_approach_bearing: gls.gls_approach_bearing, - location: Coordinates { - lat: gls.station_latitude, - long: gls.station_longitude, - }, - approach_angle: gls.gls_approach_slope, - magnetic_variation: gls.magentic_variation, - elevation: gls.station_elevation, - } - } -} +use serde::Serialize; + +use crate::{ + math::{Coordinates, Degrees, Feet}, + sql_structs, v2, +}; + +#[derive(Serialize)] +pub struct GlsNavaid { + /// The Geographic region where this navaid is + pub area_code: String, + /// The identifier of the airport which this navaid serves + pub airport_ident: String, + /// The icao prefix of the region this navaid is in + pub icao_code: String, + /// The identifier of this navaid, such as `G03P` or `A34A` + pub ident: String, + /// The category of this navaid, Technically can be multiple values, but the database only contains `1` as the + /// value for this field + pub category: String, + /// The channel of this navaid + pub channel: f64, + /// The identifier of the runway this navaid serves + pub runway_ident: String, + /// The magnetic bearing of the approach to this navaid + pub magnetic_approach_bearing: Degrees, + /// The location of this navaid + pub location: Coordinates, + /// The angle of the approach to this navaid + pub approach_angle: Degrees, + /// The magnetic variation at this navaid + pub magnetic_variation: f64, + /// The elevation of this navaid + pub elevation: Feet, +} + +impl From for GlsNavaid { + fn from(gls: sql_structs::Gls) -> Self { + Self { + area_code: gls.area_code, + airport_ident: gls.airport_identifier, + icao_code: gls.icao_code, + ident: gls.gls_ref_path_identifier, + category: gls.gls_category, + runway_ident: gls.runway_identifier, + channel: gls.gls_channel, + magnetic_approach_bearing: gls.gls_approach_bearing, + location: Coordinates { + lat: gls.station_latitude, + long: gls.station_longitude, + }, + approach_angle: gls.gls_approach_slope, + magnetic_variation: gls.magentic_variation, + elevation: gls.station_elevation, + } + } +} + +impl From for GlsNavaid { + fn from(gls: v2::sql_structs::Gls) -> Self { + Self { + area_code: gls.area_code, + airport_ident: gls.airport_identifier, + icao_code: gls.icao_code, + ident: gls.gls_ref_path_identifier, + category: gls.gls_category, + runway_ident: gls.runway_identifier, + channel: gls.gls_channel, + magnetic_approach_bearing: gls.gls_approach_bearing, + location: Coordinates { + lat: gls.station_latitude, + long: gls.station_longitude, + }, + approach_angle: gls.gls_approach_slope, + magnetic_variation: gls.magnetic_variation, + elevation: gls.station_elevation, + } + } +} diff --git a/src/database/src/output/ndb_navaid.rs b/src/database/src/output/ndb_navaid.rs index f07f8ece..8595bbba 100644 --- a/src/database/src/output/ndb_navaid.rs +++ b/src/database/src/output/ndb_navaid.rs @@ -1,42 +1,72 @@ -use serde::Serialize; - -use crate::{ - math::{Coordinates, KiloHertz}, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize)] -pub struct NdbNavaid { - /// Represents the geographic region in which this NdbNavaid is located - pub area_code: String, - /// The identifier of the airport that this NdbNavaid is associated with, if any - pub airport_ident: Option, - /// The icao prefix of the region that this NdbNavaid is in. - pub icao_code: String, - /// The identifier of this NdbNavaid (not unique), such as `BI` or `PHH` - pub ident: String, - /// The formal name of this NdbNavaid such as `HERBB OLATHE` or `KEDZI CHICAGO` - pub name: String, - /// The frequency of this NdbNavaid in kilohertz - pub frequency: KiloHertz, - /// The geographic location of thie NdbNavaid - pub location: Coordinates, -} - -impl From for NdbNavaid { - fn from(navaid: sql_structs::NdbNavaids) -> Self { - Self { - area_code: navaid.area_code, - airport_ident: navaid.airport_identifier, - icao_code: navaid.icao_code, - ident: navaid.ndb_identifier, - name: navaid.ndb_name, - frequency: navaid.ndb_frequency, - location: Coordinates { - lat: navaid.ndb_latitude, - long: navaid.ndb_longitude, - }, - } - } -} +use serde::Serialize; + +use crate::{ + math::{Coordinates, KiloHertz, NauticalMiles}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct NdbNavaid { + /// Represents the geographic region in which this NdbNavaid is located + pub area_code: String, + /// Continent of the waypoint (v2 only) + pub continent: Option, + /// Country of the waypoint (v2 only) + pub country: Option, + /// 3 Letter identifier describing the local horizontal identifier (v2 only) + pub datum_code: Option, + /// The identifier of the airport that this NdbNavaid is associated with, if any + pub airport_ident: Option, + /// The icao prefix of the region that this NdbNavaid is in. + pub icao_code: String, + /// The identifier of this NdbNavaid (not unique), such as `BI` or `PHH` + pub ident: String, + /// The formal name of this NdbNavaid such as `HERBB OLATHE` or `KEDZI CHICAGO` + pub name: String, + /// The frequency of this NdbNavaid in kilohertz + pub frequency: KiloHertz, + /// The geographic location of thie NdbNavaid + pub location: Coordinates, + /// Range of the NDB (v2 only) + pub range: Option, +} + +impl From for NdbNavaid { + fn from(navaid: sql_structs::NdbNavaids) -> Self { + Self { + area_code: navaid.area_code, + airport_ident: navaid.airport_identifier, + icao_code: navaid.icao_code, + ident: navaid.ndb_identifier, + name: navaid.ndb_name, + frequency: navaid.ndb_frequency, + location: Coordinates { + lat: navaid.ndb_latitude, + long: navaid.ndb_longitude, + }, + ..Default::default() + } + } +} + +impl From for NdbNavaid { + fn from(navaid: v2::sql_structs::NdbNavaids) -> Self { + Self { + area_code: navaid.area_code, + airport_ident: navaid.airport_identifier, + icao_code: navaid.icao_code.unwrap_or(String::from("N/A")), + ident: navaid.navaid_identifier.unwrap_or(String::from("N/A")), + name: navaid.navaid_name, + frequency: navaid.navaid_frequency, + location: Coordinates { + lat: navaid.navaid_latitude.unwrap_or_default(), + long: navaid.navaid_longitude.unwrap_or_default(), + }, + continent: navaid.continent, + country: navaid.country, + datum_code: navaid.datum_code, + range: navaid.range, + } + } +} diff --git a/src/database/src/output/path_point.rs b/src/database/src/output/path_point.rs index f1de2140..00992f70 100644 --- a/src/database/src/output/path_point.rs +++ b/src/database/src/output/path_point.rs @@ -1,70 +1,107 @@ -use serde::Serialize; - -use crate::{ - enums::ApproachTypeIdentifier, - math::{feet_to_meters, Coordinates, Degrees, Meters}, - sql_structs, -}; - -#[derive(Serialize)] -pub struct PathPoint { - pub area_code: String, - pub airport_ident: String, - pub icao_code: String, - /// The identifier of the approach this path point is used in, such as `R36RY` or `R20` - pub approach_ident: String, - /// The identifier of the runway this path point is used with, such as `RW02` or `RW36L` - pub runway_ident: String, - pub ident: String, - pub landing_threshold_location: Coordinates, - pub ltp_ellipsoid_height: Meters, - pub fpap_ellipsoid_height: Meters, - pub ltp_orthometric_height: Option, - pub fpap_orthometric_height: Option, - pub glidepath_angle: Degrees, - pub flightpath_alignment_location: Coordinates, - pub course_width: Meters, - pub length_offset: Meters, - pub path_point_tch: Meters, - pub horizontal_alert_limit: Meters, - pub vertical_alert_limit: Meters, - pub gnss_channel_number: f64, - pub approach_type: ApproachTypeIdentifier, -} - -impl From for PathPoint { - fn from(row: sql_structs::Pathpoints) -> Self { - Self { - area_code: row.area_code, - airport_ident: row.airport_identifier, - icao_code: row.icao_code, - approach_ident: row.approach_procedure_ident, - runway_ident: row.runway_identifier, - ident: row.reference_path_identifier, - landing_threshold_location: Coordinates { - lat: row.landing_threshold_latitude, - long: row.landing_threshold_longitude, - }, - ltp_ellipsoid_height: row.ltp_ellipsoid_height, - fpap_ellipsoid_height: row.fpap_ellipsoid_height, - ltp_orthometric_height: row.ltp_orthometric_height, - fpap_orthometric_height: row.fpap_orthometric_height, - glidepath_angle: row.glidepath_angle, - flightpath_alignment_location: Coordinates { - lat: row.flightpath_alignment_latitude, - long: row.flightpath_alignment_longitude, - }, - course_width: row.course_width_at_threshold, - length_offset: row.length_offset, - path_point_tch: if row.tch_units_indicator == "F".to_string() { - feet_to_meters(row.path_point_tch) - } else { - row.path_point_tch - }, - horizontal_alert_limit: row.hal, - vertical_alert_limit: row.val, - gnss_channel_number: row.gnss_channel_number, - approach_type: row.approach_type_identifier, - } - } -} +use serde::Serialize; + +use crate::{ + enums::ApproachTypeIdentifier, + math::{feet_to_meters, Coordinates, Degrees, Meters}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct PathPoint { + pub area_code: String, + pub airport_ident: String, + pub icao_code: String, + /// The identifier of the approach this path point is used in, such as `R36RY` or `R20` + pub approach_ident: String, + /// The identifier of the runway this path point is used with, such as `RW02` or `RW36L` + pub runway_ident: String, + pub ident: String, + pub landing_threshold_location: Coordinates, + pub ltp_ellipsoid_height: Meters, + /// Other heights are v1 only + pub fpap_ellipsoid_height: Option, // Does not exist on v2 + pub ltp_orthometric_height: Option, + pub fpap_orthometric_height: Option, + pub glidepath_angle: Degrees, + pub flightpath_alignment_location: Coordinates, + pub course_width: Meters, + pub length_offset: Meters, + pub path_point_tch: Meters, + pub horizontal_alert_limit: Meters, + pub vertical_alert_limit: Meters, + pub gnss_channel_number: f64, + pub approach_type: ApproachTypeIdentifier, +} + +impl From for PathPoint { + fn from(row: sql_structs::Pathpoints) -> Self { + Self { + area_code: row.area_code, + airport_ident: row.airport_identifier, + icao_code: row.icao_code, + approach_ident: row.approach_procedure_ident, + runway_ident: row.runway_identifier, + ident: row.reference_path_identifier, + landing_threshold_location: Coordinates { + lat: row.landing_threshold_latitude, + long: row.landing_threshold_longitude, + }, + ltp_ellipsoid_height: row.ltp_ellipsoid_height, + fpap_ellipsoid_height: Some(row.fpap_ellipsoid_height), + ltp_orthometric_height: row.ltp_orthometric_height, + fpap_orthometric_height: row.fpap_orthometric_height, + glidepath_angle: row.glidepath_angle, + flightpath_alignment_location: Coordinates { + lat: row.flightpath_alignment_latitude, + long: row.flightpath_alignment_longitude, + }, + course_width: row.course_width_at_threshold, + length_offset: row.length_offset, + path_point_tch: if row.tch_units_indicator == *"F" { + feet_to_meters(row.path_point_tch) + } else { + row.path_point_tch + }, + horizontal_alert_limit: row.hal, + vertical_alert_limit: row.val, + gnss_channel_number: row.gnss_channel_number, + approach_type: row.approach_type_identifier, + } + } +} + +impl From for PathPoint { + fn from(row: v2::sql_structs::Pathpoints) -> Self { + Self { + area_code: row.area_code, + airport_ident: row.airport_identifier, + icao_code: row.airport_icao_code, + approach_ident: row.approach_procedure_ident, + runway_ident: row.runway_identifier, + ident: row.reference_path_identifier, + landing_threshold_location: Coordinates { + lat: row.landing_threshold_point_latitude, + long: row.landing_threshold_point_longitude, + }, + ltp_ellipsoid_height: row.ltp_ellipsoid_height, + glidepath_angle: row.glide_path_angle, + flightpath_alignment_location: Coordinates { + lat: row.flight_path_alignment_point_latitude, + long: row.flight_path_alignment_point_longitude, + }, + course_width: row.course_width_at_threshold, + length_offset: row.length_offset.unwrap_or_default(), + path_point_tch: if row.tch_units_indicator == *"F" { + feet_to_meters(row.path_point_tch) + } else { + row.path_point_tch + }, + horizontal_alert_limit: row.hal, + vertical_alert_limit: row.val, + gnss_channel_number: row.gnss_channel_number, + approach_type: row.approach_type_identifier, + ..Default::default() + } + } +} diff --git a/src/database/src/output/procedure/approach.rs b/src/database/src/output/procedure/approach.rs index c275e781..56d7dffb 100644 --- a/src/database/src/output/procedure/approach.rs +++ b/src/database/src/output/procedure/approach.rs @@ -1,140 +1,204 @@ -use std::collections::{hash_map::Entry, HashMap}; - -use regex::Regex; -use serde::Serialize; - -use super::{apply_enroute_transition_leg, Transition}; -use crate::{enums::ApproachType, output::procedure_leg::ProcedureLeg, sql_structs}; - -#[derive(Serialize)] -/// Represents an approach procedure for an airport. -/// -/// # Example -/// Basic querying: -/// ```rs -/// let database = Database::new(); -/// let approaches: Vec = database.get_approaches_at_airport("KJFK"); -/// ``` -pub struct Approach { - /// The `ident` uniquely identifies this approach within the airport which it serves - /// - /// For approaches which are for a specific runway, it will have a format such as `I08L` or `R12-M`. - /// - The first character identifies the type of approach, however this will not always match the `approach_type` - /// field. The next three characters represent the runway identifier, such as `08L` or `12`. - /// - The 5th character (optional) is the multiple indicator of the approach, it can be any capital letter. - /// - For approaches with a multiple indicator and no `LCR` on the runway, the 4th character will be a `-` - /// - /// If this approach is for no specific runway, it will have a format such as `RNVC` or `GPSM` - ident: String, - /// Contains the transitions for the approach. On Airbus aircraft, these are known as `VIAs`. - transitions: Vec, - /// Contains the legs which make up the main body of this approach. - legs: Vec, - /// Contains the legs which are part of the missed approach portion of this approach. - missed_legs: Vec, - /// Represents the runway which this approach is for, if it is for a specific runway. - /// - /// This Field is generated from the `ident` in order to better match the `ident` field of `RunwayThreshold`. - /// - /// e.g. `RW27L` - runway_ident: Option, - /// Determines the type of approach, such as ILS, GPS, RNAV, etc. - /// - /// This is not garunteed to match the type found through the `ident` field. - approach_type: ApproachType, -} - -/// Extracts the following information from a standard runway approach identifier. -/// - The approach type character -/// - The runway identifier -/// - The multiple indicator -/// -/// If the approach identifier is not in this format, this function will return `None`. -/// -/// # Example -/// ```rs -/// let (approach_type, runway_ident, multiple_indicator) = split_approach_ident("I08L".to_string()).unwrap(); -/// -/// assert_eq!(approach_type, "I"); -/// assert_eq!(runway_ident, "08L"); -/// assert_eq!(multiple_indicator, None); -/// ``` -pub fn split_approach_ident(ident: String) -> Option<(String, String, Option)> { - let regex = Regex::new("^([A-Z])([0-9]{2}[LCR]?)-?([A-Z])?$").unwrap(); - let captures = regex.captures_iter(ident.as_str()).next()?; - - Some(( - captures.get(1).unwrap().as_str().to_string(), - captures.get(2).unwrap().as_str().to_string(), - captures.get(3).map(|x| x.as_str().to_string()), - )) -} - -/// Maps a list of approach rows from the sqlite database into `Approach` structs, by condensing them by -/// `procedure_identifier` and `transition_identifier` -/// -/// This function requires complete data for a single airport and the same ordering as the database provides by default. -/// -/// The recommended SQL query to load the neccesary data for this function is: -/// ```sql -/// SELECT * FROM tbl_iaps WHERE airport_identifier = (?1) -/// ``` -pub(crate) fn map_approaches(data: Vec) -> Vec { - let mut missed_started = false; - - data.into_iter() - .fold(HashMap::new(), |mut approaches, row| { - let approach = match approaches.entry(row.procedure_identifier.clone()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - missed_started = false; - - entry.insert(Approach { - ident: row.procedure_identifier.clone(), - transitions: Vec::new(), - legs: Vec::new(), - missed_legs: Vec::new(), - runway_ident: split_approach_ident(row.procedure_identifier.clone()) - .map(|(_, runway_ident, _)| format!("RW{}", runway_ident)), - approach_type: ApproachType::Fms, /* Set to an arbitrary value, will be overwritten once we - * find a row with a valid approach type (the first row in - * an approach will usually be a transition so it can not - * be used to find the approach type) */ - }) - }, - }; - - let route_type = row.route_type.clone(); - let transition_identifier = row.transition_identifier.clone(); - - if let Some(description_code) = &row.waypoint_description_code { - if description_code.chars().nth(2) == Some('M') { - missed_started = true; - } - } - - let leg = ProcedureLeg::from(row); - - match route_type.as_str() { - "A" => apply_enroute_transition_leg( - leg, - transition_identifier.expect("Transition leg was found without a transition identifier"), - &mut approach.transitions, - ), - "Z" => approach.missed_legs.push(leg), - x => { - if missed_started || x == "Z" { - approach.missed_legs.push(leg); - } else { - approach.approach_type = serde_json::from_value(serde_json::Value::String(route_type)).unwrap(); - - approach.legs.push(leg) - } - }, - } - - approaches - }) - .into_values() - .collect() -} +use std::collections::{hash_map::Entry, HashMap}; + +use regex::Regex; +use serde::Serialize; + +use super::{apply_enroute_transition_leg, Transition}; +use crate::{enums::ApproachType, output::procedure_leg::ProcedureLeg, sql_structs, v2}; + +#[derive(Serialize)] +/// Represents an approach procedure for an airport. +/// +/// # Example +/// Basic querying: +/// ```rs +/// let database = Database::new(); +/// let approaches: Vec = database.get_approaches_at_airport("KJFK"); +/// ``` +pub struct Approach { + /// The `ident` uniquely identifies this approach within the airport which it serves + /// + /// For approaches which are for a specific runway, it will have a format such as `I08L` or `R12-M`. + /// - The first character identifies the type of approach, however this will not always match the `approach_type` + /// field. The next three characters represent the runway identifier, such as `08L` or `12`. + /// - The 5th character (optional) is the multiple indicator of the approach, it can be any capital letter. + /// - For approaches with a multiple indicator and no `LCR` on the runway, the 4th character will be a `-` + /// + /// If this approach is for no specific runway, it will have a format such as `RNVC` or `GPSM` + ident: String, + /// Contains the transitions for the approach. On Airbus aircraft, these are known as `VIAs`. + transitions: Vec, + /// Contains the legs which make up the main body of this approach. + legs: Vec, + /// Contains the legs which are part of the missed approach portion of this approach. + missed_legs: Vec, + /// Represents the runway which this approach is for, if it is for a specific runway. + /// + /// This Field is generated from the `ident` in order to better match the `ident` field of `RunwayThreshold`. + /// + /// e.g. `RW27L` + runway_ident: Option, + /// Determines the type of approach, such as ILS, GPS, RNAV, etc. + /// + /// This is not garunteed to match the type found through the `ident` field. + approach_type: ApproachType, +} + +/// Extracts the following information from a standard runway approach identifier. +/// - The approach type character +/// - The runway identifier +/// - The multiple indicator +/// +/// If the approach identifier is not in this format, this function will return `None`. +/// +/// # Example +/// ```rs +/// let (approach_type, runway_ident, multiple_indicator) = split_approach_ident("I08L".to_string()).unwrap(); +/// +/// assert_eq!(approach_type, "I"); +/// assert_eq!(runway_ident, "08L"); +/// assert_eq!(multiple_indicator, None); +/// ``` +pub fn split_approach_ident(ident: String) -> Option<(String, String, Option)> { + let regex = Regex::new("^([A-Z])([0-9]{2}[LCR]?)-?([A-Z])?$").unwrap(); + let captures = regex.captures_iter(ident.as_str()).next()?; + + Some(( + captures.get(1).unwrap().as_str().to_string(), + captures.get(2).unwrap().as_str().to_string(), + captures.get(3).map(|x| x.as_str().to_string()), + )) +} + +/// Maps a list of approach rows from the sqlite database into `Approach` structs, by condensing them by +/// `procedure_identifier` and `transition_identifier` +/// +/// This function requires complete data for a single airport and the same ordering as the database provides by default. +/// +/// The recommended SQL query to load the neccesary data for this function is: +/// ```sql +/// SELECT * FROM tbl_iaps WHERE airport_identifier = (?1) +/// ``` +pub(crate) fn map_approaches(data: Vec) -> Vec { + let mut missed_started = false; + + data.into_iter() + .fold(HashMap::new(), |mut approaches, row| { + let approach = match approaches.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + missed_started = false; + + entry.insert(Approach { + ident: row.procedure_identifier.clone(), + transitions: Vec::new(), + legs: Vec::new(), + missed_legs: Vec::new(), + runway_ident: split_approach_ident(row.procedure_identifier.clone()) + .map(|(_, runway_ident, _)| format!("RW{}", runway_ident)), + approach_type: ApproachType::Fms, /* Set to an arbitrary value, will be overwritten once we + * find a row with a valid approach type (the first row in + * an approach will usually be a transition so it can not + * be used to find the approach type) */ + }) + } + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + if let Some(description_code) = &row.waypoint_description_code { + if description_code.chars().nth(2) == Some('M') { + missed_started = true; + } + } + + let leg = ProcedureLeg::from(row); + + match route_type.as_str() { + "A" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Transition leg was found without a transition identifier"), + &mut approach.transitions, + ), + "Z" => approach.missed_legs.push(leg), + x => { + if missed_started || x == "Z" { + approach.missed_legs.push(leg); + } else { + approach.approach_type = + serde_json::from_value(serde_json::Value::String(route_type)).unwrap(); + + approach.legs.push(leg) + } + } + } + + approaches + }) + .into_values() + .collect() +} + +pub(crate) fn map_approaches_v2(data: Vec) -> Vec { + let mut missed_started = false; + + data.into_iter() + .fold(HashMap::new(), |mut approaches, row| { + let approach = match approaches.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + missed_started = false; + + entry.insert(Approach { + ident: row.procedure_identifier.clone(), + transitions: Vec::new(), + legs: Vec::new(), + missed_legs: Vec::new(), + runway_ident: split_approach_ident(row.procedure_identifier.clone()) + .map(|(_, runway_ident, _)| format!("RW{}", runway_ident)), + approach_type: ApproachType::Fms, /* Set to an arbitrary value, will be overwritten once we + * find a row with a valid approach type (the first row in + * an approach will usually be a transition so it can not + * be used to find the approach type) */ + }) + } + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + if let Some(description_code) = &row.waypoint_description_code { + if description_code.chars().nth(2) == Some('M') { + missed_started = true; + } + } + + let leg = ProcedureLeg::from(row); + + match route_type.as_str() { + "A" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Transition leg was found without a transition identifier"), + &mut approach.transitions, + ), + "Z" => approach.missed_legs.push(leg), + x => { + if missed_started || x == "Z" { + approach.missed_legs.push(leg); + } else { + approach.approach_type = + serde_json::from_value(serde_json::Value::String(route_type)).unwrap(); + + approach.legs.push(leg) + } + } + } + + approaches + }) + .into_values() + .collect() +} diff --git a/src/database/src/output/procedure/arrival.rs b/src/database/src/output/procedure/arrival.rs index 8e5e3fcd..291cde83 100644 --- a/src/database/src/output/procedure/arrival.rs +++ b/src/database/src/output/procedure/arrival.rs @@ -1,95 +1,159 @@ -use std::collections::{hash_map::Entry, HashMap}; - -use serde::Serialize; - -use super::{apply_common_leg, apply_enroute_transition_leg, apply_runway_transition_leg, Transition}; -use crate::{output::procedure_leg::ProcedureLeg, sql_structs}; - -#[derive(Serialize)] -/// Represents an arrival procedure (STAR) for an airport. -/// -/// # Example -/// Basic querying: -/// ```rs -/// let database = Database::new(); -/// let approaches: Vec = database.get_arrivals_at_airport("KJFK"); -/// ``` -pub struct Arrival { - /// The `ident` uniquely identifies this arrival within the airport which it serves. - /// - /// While arrival identifiers may seem unique everywhere, it is possible for two airports to share a arrival or - /// have a arrival of the same name like Approaches - ident: String, - /// A list of the transitions which are available for this arrival. - enroute_transitions: Vec, - /// A list of legs which apply to all runways which this arrival serves. - /// - /// Keep in mind it is not common for this field to have any values as most arrivals consist only serve a single - /// runway, and will hence have a single runway transition and no `common_legs` - common_legs: Vec, - /// A list of runway transitions which are part of this Arrival. - /// - /// This field can be used to determine which runways this arrival serves, and is garunteed to always have at - /// least one value. - runway_transitions: Vec, -} - -/// Maps a list of arrival rows from the sqlite database into `Arrival` structs, by condensing them using -/// `procedure_identifier` and `transition_identifier` -/// -/// This function requires complete data for a single airport and the same ordering as the database provides by default. -/// -/// The recommended SQL query to load the neccesary data for this function is: -/// ```sql -/// SELECT * FROM tbl_stars WHERE airport_identifier = (?1) -/// ``` -pub(crate) fn map_arrivals(data: Vec, runways: Vec) -> Vec { - data.into_iter() - .fold(HashMap::new(), |mut arrivals, row| { - let arrival = match arrivals.entry(row.procedure_identifier.clone()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => entry.insert(Arrival { - ident: row.procedure_identifier.clone(), - enroute_transitions: Vec::new(), - common_legs: Vec::new(), - runway_transitions: Vec::new(), - }), - }; - - let route_type = row.route_type.clone(); - let transition_identifier = row.transition_identifier.clone(); - - let leg = ProcedureLeg::from(row); - - // We want to ensure there is a runway transition for every single runway which this procedure serves, even - // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there - // need not be special logic for determining which runways are compatible - match route_type.as_str() { - "1" | "4" | "7" | "F" => apply_enroute_transition_leg( - leg, - transition_identifier.expect("Enroute transition leg was found without a transition identifier"), - &mut arrival.enroute_transitions, - ), - // These route types are for common legs - "2" | "5" | "8" | "M" => apply_common_leg( - leg, - transition_identifier, - &mut arrival.runway_transitions, - &mut arrival.common_legs, - &runways, - ), - // These route types are for runway transitions - "3" | "6" | "9" | "S" => apply_runway_transition_leg( - leg, - transition_identifier.expect("Runway transition leg was found without a transition identifier"), - &mut arrival.runway_transitions, - &runways, - ), - _ => unreachable!(), - } - - arrivals - }) - .into_values() - .collect() -} +use std::collections::{hash_map::Entry, HashMap}; + +use serde::Serialize; + +use super::{ + apply_common_leg, apply_common_leg_v2, apply_enroute_transition_leg, + apply_runway_transition_leg, apply_runway_transition_leg_v2, Transition, +}; +use crate::{output::procedure_leg::ProcedureLeg, sql_structs, v2}; + +#[derive(Serialize)] +/// Represents an arrival procedure (STAR) for an airport. +/// +/// # Example +/// Basic querying: +/// ```rs +/// let database = Database::new(); +/// let approaches: Vec = database.get_arrivals_at_airport("KJFK"); +/// ``` +pub struct Arrival { + /// The `ident` uniquely identifies this arrival within the airport which it serves. + /// + /// While arrival identifiers may seem unique everywhere, it is possible for two airports to share a arrival or + /// have a arrival of the same name like Approaches + ident: String, + /// A list of the transitions which are available for this arrival. + enroute_transitions: Vec, + /// A list of legs which apply to all runways which this arrival serves. + /// + /// Keep in mind it is not common for this field to have any values as most arrivals consist only serve a single + /// runway, and will hence have a single runway transition and no `common_legs` + common_legs: Vec, + /// A list of runway transitions which are part of this Arrival. + /// + /// This field can be used to determine which runways this arrival serves, and is garunteed to always have at + /// least one value. + runway_transitions: Vec, +} + +/// Maps a list of arrival rows from the sqlite database into `Arrival` structs, by condensing them using +/// `procedure_identifier` and `transition_identifier` +/// +/// This function requires complete data for a single airport and the same ordering as the database provides by default. +/// +/// The recommended SQL query to load the neccesary data for this function is: +/// ```sql +/// SELECT * FROM tbl_stars WHERE airport_identifier = (?1) +/// ``` +pub(crate) fn map_arrivals( + data: Vec, + runways: Vec, +) -> Vec { + data.into_iter() + .fold(HashMap::new(), |mut arrivals, row| { + let arrival = match arrivals.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(Arrival { + ident: row.procedure_identifier.clone(), + enroute_transitions: Vec::new(), + common_legs: Vec::new(), + runway_transitions: Vec::new(), + }), + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + let leg = ProcedureLeg::from(row); + + // We want to ensure there is a runway transition for every single runway which this procedure serves, even + // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there + // need not be special logic for determining which runways are compatible + match route_type.as_str() { + "1" | "4" | "7" | "F" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Enroute transition leg was found without a transition identifier"), + &mut arrival.enroute_transitions, + ), + // These route types are for common legs + "2" | "5" | "8" | "M" => apply_common_leg( + leg, + transition_identifier, + &mut arrival.runway_transitions, + &mut arrival.common_legs, + &runways, + ), + // These route types are for runway transitions + "3" | "6" | "9" | "S" => apply_runway_transition_leg( + leg, + transition_identifier + .expect("Runway transition leg was found without a transition identifier"), + &mut arrival.runway_transitions, + &runways, + ), + _ => unreachable!(), + } + + arrivals + }) + .into_values() + .collect() +} + +pub(crate) fn map_arrivals_v2( + data: Vec, + runways: Vec, +) -> Vec { + data.into_iter() + .fold(HashMap::new(), |mut arrivals, row| { + let arrival = match arrivals.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(Arrival { + ident: row.procedure_identifier.clone(), + enroute_transitions: Vec::new(), + common_legs: Vec::new(), + runway_transitions: Vec::new(), + }), + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + let leg = ProcedureLeg::from(row); + + // We want to ensure there is a runway transition for every single runway which this procedure serves, even + // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there + // need not be special logic for determining which runways are compatible + match route_type.as_str() { + "1" | "4" | "7" | "F" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Enroute transition leg was found without a transition identifier"), + &mut arrival.enroute_transitions, + ), + // These route types are for common legs + "2" | "5" | "8" | "M" => apply_common_leg_v2( + leg, + transition_identifier, + &mut arrival.runway_transitions, + &mut arrival.common_legs, + &runways, + ), + // These route types are for runway transitions + "3" | "6" | "9" | "S" => apply_runway_transition_leg_v2( + leg, + transition_identifier + .expect("Runway transition leg was found without a transition identifier"), + &mut arrival.runway_transitions, + &runways, + ), + _ => unreachable!(), + } + + arrivals + }) + .into_values() + .collect() +} diff --git a/src/database/src/output/procedure/departure.rs b/src/database/src/output/procedure/departure.rs index 3ee0bc17..f474c84c 100644 --- a/src/database/src/output/procedure/departure.rs +++ b/src/database/src/output/procedure/departure.rs @@ -1,91 +1,158 @@ -use std::collections::{hash_map::Entry, HashMap}; - -use serde::Serialize; - -use super::{apply_common_leg, apply_enroute_transition_leg, apply_runway_transition_leg, Transition}; -use crate::{output::procedure_leg::ProcedureLeg, sql_structs}; - -#[derive(Serialize)] -pub struct Departure { - /// The `ident` uniquely identifies this arrival within the airport which it serves. - /// - /// While departure identifiers may seem unique everywhere, it is possible for two airports to share a departure or - /// have a departure of the same name like Approaches - ident: String, - /// A list of runway transitions which are part of this departure. - /// - /// This field can be used to determine which runways this departure serves, and is garunteed to always have at - /// least one value. - runway_transitions: Vec, - /// A list of legs which apply to all runways which this departure serves. - /// - /// Keep in mind it is not common for this field to have any values as most departure consist only serve a single - /// runway, and will hence have a single runway transition and no `common_legs` - common_legs: Vec, - /// A list of the transitions which are available for this arrival. - enroute_transitions: Vec, - engine_out_legs: Vec, -} - -/// Maps a list of departure rows from the sqlite database into `Departure` structs, by condensing them using -/// `procedure_identifier` and `transition_identifier` -/// -/// This function requires complete data for a single airport and the same ordering as the database provides by default. -/// -/// The recommended SQL query to load the neccesary data for this function is: -/// ```sql -/// SELECT * FROM tbl_sids WHERE airport_identifier = (?1) -/// ``` -pub(crate) fn map_departures(data: Vec, runways: Vec) -> Vec { - data.into_iter() - .fold(HashMap::new(), |mut departures, row| { - let departure = match departures.entry(row.procedure_identifier.clone()) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => entry.insert(Departure { - ident: row.procedure_identifier.clone(), - runway_transitions: Vec::new(), - common_legs: Vec::new(), - enroute_transitions: Vec::new(), - engine_out_legs: Vec::new(), - }), - }; - - let route_type = row.route_type.clone(); - let transition_identifier = row.transition_identifier.clone(); - - let leg = ProcedureLeg::from(row); - - // We want to ensure there is a runway transition for every single runway which this procedure serves, even - // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there - // need not be special logic for determining which runways are compatible - match route_type.as_str() { - "0" => departure.engine_out_legs.push(leg), - // These route types are for runway transitions - "1" | "4" | "F" | "T" => apply_runway_transition_leg( - leg, - transition_identifier.expect("Runway transition leg was found without a transition identifier"), - &mut departure.runway_transitions, - &runways, - ), - // These route types are for common legs - "2" | "5" | "M" => apply_common_leg( - leg, - transition_identifier, - &mut departure.runway_transitions, - &mut departure.common_legs, - &runways, - ), - // These route types are for enroute transitions - "3" | "6" | "S" | "V" => apply_enroute_transition_leg( - leg, - transition_identifier.expect("Enroute transition leg was found without a transition identifier"), - &mut departure.enroute_transitions, - ), - _ => unreachable!(), - } - - departures - }) - .into_values() - .collect() -} +use std::collections::{hash_map::Entry, HashMap}; + +use serde::Serialize; + +use super::{ + apply_common_leg, apply_common_leg_v2, apply_enroute_transition_leg, + apply_runway_transition_leg, apply_runway_transition_leg_v2, Transition, +}; +use crate::{output::procedure_leg::ProcedureLeg, sql_structs, v2}; + +#[derive(Serialize)] +pub struct Departure { + /// The `ident` uniquely identifies this arrival within the airport which it serves. + /// + /// While departure identifiers may seem unique everywhere, it is possible for two airports to share a departure or + /// have a departure of the same name like Approaches + ident: String, + /// A list of runway transitions which are part of this departure. + /// + /// This field can be used to determine which runways this departure serves, and is garunteed to always have at + /// least one value. + runway_transitions: Vec, + /// A list of legs which apply to all runways which this departure serves. + /// + /// Keep in mind it is not common for this field to have any values as most departure consist only serve a single + /// runway, and will hence have a single runway transition and no `common_legs` + common_legs: Vec, + /// A list of the transitions which are available for this arrival. + enroute_transitions: Vec, + engine_out_legs: Vec, +} + +/// Maps a list of departure rows from the sqlite database into `Departure` structs, by condensing them using +/// `procedure_identifier` and `transition_identifier` +/// +/// This function requires complete data for a single airport and the same ordering as the database provides by default. +/// +/// The recommended SQL query to load the neccesary data for this function is: +/// ```sql +/// SELECT * FROM tbl_sids WHERE airport_identifier = (?1) +/// ``` +pub(crate) fn map_departures( + data: Vec, + runways: Vec, +) -> Vec { + data.into_iter() + .fold(HashMap::new(), |mut departures, row| { + let departure = match departures.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(Departure { + ident: row.procedure_identifier.clone(), + runway_transitions: Vec::new(), + common_legs: Vec::new(), + enroute_transitions: Vec::new(), + engine_out_legs: Vec::new(), + }), + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + let leg = ProcedureLeg::from(row); + + // We want to ensure there is a runway transition for every single runway which this procedure serves, even + // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there + // need not be special logic for determining which runways are compatible + match route_type.as_str() { + "0" => departure.engine_out_legs.push(leg), + // These route types are for runway transitions + "1" | "4" | "F" | "T" => apply_runway_transition_leg( + leg, + transition_identifier + .expect("Runway transition leg was found without a transition identifier"), + &mut departure.runway_transitions, + &runways, + ), + // These route types are for common legs + "2" | "5" | "M" => apply_common_leg( + leg, + transition_identifier, + &mut departure.runway_transitions, + &mut departure.common_legs, + &runways, + ), + // These route types are for enroute transitions + "3" | "6" | "S" | "V" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Enroute transition leg was found without a transition identifier"), + &mut departure.enroute_transitions, + ), + _ => unreachable!(), + } + + departures + }) + .into_values() + .collect() +} + +pub(crate) fn map_departures_v2( + data: Vec, + runways: Vec, +) -> Vec { + data.into_iter() + .fold(HashMap::new(), |mut departures, row| { + let departure = match departures.entry(row.procedure_identifier.clone()) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => entry.insert(Departure { + ident: row.procedure_identifier.clone(), + runway_transitions: Vec::new(), + common_legs: Vec::new(), + enroute_transitions: Vec::new(), + engine_out_legs: Vec::new(), + }), + }; + + let route_type = row.route_type.clone(); + let transition_identifier = row.transition_identifier.clone(); + + let leg = ProcedureLeg::from(row); + + // We want to ensure there is a runway transition for every single runway which this procedure serves, even + // if the procedure does not differ between runways This makes it very easy to implement in an FMS as there + // need not be special logic for determining which runways are compatible + match route_type.as_str() { + "0" => departure.engine_out_legs.push(leg), + // These route types are for runway transitions + "1" | "4" | "F" | "T" => apply_runway_transition_leg_v2( + leg, + transition_identifier + .expect("Runway transition leg was found without a transition identifier"), + &mut departure.runway_transitions, + &runways, + ), + // These route types are for common legs + "2" | "5" | "M" => apply_common_leg_v2( + leg, + transition_identifier, + &mut departure.runway_transitions, + &mut departure.common_legs, + &runways, + ), + // These route types are for enroute transitions + "3" | "6" | "S" | "V" => apply_enroute_transition_leg( + leg, + transition_identifier + .expect("Enroute transition leg was found without a transition identifier"), + &mut departure.enroute_transitions, + ), + _ => unreachable!(), + } + + departures + }) + .into_values() + .collect() +} diff --git a/src/database/src/output/procedure/mod.rs b/src/database/src/output/procedure/mod.rs index 48b51ed5..97bdb79c 100644 --- a/src/database/src/output/procedure/mod.rs +++ b/src/database/src/output/procedure/mod.rs @@ -1,147 +1,263 @@ -use serde::Serialize; - -use super::procedure_leg::ProcedureLeg; -use crate::sql_structs; - -pub mod approach; -pub mod arrival; -pub mod departure; - -#[derive(Serialize)] -pub struct Transition { - ident: String, - legs: Vec, -} - -/// A helper function which returns a mutable reference to an item in a vector if it can be found using the `condition`, -/// or inserts a new item `val` into the vector and returns a mutable reference to it. -fn mut_find_or_insert bool>(vec: &mut Vec, condition: P, val: T) -> &mut T { - if let Some(index) = vec.iter().position(condition) { - &mut vec[index] - } else { - vec.push(val); - - vec.last_mut().unwrap() - } -} - -/// Applies the neccesary logic for adding a leg with an enroute transition route type into a procedure -pub(self) fn apply_enroute_transition_leg( - leg: ProcedureLeg, transition_identifier: String, enroute_transitions: &mut Vec, -) { - let transition = mut_find_or_insert( - enroute_transitions, - |transition| transition.ident == transition_identifier, - Transition { - ident: transition_identifier.to_string(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg); -} - -/// Applies the neccesary logic for adding a leg with a common leg route type into a procedure -pub(self) fn apply_common_leg( - leg: ProcedureLeg, transition_identifier: Option, runway_transitions: &mut Vec, - common_legs: &mut Vec, runways: &Vec, -) { - // Common legs can still have a transition identifier, meaning that this procedure is only for - // specific runways, but with the same legs for each runway. - // - // If it is not present, it means there are seperate runway transitions for each runway after these - // common legs - if let Some(transition_identifier) = transition_identifier { - // If the transition identifier is `ALL`, this means that this procedure is for all runways and - // has exactly the same legs for each runway, so we insert a runway transition for every runway - // at the airport - if transition_identifier == "ALL" { - for runway in runways.iter() { - let transition = mut_find_or_insert( - runway_transitions, - |transition| transition.ident == runway.runway_identifier, - Transition { - ident: runway.runway_identifier.clone(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg.clone()); - } - // When the identifier ends with B, this procedure is for all runways with that number - } else if transition_identifier.chars().nth(4) == Some('B') { - let target_runways = runways - .iter() - .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); - - for runway in target_runways { - let transition = mut_find_or_insert( - runway_transitions, - |transition| transition.ident == runway.runway_identifier, - Transition { - ident: runway.runway_identifier.clone(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg.clone()); - } - // In this case, the transition identifier is for a specific runway, so we insert it as a runway - // transition to indicate which runway this procedure is specifically for - } else { - let transition = mut_find_or_insert( - runway_transitions, - |transition| transition.ident == transition_identifier, - Transition { - ident: transition_identifier.to_string(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg); - } - // When there is no transiton identifier, that means there are seperate runway transitions, so these - // legs should actually be inserted as common legs - } else { - common_legs.push(leg); - } -} - -/// Applies the neccesary logic for adding a leg with a runway transition route type into a procedure -pub(self) fn apply_runway_transition_leg( - leg: ProcedureLeg, transition_identifier: String, runway_transitions: &mut Vec, - runways: &Vec, -) { - // If transition identifier ends in B, it means this transition serves all runways with the same - // number. To make this easier to use in an FMS, we duplicate the transitions for all runways which - // it serves - if transition_identifier.chars().nth(4) == Some('B') { - let target_runways = runways - .iter() - .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); - - for runway in target_runways { - let transition = mut_find_or_insert( - runway_transitions, - |transition| transition.ident == runway.runway_identifier, - Transition { - ident: runway.runway_identifier.clone(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg.clone()); - } - } else { - let transition = mut_find_or_insert( - runway_transitions, - |transition| transition.ident == transition_identifier, - Transition { - ident: transition_identifier.to_string(), - legs: Vec::new(), - }, - ); - - transition.legs.push(leg.clone()); - } -} +use serde::Serialize; + +use super::procedure_leg::ProcedureLeg; +use crate::{sql_structs, v2}; + +pub mod approach; +pub mod arrival; +pub mod departure; + +#[derive(Serialize)] +pub struct Transition { + ident: String, + legs: Vec, +} + +/// A helper function which returns a mutable reference to an item in a vector if it can be found using the `condition`, +/// or inserts a new item `val` into the vector and returns a mutable reference to it. +fn mut_find_or_insert bool>(vec: &mut Vec, condition: P, val: T) -> &mut T { + if let Some(index) = vec.iter().position(condition) { + &mut vec[index] + } else { + vec.push(val); + + vec.last_mut().unwrap() + } +} + +/// Applies the neccesary logic for adding a leg with an enroute transition route type into a procedure +fn apply_enroute_transition_leg( + leg: ProcedureLeg, + transition_identifier: String, + enroute_transitions: &mut Vec, +) { + let transition = mut_find_or_insert( + enroute_transitions, + |transition| transition.ident == transition_identifier, + Transition { + ident: transition_identifier.to_string(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg); +} + +/// Applies the neccesary logic for adding a leg with a common leg route type into a procedure +fn apply_common_leg( + leg: ProcedureLeg, + transition_identifier: Option, + runway_transitions: &mut Vec, + common_legs: &mut Vec, + runways: &[sql_structs::Runways], +) { + // Common legs can still have a transition identifier, meaning that this procedure is only for + // specific runways, but with the same legs for each runway. + // + // If it is not present, it means there are seperate runway transitions for each runway after these + // common legs + if let Some(transition_identifier) = transition_identifier { + // If the transition identifier is `ALL`, this means that this procedure is for all runways and + // has exactly the same legs for each runway, so we insert a runway transition for every runway + // at the airport + if transition_identifier == "ALL" { + for runway in runways.iter() { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + // When the identifier ends with B, this procedure is for all runways with that number + } else if transition_identifier.chars().nth(4) == Some('B') { + let target_runways = runways + .iter() + .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); + + for runway in target_runways { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + // In this case, the transition identifier is for a specific runway, so we insert it as a runway + // transition to indicate which runway this procedure is specifically for + } else { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == transition_identifier, + Transition { + ident: transition_identifier.to_string(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg); + } + // When there is no transiton identifier, that means there are seperate runway transitions, so these + // legs should actually be inserted as common legs + } else { + common_legs.push(leg); + } +} + +/// Applies the neccesary logic for adding a leg with a common leg route type into a procedure +fn apply_common_leg_v2( + leg: ProcedureLeg, + transition_identifier: Option, + runway_transitions: &mut Vec, + common_legs: &mut Vec, + runways: &[v2::sql_structs::Runways], +) { + // Common legs can still have a transition identifier, meaning that this procedure is only for + // specific runways, but with the same legs for each runway. + // + // If it is not present, it means there are seperate runway transitions for each runway after these + // common legs + if let Some(transition_identifier) = transition_identifier { + // If the transition identifier is `ALL`, this means that this procedure is for all runways and + // has exactly the same legs for each runway, so we insert a runway transition for every runway + // at the airport + if transition_identifier == "ALL" { + for runway in runways.iter() { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + // When the identifier ends with B, this procedure is for all runways with that number + } else if transition_identifier.chars().nth(4) == Some('B') { + let target_runways = runways + .iter() + .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); + + for runway in target_runways { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + // In this case, the transition identifier is for a specific runway, so we insert it as a runway + // transition to indicate which runway this procedure is specifically for + } else { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == transition_identifier, + Transition { + ident: transition_identifier.to_string(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg); + } + // When there is no transiton identifier, that means there are seperate runway transitions, so these + // legs should actually be inserted as common legs + } else { + common_legs.push(leg); + } +} + +/// Applies the neccesary logic for adding a leg with a runway transition route type into a procedure +fn apply_runway_transition_leg( + leg: ProcedureLeg, + transition_identifier: String, + runway_transitions: &mut Vec, + runways: &[sql_structs::Runways], +) { + // If transition identifier ends in B, it means this transition serves all runways with the same + // number. To make this easier to use in an FMS, we duplicate the transitions for all runways which + // it serves + if transition_identifier.chars().nth(4) == Some('B') { + let target_runways = runways + .iter() + .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); + + for runway in target_runways { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + } else { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == transition_identifier, + Transition { + ident: transition_identifier.to_string(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } +} + +fn apply_runway_transition_leg_v2( + leg: ProcedureLeg, + transition_identifier: String, + runway_transitions: &mut Vec, + runways: &[v2::sql_structs::Runways], +) { + // If transition identifier ends in B, it means this transition serves all runways with the same + // number. To make this easier to use in an FMS, we duplicate the transitions for all runways which + // it serves + if transition_identifier.chars().nth(4) == Some('B') { + let target_runways = runways + .iter() + .filter(|runway| runway.runway_identifier[0..4] == transition_identifier[0..4]); + + for runway in target_runways { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == runway.runway_identifier, + Transition { + ident: runway.runway_identifier.clone(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } + } else { + let transition = mut_find_or_insert( + runway_transitions, + |transition| transition.ident == transition_identifier, + Transition { + ident: transition_identifier.to_string(), + legs: Vec::new(), + }, + ); + + transition.legs.push(leg.clone()); + } +} diff --git a/src/database/src/output/procedure_leg.rs b/src/database/src/output/procedure_leg.rs index f03e77bf..05acdc15 100644 --- a/src/database/src/output/procedure_leg.rs +++ b/src/database/src/output/procedure_leg.rs @@ -1,147 +1,228 @@ -use serde::Serialize; - -use super::fix::Fix; -use crate::{ - enums::{AltitudeDescriptor, LegType, SpeedDescriptor, TurnDirection}, - math::{Degrees, Feet, Knots, Minutes, NauticalMiles}, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone)] -pub struct AltitudeContstraint { - altitude1: Feet, - altitude2: Option, - descriptor: AltitudeDescriptor, -} - -#[derive(Serialize, Clone)] -pub struct SpeedConstraint { - value: Knots, - descriptor: SpeedDescriptor, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone)] -/// Represents a leg as part of a `Departure`, `Arrival`, or `Approach`. -pub struct ProcedureLeg { - /// Whether or not this termination of this leg should be flown directly over - overfly: bool, - - /// The type of leg - leg_type: LegType, - - /// The altitude constraint of this leg. - /// - /// This is a required field for any `XA` or `PI` leg - altitude: Option, - - /// The speed constraint of this leg - speed: Option, - - /// The vertical angle constraint of this leg - vertical_angle: Option, - - /// The rnp (required navigational performance) of this leg in nautical miles - rnp: Option, - - /// The fix that this leg terminates at - /// - /// This is a required field for any `XF`, `FX`, `HX` or `PI` leg. - fix: Option, - - /// The fix that is used as the associated radio navigational aid for this leg. - /// - /// This is a required field for any `AF`, `CD`, `CF`, `CR`, `FX`, `PI`, `VD`, or `VR` leg - recommended_navaid: Option, - - /// The magnetic bearing from the `recommended_navaid` to the `fix`, or the magnetic radial from the - /// `recommended_navaid` to intersect with in a `XR` leg - theta: Option, - - /// The distance in nautical miles from the `recommended_navaid` to the `fix` - rho: Option, - - /// The magnetic course to be flown for legs which are defined by a course or heading to a termination, or the - /// radial from the `recomended_navaid` to the expected start location on an `AF` leg - magnetic_course: Option, - - /// The length of the leg in nautical miles - length: Option, - - /// The time to be used when flying a hold leg, if any - length_time: Option, - - /// The constraint on the direction of turn to be used when flying this leg - turn_direction: Option, - - /// The center of the arc to be flown for an `RF` leg - arc_center_fix: Option, - - /// The radius of the arc to be flown for an `RF` leg - arc_radius: Option, -} - -impl From for ProcedureLeg { - fn from(leg: sql_structs::Procedures) -> Self { - ProcedureLeg { - overfly: leg - .waypoint_description_code - .map_or(false, |x| x.chars().nth(1) == Some('Y')), - altitude: leg.altitude1.map(|altitude1| AltitudeContstraint { - altitude1, - altitude2: leg.altitude2, - descriptor: leg.altitude_description.unwrap_or(AltitudeDescriptor::AtAlt1), - }), - speed: leg.speed_limit.map(|speed| SpeedConstraint { - value: speed, - descriptor: leg.speed_limit_description.unwrap_or(SpeedDescriptor::Mandatory), - }), - vertical_angle: leg.vertical_angle, - rnp: leg.rnp, - fix: if !leg.id.is_empty() { - Some(Fix::from_row_data( - leg.waypoint_latitude.unwrap(), - leg.waypoint_longitude.unwrap(), - leg.id, - )) - } else { - None - }, - recommended_navaid: if !leg.recommanded_id.is_empty() { - Some(Fix::from_row_data( - leg.recommanded_navaid_latitude.unwrap(), - leg.recommanded_navaid_longitude.unwrap(), - leg.recommanded_id, - )) - } else { - None - }, - theta: leg.theta, - rho: leg.rho, - magnetic_course: leg.magnetic_course, - length: if leg.distance_time == Some("D".to_string()) { - leg.route_distance_holding_distance_time - } else { - None - }, - length_time: if leg.distance_time == Some("T".to_string()) { - leg.route_distance_holding_distance_time - } else { - None - }, - turn_direction: leg.turn_direction, - arc_center_fix: if !leg.center_id.is_empty() { - Some(Fix::from_row_data( - leg.center_waypoint_latitude.unwrap(), - leg.center_waypoint_longitude.unwrap(), - leg.center_id, - )) - } else { - None - }, - arc_radius: leg.arc_radius, - leg_type: leg.path_termination, - } - } -} +use serde::Serialize; + +use super::fix::Fix; +use crate::{ + enums::{AltitudeDescriptor, LegType, SpeedDescriptor, TurnDirection}, + math::{Degrees, Feet, Knots, Minutes, NauticalMiles}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone)] +pub struct AltitudeContstraint { + altitude1: Feet, + altitude2: Option, + descriptor: AltitudeDescriptor, +} + +#[derive(Serialize, Clone)] +pub struct SpeedConstraint { + value: Knots, + descriptor: SpeedDescriptor, +} + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone)] +/// Represents a leg as part of a `Departure`, `Arrival`, or `Approach`. +pub struct ProcedureLeg { + /// Whether or not this termination of this leg should be flown directly over + overfly: bool, + + /// The type of leg + leg_type: LegType, + + /// The altitude constraint of this leg. + /// + /// This is a required field for any `XA` or `PI` leg + altitude: Option, + + /// The speed constraint of this leg + speed: Option, + + /// The vertical angle constraint of this leg + vertical_angle: Option, + + /// The rnp (required navigational performance) of this leg in nautical miles + rnp: Option, + + /// The fix that this leg terminates at + /// + /// This is a required field for any `XF`, `FX`, `HX` or `PI` leg. + fix: Option, + + /// The fix that is used as the associated radio navigational aid for this leg. + /// + /// This is a required field for any `AF`, `CD`, `CF`, `CR`, `FX`, `PI`, `VD`, or `VR` leg + recommended_navaid: Option, + + /// The magnetic bearing from the `recommended_navaid` to the `fix`, or the magnetic radial from the + /// `recommended_navaid` to intersect with in a `XR` leg + theta: Option, + + /// The distance in nautical miles from the `recommended_navaid` to the `fix` + rho: Option, + + /// The magnetic course to be flown for legs which are defined by a course or heading to a termination, or the + /// radial from the `recomended_navaid` to the expected start location on an `AF` leg + magnetic_course: Option, + + /// The length of the leg in nautical miles + length: Option, + + /// The time to be used when flying a hold leg, if any + length_time: Option, + + /// The constraint on the direction of turn to be used when flying this leg + turn_direction: Option, + + /// The center of the arc to be flown for an `RF` leg + arc_center_fix: Option, + + /// The radius of the arc to be flown for an `RF` leg + arc_radius: Option, +} + +impl From for ProcedureLeg { + fn from(leg: sql_structs::Procedures) -> Self { + ProcedureLeg { + overfly: leg + .waypoint_description_code + .map_or(false, |x| x.chars().nth(1) == Some('Y')), + altitude: leg.altitude1.map(|altitude1| AltitudeContstraint { + altitude1, + altitude2: leg.altitude2, + descriptor: leg + .altitude_description + .unwrap_or(AltitudeDescriptor::AtAlt1), + }), + speed: leg.speed_limit.map(|speed| SpeedConstraint { + value: speed, + descriptor: leg + .speed_limit_description + .unwrap_or(SpeedDescriptor::Mandatory), + }), + vertical_angle: leg.vertical_angle, + rnp: leg.rnp, + fix: if !leg.id.is_empty() { + Some(Fix::from_row_data( + leg.waypoint_latitude.unwrap(), + leg.waypoint_longitude.unwrap(), + leg.id, + )) + } else { + None + }, + recommended_navaid: if !leg.recommanded_id.is_empty() { + Some(Fix::from_row_data( + leg.recommanded_navaid_latitude.unwrap(), + leg.recommanded_navaid_longitude.unwrap(), + leg.recommanded_id, + )) + } else { + None + }, + theta: leg.theta, + rho: leg.rho, + magnetic_course: leg.magnetic_course, + length: if leg.distance_time == Some("D".to_string()) { + leg.route_distance_holding_distance_time + } else { + None + }, + length_time: if leg.distance_time == Some("T".to_string()) { + leg.route_distance_holding_distance_time + } else { + None + }, + turn_direction: leg.turn_direction, + arc_center_fix: if !leg.center_id.is_empty() { + Some(Fix::from_row_data( + leg.center_waypoint_latitude.unwrap(), + leg.center_waypoint_longitude.unwrap(), + leg.center_id, + )) + } else { + None + }, + arc_radius: leg.arc_radius, + leg_type: leg.path_termination, + } + } +} + +impl From for ProcedureLeg { + fn from(leg: v2::sql_structs::Procedures) -> Self { + ProcedureLeg { + overfly: leg + .waypoint_description_code + .map_or(false, |x| x.chars().nth(1) == Some('Y')), + altitude: leg.altitude1.map(|altitude1| AltitudeContstraint { + altitude1, + altitude2: leg.altitude2, + descriptor: leg + .altitude_description + .unwrap_or(AltitudeDescriptor::AtAlt1), + }), + speed: leg.speed_limit.map(|speed| SpeedConstraint { + value: speed, + descriptor: leg + .speed_limit_description + .unwrap_or(SpeedDescriptor::Mandatory), + }), + vertical_angle: leg.vertical_angle, + rnp: leg.rnp, + fix: if leg.waypoint_identifier.is_some() { + Some(Fix::from_row_data_v2( + leg.waypoint_latitude.unwrap(), + leg.waypoint_longitude.unwrap(), + leg.waypoint_identifier.unwrap(), + leg.waypoint_icao_code.unwrap(), + Some(leg.airport_identifier.clone()), + leg.waypoint_ref_table, + )) + } else { + None + }, + recommended_navaid: if leg.recommended_navaid.is_some() { + Some(Fix::from_row_data_v2( + leg.recommended_navaid_latitude.unwrap(), + leg.recommended_navaid_longitude.unwrap(), + leg.recommended_navaid.unwrap(), + leg.recommended_navaid_icao_code.unwrap(), + Some(leg.airport_identifier.clone()), + leg.recommended_navaid_ref_table, + )) + } else { + None + }, + theta: leg.theta, + rho: leg.rho, + magnetic_course: None, + length: if leg.route_distance_holding_distance_time == Some("D".to_string()) { + leg.distance_time + } else { + None + }, + length_time: if leg.route_distance_holding_distance_time == Some("T".to_string()) { + leg.distance_time + } else { + None + }, + turn_direction: leg.turn_direction, + arc_center_fix: if leg.center_waypoint.is_some() { + Some(Fix::from_row_data_v2( + leg.center_waypoint_latitude.unwrap(), + leg.center_waypoint_longitude.unwrap(), + leg.center_waypoint.unwrap(), + leg.center_waypoint_icao_code.unwrap(), + Some(leg.airport_identifier), + leg.center_waypoint_ref_table, + )) + } else { + None + }, + arc_radius: leg.arc_radius, + leg_type: leg.path_termination, + } + } +} diff --git a/src/database/src/output/runway.rs b/src/database/src/output/runway.rs index c785a97e..888f8d07 100644 --- a/src/database/src/output/runway.rs +++ b/src/database/src/output/runway.rs @@ -1,49 +1,80 @@ -use serde::Serialize; - -use crate::{ - math::{Coordinates, Degrees, Feet}, - sql_structs, -}; - -#[derive(Serialize, Clone)] -pub struct RunwayThreshold { - /// The identifier of this runway, such as `RW18L` or `RW36R` - pub ident: String, - /// The icao prefix of the region that this runway is in. - pub icao_code: String, - /// The length of this runway in feet - pub length: Feet, - /// The width of this runway in feet - pub width: Feet, - /// The true bearing of this runway in degrees - pub true_bearing: Degrees, - /// The magnetic bearing of this runway in degrees. - /// - /// This field is rounded to the nearest degree - pub magnetic_bearing: Degrees, - /// The gradient of this runway in degrees - pub gradient: Degrees, - /// The geographic location of the landing threshold of this runway - pub location: Coordinates, - /// The elevation of the landing threshold of this runway in feet - pub elevation: Feet, -} - -impl From for RunwayThreshold { - fn from(runway: sql_structs::Runways) -> Self { - Self { - ident: runway.runway_identifier, - icao_code: runway.icao_code, - length: runway.runway_length, - width: runway.runway_width, - true_bearing: runway.runway_true_bearing, - magnetic_bearing: runway.runway_magnetic_bearing, - gradient: runway.runway_gradient, - location: Coordinates { - lat: runway.runway_latitude, - long: runway.runway_longitude, - }, - elevation: runway.landing_threshold_elevation, - } - } -} +use serde::Serialize; + +use crate::{ + enums::{RunwayLights, RunwaySurface, TrafficPattern}, + math::{Coordinates, Degrees, Feet}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Clone, Default)] +pub struct RunwayThreshold { + /// The identifier of this runway, such as `RW18L` or `RW36R` + pub ident: String, + /// The icao prefix of the region that this runway is in. + pub icao_code: String, + /// The length of this runway in feet + pub length: Feet, + /// The width of this runway in feet + pub width: Feet, + /// The true bearing of this runway in degrees + pub true_bearing: Degrees, + /// The magnetic bearing of this runway in degrees. + /// + /// This field is rounded to the nearest degree + pub magnetic_bearing: Degrees, + /// The gradient of this runway in degrees + pub gradient: Degrees, + /// The geographic location of the landing threshold of this runway + pub location: Coordinates, + /// The elevation of the landing threshold of this runway in feet + pub elevation: Feet, + /// Whether or not the runway has lights (v2 only) + pub lights: Option, + /// Material that the runway is made out of (v2 only) + pub surface: Option, + /// The traffic pattern of the runway (v2 only) + pub traffic_pattern: Option, +} + +impl From for RunwayThreshold { + fn from(runway: sql_structs::Runways) -> Self { + Self { + ident: runway.runway_identifier, + icao_code: runway.icao_code, + length: runway.runway_length, + width: runway.runway_width, + true_bearing: runway.runway_true_bearing, + magnetic_bearing: runway.runway_magnetic_bearing, + gradient: runway.runway_gradient, + location: Coordinates { + lat: runway.runway_latitude, + long: runway.runway_longitude, + }, + elevation: runway.landing_threshold_elevation, + ..Default::default() + } + } +} + +impl From for RunwayThreshold { + fn from(runway: v2::sql_structs::Runways) -> Self { + Self { + ident: runway.runway_identifier, + icao_code: runway.icao_code.unwrap_or("UNK".to_string()), + length: runway.runway_length, + width: runway.runway_width, + true_bearing: runway.runway_true_bearing.unwrap_or_default(), + magnetic_bearing: runway.runway_magnetic_bearing.unwrap_or_default(), + gradient: runway.runway_gradient.unwrap_or_default(), + location: Coordinates { + lat: runway.runway_latitude.unwrap_or_default(), + long: runway.runway_longitude.unwrap_or_default(), + }, + elevation: runway.landing_threshold_elevation, + surface: runway.surface_code, + traffic_pattern: runway.traffic_pattern, + lights: runway.runway_lights, + } + } +} diff --git a/src/database/src/output/vhf_navaid.rs b/src/database/src/output/vhf_navaid.rs index a33ffc74..ae8dbaf8 100644 --- a/src/database/src/output/vhf_navaid.rs +++ b/src/database/src/output/vhf_navaid.rs @@ -1,45 +1,85 @@ -use serde::Serialize; - -use crate::{ - math::{Coordinates, Degrees, MegaHertz}, - sql_structs, -}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize)] -pub struct VhfNavaid { - /// Represents the geographic region in which this VhfNavaid is located - pub area_code: String, - /// The identifier of the airport that this VhfNavaid is associated with, if any - pub airport_ident: Option, - /// The icao prefix of the region that this VhfNavaid is in. - pub icao_code: String, - /// The identifier of the VOR station used in this VhfNavaid (not unique), such as `ITA` or `NZ` - pub ident: String, - /// The formal name of the VOR station used in this VhfNavaid such as `NARSARSUAQ` or `PHOENIX MCMURDO STATION` - pub name: String, - /// The frequency of this the VOR station used in this `VhfNavaid` in megahertz - pub frequency: MegaHertz, - /// The geographic location of the VOR station used in this `VhfNavaid` - pub location: Coordinates, - /// The magnetic declination of this `VhfNavaid` in degrees - pub station_declination: Option, -} - -impl From for VhfNavaid { - fn from(navaid: sql_structs::VhfNavaids) -> Self { - Self { - area_code: navaid.area_code, - airport_ident: navaid.airport_identifier, - icao_code: navaid.icao_code, - ident: navaid.vor_identifier, - name: navaid.vor_name, - frequency: navaid.vor_frequency, - location: Coordinates { - lat: navaid.vor_latitude, - long: navaid.vor_longitude, - }, - station_declination: navaid.station_declination, - } - } -} +use serde::Serialize; + +use crate::{ + math::{Coordinates, Degrees, MegaHertz, NauticalMiles}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct VhfNavaid { + /// Represents the geographic region in which this VhfNavaid is located + pub area_code: String, + /// Contenent of the navaid (v2 only) + pub continent: Option, + /// Country of the navaid (v2 only) + pub country: Option, + /// 3 Letter identifier describing the local horizontal identifier (v2 only) + pub datum_code: Option, + /// The identifier of the airport that this VhfNavaid is associated with, if any + pub airport_ident: Option, + /// The icao prefix of the region that this VhfNavaid is in. + pub icao_code: String, + /// The identifier of the VOR station used in this VhfNavaid (not unique), such as `ITA` or `NZ` + pub ident: String, + /// The formal name of the VOR station used in this VhfNavaid such as `NARSARSUAQ` or `PHOENIX MCMURDO STATION` + pub name: String, + /// The frequency of this the VOR station used in this `VhfNavaid` in megahertz + pub frequency: MegaHertz, + /// The geographic location of the VOR station used in this `VhfNavaid` + pub location: Coordinates, + /// The magnetic declination of this `VhfNavaid` in degrees + pub station_declination: Option, + /// Magnetic variation + pub magnetic_variation: Option, + /// VOR range (v2 only) + pub range: Option, +} + +impl From for VhfNavaid { + fn from(navaid: sql_structs::VhfNavaids) -> Self { + Self { + area_code: navaid.area_code, + airport_ident: navaid.airport_identifier, + icao_code: navaid.icao_code, + ident: navaid.vor_identifier, + name: navaid.vor_name, + frequency: navaid.vor_frequency, + location: Coordinates { + lat: navaid.vor_latitude, + long: navaid.vor_longitude, + }, + station_declination: navaid.station_declination, + magnetic_variation: Some(navaid.magnetic_variation), + ..Default::default() + } + } +} + +impl From for VhfNavaid { + fn from(navaid: v2::sql_structs::VhfNavaids) -> Self { + Self { + area_code: navaid.area_code, + airport_ident: navaid.airport_identifier, + // Not entirely sure if this is behaviour we intend + icao_code: navaid.icao_code.unwrap_or_default(), + ident: navaid.navaid_identifier, + name: navaid.navaid_name, + frequency: navaid.navaid_frequency, + location: Coordinates { + lat: navaid + .navaid_latitude + .unwrap_or(navaid.dme_latitude.unwrap_or_default()), + long: navaid + .navaid_longitude + .unwrap_or(navaid.dme_longitude.unwrap_or_default()), + }, + station_declination: navaid.station_declination, + continent: navaid.continent, + country: navaid.country, + magnetic_variation: navaid.magnetic_variation, + range: navaid.range, + datum_code: navaid.datum_code, + } + } +} diff --git a/src/database/src/output/waypoint.rs b/src/database/src/output/waypoint.rs index f5ff1a81..2ed80a0e 100644 --- a/src/database/src/output/waypoint.rs +++ b/src/database/src/output/waypoint.rs @@ -1,36 +1,69 @@ -use serde::Serialize; - -use crate::{math::Coordinates, sql_structs}; - -#[serde_with::skip_serializing_none] -#[derive(Serialize)] -pub struct Waypoint { - /// Represents the geographic region in which this Waypoint is located - pub area_code: String, - /// The identifier of the airport that this Waypoint is associated with, if any - pub airport_ident: Option, - /// The icao prefix of the region that this Waypoint is in. - pub icao_code: String, - /// The identifier of this Waypoint (not unique), such as `IRNMN` or `BRAIN` - pub ident: String, - /// The formal name of this Waypoint such as `HJALTEYRI AKUREYRI` or `ORAN` - pub name: String, - /// The geographic location of this Waypoint - pub location: Coordinates, -} - -impl From for Waypoint { - fn from(waypoint: sql_structs::Waypoints) -> Self { - Self { - area_code: waypoint.area_code, - airport_ident: waypoint.region_code, - icao_code: waypoint.icao_code, - ident: waypoint.waypoint_identifier, - name: waypoint.waypoint_name, - location: Coordinates { - lat: waypoint.waypoint_latitude, - long: waypoint.waypoint_longitude, - }, - } - } -} +use serde::Serialize; + +use crate::{ + math::{Coordinates, Degrees}, + sql_structs, v2, +}; + +#[serde_with::skip_serializing_none] +#[derive(Serialize, Default)] +pub struct Waypoint { + /// Represents the geographic region in which this Waypoint is located + pub area_code: String, + /// Contenent of the waypoint (v2 only) + pub continent: Option, + /// Country of the waypoint (v2 only) + pub country: Option, + /// 3 Letter identifier describing the local horizontal identifier (v2 only) + pub datum_code: Option, + /// The identifier of the airport that this Waypoint is associated with, if any + pub airport_ident: Option, + /// The icao prefix of the region that this Waypoint is in. + pub icao_code: String, + /// The identifier of this Waypoint (not unique), such as `IRNMN` or `BRAIN` + pub ident: String, + /// The formal name of this Waypoint such as `HJALTEYRI AKUREYRI` or `ORAN` + pub name: String, + /// The geographic location of this Waypoint + pub location: Coordinates, + /// Magnetic variation (v2 only) + pub magnetic_variation: Option, +} + +impl From for Waypoint { + fn from(waypoint: sql_structs::Waypoints) -> Self { + Self { + area_code: waypoint.area_code, + airport_ident: waypoint.region_code, + icao_code: waypoint.icao_code, + ident: waypoint.waypoint_identifier, + name: waypoint.waypoint_name, + location: Coordinates { + lat: waypoint.waypoint_latitude, + long: waypoint.waypoint_longitude, + }, + ..Default::default() + } + } +} + +impl From for Waypoint { + fn from(waypoint: v2::sql_structs::Waypoints) -> Self { + Self { + area_code: waypoint.area_code, + airport_ident: waypoint.region_code, + // Not entirely sure if this is behaviour we intend + icao_code: waypoint.icao_code.unwrap_or("UNK".to_string()), + ident: waypoint.waypoint_identifier, + name: waypoint.waypoint_name, + location: Coordinates { + lat: waypoint.waypoint_latitude, + long: waypoint.waypoint_longitude, + }, + continent: waypoint.continent, + country: waypoint.country, + magnetic_variation: waypoint.magnetic_varation, + datum_code: waypoint.datum_code, + } + } +} diff --git a/src/database/src/sql_structs.rs b/src/database/src/sql_structs.rs index a66fbf71..446f3e9c 100644 --- a/src/database/src/sql_structs.rs +++ b/src/database/src/sql_structs.rs @@ -1,549 +1,551 @@ -use serde::Deserialize; - -use super::enums::{ - AirwayDirection, AirwayLevel, AirwayRouteType, AltitudeDescriptor, LegType, SpeedDescriptor, TurnDirection, -}; -use crate::enums::{ - ApproachTypeIdentifier, CommunicationType, ControlledAirspaceType, FrequencyUnits, IfrCapability, - RestrictiveAirspaceType, RunwaySurfaceCode, -}; - -#[derive(Deserialize, Debug)] -pub struct AirportCommunication { - pub area_code: String, - pub icao_code: String, - pub airport_identifier: String, - pub communication_type: CommunicationType, - pub communication_frequency: f64, - pub frequency_units: FrequencyUnits, - pub service_indicator: Option, - pub callsign: Option, - pub latitude: f64, - pub longitude: f64, -} - -#[derive(Deserialize, Debug)] -pub struct AirportMsa { - pub area_code: Option, - pub icao_code: Option, - pub airport_identifier: Option, - pub msa_center: Option, - pub msa_center_latitude: Option, - pub msa_center_longitude: Option, - pub magnetic_true_indicator: Option, - pub multiple_code: Option, - pub radius_limit: Option, - pub sector_bearing_1: Option, - pub sector_altitude_1: Option, - pub sector_bearing_2: Option, - pub sector_altitude_2: Option, - pub sector_bearing_3: Option, - pub sector_altitude_3: Option, - pub sector_bearing_4: Option, - pub sector_altitude_4: Option, - pub sector_bearing_5: Option, - pub sector_altitude_5: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Airports { - pub area_code: String, - pub icao_code: String, - pub airport_identifier: String, - pub airport_identifier_3letter: Option, - pub airport_name: String, - pub airport_ref_latitude: f64, - pub airport_ref_longitude: f64, - pub ifr_capability: IfrCapability, - pub longest_runway_surface_code: Option, - pub elevation: f64, - pub transition_altitude: Option, - pub transition_level: Option, - pub speed_limit: Option, - pub speed_limit_altitude: Option, - pub iata_ata_designator: Option, - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct ControlledAirspace { - pub area_code: String, - pub icao_code: String, - pub airspace_center: String, - pub controlled_airspace_name: Option, - pub airspace_type: ControlledAirspaceType, - pub airspace_classification: Option, - pub multiple_code: Option, - pub time_code: Option, - pub seqno: f64, - pub flightlevel: Option, - pub boundary_via: String, - pub latitude: Option, - pub longitude: Option, - pub arc_origin_latitude: Option, - pub arc_origin_longitude: Option, - pub arc_distance: Option, - pub arc_bearing: Option, - pub unit_indicator_lower_limit: Option, - pub lower_limit: Option, - pub unit_indicator_upper_limit: Option, - pub upper_limit: Option, -} - -#[derive(Deserialize, Debug)] -pub struct CruisingTables { - pub cruise_table_identifier: Option, - pub seqno: Option, - pub course_from: Option, - pub course_to: Option, - pub mag_true: Option, - pub cruise_level_from1: Option, - pub vertical_separation1: Option, - pub cruise_level_to1: Option, - pub cruise_level_from2: Option, - pub vertical_separation2: Option, - pub cruise_level_to2: Option, - pub cruise_level_from3: Option, - pub vertical_separation3: Option, - pub cruise_level_to3: Option, - pub cruise_level_from4: Option, - pub vertical_separation4: Option, - pub cruise_level_to4: Option, -} - -#[derive(Deserialize, Debug)] -pub struct EnrouteAirwayRestriction { - pub area_code: Option, - pub route_identifier: Option, - pub restriction_identifier: Option, - pub restriction_type: Option, - pub start_waypoint_identifier: Option, - pub start_waypoint_latitude: Option, - pub start_waypoint_longitude: Option, - pub end_waypoint_identifier: Option, - pub end_waypoint_latitude: Option, - pub end_waypoint_longitude: Option, - pub start_date: Option, - pub end_date: Option, - pub units_of_altitude: Option, - pub restriction_altitude1: Option, - pub block_indicator1: Option, - pub restriction_altitude2: Option, - pub block_indicator2: Option, - pub restriction_altitude3: Option, - pub block_indicator3: Option, - pub restriction_altitude4: Option, - pub block_indicator4: Option, - pub restriction_altitude5: Option, - pub block_indicator5: Option, - pub restriction_altitude6: Option, - pub block_indicator6: Option, - pub restriction_altitude7: Option, - pub block_indicator7: Option, - pub restriction_notes: Option, -} - -#[derive(Deserialize, Debug)] -pub struct EnrouteAirways { - pub area_code: String, - pub route_identifier: String, - pub seqno: f64, - pub icao_code: String, - pub waypoint_identifier: String, - pub waypoint_latitude: f64, - pub waypoint_longitude: f64, - pub waypoint_description_code: String, - pub route_type: AirwayRouteType, - pub flightlevel: AirwayLevel, - pub direction_restriction: Option, - pub crusing_table_identifier: Option, - pub minimum_altitude1: Option, - pub minimum_altitude2: Option, - pub maximum_altitude: Option, - pub outbound_course: f64, - pub inbound_course: f64, - pub inbound_distance: f64, - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct EnrouteCommunication { - pub area_code: String, - pub fir_rdo_ident: String, - pub fir_uir_indicator: Option, - pub communication_type: CommunicationType, - pub communication_frequency: f64, - pub frequency_units: FrequencyUnits, - pub service_indicator: Option, - pub remote_name: Option, - pub callsign: Option, - pub latitude: f64, - pub longitude: f64, -} - -#[derive(Deserialize, Debug)] -pub struct FirUir { - pub area_code: Option, - pub fir_uir_identifier: Option, - pub fir_uir_address: Option, - pub fir_uir_name: Option, - pub fir_uir_indicator: Option, - pub seqno: Option, - pub boundary_via: Option, - pub adjacent_fir_identifier: Option, - pub adjacent_uir_identifier: Option, - pub reporting_units_speed: Option, - pub reporting_units_altitude: Option, - pub fir_uir_latitude: Option, - pub fir_uir_longitude: Option, - pub arc_origin_latitude: Option, - pub arc_origin_longitude: Option, - pub arc_distance: Option, - pub arc_bearing: Option, - pub fir_upper_limit: Option, - pub uir_lower_limit: Option, - pub uir_upper_limit: Option, - pub cruise_table_identifier: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Gate { - pub area_code: String, - pub airport_identifier: String, - pub icao_code: String, - pub gate_identifier: String, - pub gate_latitude: f64, - pub gate_longitude: f64, - pub name: String, -} - -#[derive(Deserialize, Debug)] -pub struct Gls { - pub area_code: String, - pub airport_identifier: String, - pub icao_code: String, - pub gls_ref_path_identifier: String, - pub gls_category: String, - pub gls_channel: f64, - pub runway_identifier: String, - pub gls_approach_bearing: f64, - pub station_latitude: f64, - pub station_longitude: f64, - pub gls_station_ident: String, - pub gls_approach_slope: f64, - /// Yes its spelt wrong in the database - pub magentic_variation: f64, - pub station_elevation: f64, - pub station_type: Option, - - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct GridMora { - pub starting_latitude: Option, - pub starting_longitude: Option, - pub mora01: Option, - pub mora02: Option, - pub mora03: Option, - pub mora04: Option, - pub mora05: Option, - pub mora06: Option, - pub mora07: Option, - pub mora08: Option, - pub mora09: Option, - pub mora10: Option, - pub mora11: Option, - pub mora12: Option, - pub mora13: Option, - pub mora14: Option, - pub mora15: Option, - pub mora16: Option, - pub mora17: Option, - pub mora18: Option, - pub mora19: Option, - pub mora20: Option, - pub mora21: Option, - pub mora22: Option, - pub mora23: Option, - pub mora24: Option, - pub mora25: Option, - pub mora26: Option, - pub mora27: Option, - pub mora28: Option, - pub mora29: Option, - pub mora30: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Header { - pub version: String, - pub arincversion: String, - pub record_set: String, - pub current_airac: String, - pub revision: String, - pub effective_fromto: String, - pub previous_airac: String, - pub previous_fromto: String, - pub parsed_at: String, -} - -#[derive(Deserialize, Debug)] -pub struct Holdings { - pub area_code: Option, - pub region_code: Option, - pub icao_code: Option, - pub waypoint_identifier: Option, - pub holding_name: Option, - pub waypoint_latitude: Option, - pub waypoint_longitude: Option, - pub duplicate_identifier: Option, - pub inbound_holding_course: Option, - pub turn_direction: Option, - pub leg_length: Option, - pub leg_time: Option, - pub minimum_altitude: Option, - pub maximum_altitude: Option, - pub holding_speed: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Procedures { - pub area_code: String, - pub airport_identifier: String, - pub procedure_identifier: String, - pub route_type: String, - pub transition_identifier: Option, - pub seqno: f64, - pub waypoint_icao_code: Option, - pub waypoint_identifier: Option, - pub waypoint_latitude: Option, - pub waypoint_longitude: Option, - pub waypoint_description_code: Option, - pub turn_direction: Option, - pub rnp: Option, - pub path_termination: LegType, - pub recommanded_navaid: Option, - pub recommanded_navaid_latitude: Option, - pub recommanded_navaid_longitude: Option, - pub arc_radius: Option, - pub theta: Option, - pub rho: Option, - pub magnetic_course: Option, - pub route_distance_holding_distance_time: Option, - pub distance_time: Option, - pub altitude_description: Option, - pub altitude1: Option, - pub altitude2: Option, - pub transition_altitude: Option, - pub speed_limit_description: Option, - pub speed_limit: Option, - pub vertical_angle: Option, - pub center_waypoint: Option, - pub center_waypoint_latitude: Option, - pub center_waypoint_longitude: Option, - pub aircraft_category: Option, - pub id: String, - pub recommanded_id: String, - pub center_id: String, -} - -#[derive(Deserialize, Debug)] -pub struct LocalizerMarker { - pub area_code: String, - pub icao_code: String, - pub airport_identifier: String, - pub runway_identifier: String, - pub llz_identifier: String, - pub marker_identifier: String, - pub marker_type: String, - pub marker_latitude: f64, - pub marker_longitude: f64, - pub id: Option, -} - -#[derive(Deserialize, Debug)] -pub struct LocalizersGlideslopes { - pub area_code: Option, - pub icao_code: Option, - pub airport_identifier: String, - pub runway_identifier: Option, - pub llz_identifier: String, - pub llz_latitude: Option, - pub llz_longitude: Option, - pub llz_frequency: Option, - pub llz_bearing: Option, - pub llz_width: Option, - pub ils_mls_gls_category: Option, - pub gs_latitude: Option, - pub gs_longitude: Option, - pub gs_angle: Option, - pub gs_elevation: Option, - pub station_declination: Option, - pub id: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Pathpoints { - pub area_code: String, - pub airport_identifier: String, - pub icao_code: String, - pub approach_procedure_ident: String, - pub runway_identifier: String, - pub sbas_service_provider_identifier: f64, - pub reference_path_identifier: String, - pub landing_threshold_latitude: f64, - pub landing_threshold_longitude: f64, - pub ltp_ellipsoid_height: f64, - pub glidepath_angle: f64, - pub flightpath_alignment_latitude: f64, - pub flightpath_alignment_longitude: f64, - pub course_width_at_threshold: f64, - pub length_offset: f64, - pub path_point_tch: f64, - pub tch_units_indicator: String, - pub hal: f64, - pub val: f64, - pub fpap_ellipsoid_height: f64, - pub fpap_orthometric_height: Option, - pub ltp_orthometric_height: Option, - pub approach_type_identifier: ApproachTypeIdentifier, - pub gnss_channel_number: f64, -} - -#[derive(Deserialize, Debug)] -pub struct RestrictiveAirspace { - pub area_code: String, - pub icao_code: String, - pub restrictive_airspace_designation: String, - pub restrictive_airspace_name: Option, - pub restrictive_type: RestrictiveAirspaceType, - pub multiple_code: Option, - pub seqno: f64, - pub boundary_via: String, - pub flightlevel: Option, - pub latitude: Option, - pub longitude: Option, - pub arc_origin_latitude: Option, - pub arc_origin_longitude: Option, - pub arc_distance: Option, - pub arc_bearing: Option, - pub unit_indicator_lower_limit: Option, - pub lower_limit: Option, - pub unit_indicator_upper_limit: Option, - pub upper_limit: Option, -} - -#[derive(Deserialize, Debug)] -pub struct Runways { - pub area_code: String, - pub icao_code: String, - pub airport_identifier: String, - pub runway_identifier: String, - pub runway_latitude: f64, - pub runway_longitude: f64, - pub runway_gradient: f64, - pub runway_magnetic_bearing: f64, - pub runway_true_bearing: f64, - pub landing_threshold_elevation: f64, - pub displaced_threshold_distance: f64, - pub threshold_crossing_height: f64, - pub runway_length: f64, - pub runway_width: f64, - pub llz_identifier: Option, - pub llz_mls_gls_category: Option, - pub surface_code: f64, - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct Sids { - pub area_code: Option, - pub airport_identifier: Option, - pub procedure_identifier: Option, - pub route_type: Option, - pub transition_identifier: Option, - pub seqno: Option, - pub waypoint_icao_code: Option, - pub waypoint_identifier: Option, - pub waypoint_latitude: Option, - pub waypoint_longitude: Option, - pub waypoint_description_code: Option, - pub turn_direction: Option, - pub rnp: Option, - pub path_termination: Option, - pub recommanded_navaid: Option, - pub recommanded_navaid_latitude: Option, - pub recommanded_navaid_longitude: Option, - pub arc_radius: Option, - pub theta: Option, - pub rho: Option, - pub magnetic_course: Option, - pub route_distance_holding_distance_time: Option, - pub distance_time: Option, - pub altitude_description: Option, - pub altitude1: Option, - pub altitude2: Option, - pub transition_altitude: Option, - pub speed_limit_description: Option, - pub speed_limit: Option, - pub vertical_angle: Option, - pub center_waypoint: Option, - pub center_waypoint_latitude: Option, - pub center_waypoint_longitude: Option, - pub aircraft_category: Option, - pub id: Option, - pub recommanded_id: Option, - pub center_id: Option, -} - -#[derive(Deserialize, Debug)] -pub struct NdbNavaids { - pub area_code: String, - pub airport_identifier: Option, - pub icao_code: String, - pub ndb_identifier: String, - pub ndb_name: String, - pub ndb_frequency: f64, - pub navaid_class: String, - pub ndb_latitude: f64, - pub ndb_longitude: f64, - pub range: f64, - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct Waypoints { - pub area_code: String, - pub region_code: Option, - pub icao_code: String, - pub waypoint_identifier: String, - pub waypoint_name: String, - pub waypoint_type: String, - pub waypoint_usage: Option, - pub waypoint_latitude: f64, - pub waypoint_longitude: f64, - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct VhfNavaids { - pub area_code: String, - pub airport_identifier: Option, - pub icao_code: String, - pub vor_identifier: String, - pub vor_name: String, - pub vor_frequency: f64, - pub navaid_class: String, - pub vor_latitude: f64, - pub vor_longitude: f64, - pub dme_ident: Option, - pub dme_latitude: Option, - pub dme_longitude: Option, - pub dme_elevation: Option, - pub ilsdme_bias: Option, - pub range: f64, - pub station_declination: Option, - pub magnetic_variation: f64, - pub id: String, -} +#![allow(dead_code)] +use serde::Deserialize; + +use super::enums::{ + AirwayDirection, AirwayLevel, AirwayRouteType, AltitudeDescriptor, LegType, SpeedDescriptor, + TurnDirection, +}; +use crate::enums::{ + ApproachTypeIdentifier, CommunicationType, ControlledAirspaceType, FrequencyUnits, + IfrCapability, RestrictiveAirspaceType, RunwaySurfaceCode, +}; + +#[derive(Deserialize, Debug)] +pub struct AirportCommunication { + pub area_code: String, + pub icao_code: String, + pub airport_identifier: String, + pub communication_type: CommunicationType, + pub communication_frequency: f64, + pub frequency_units: FrequencyUnits, + pub service_indicator: Option, + pub callsign: Option, + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Deserialize, Debug)] +pub struct AirportMsa { + pub area_code: Option, + pub icao_code: Option, + pub airport_identifier: Option, + pub msa_center: Option, + pub msa_center_latitude: Option, + pub msa_center_longitude: Option, + pub magnetic_true_indicator: Option, + pub multiple_code: Option, + pub radius_limit: Option, + pub sector_bearing_1: Option, + pub sector_altitude_1: Option, + pub sector_bearing_2: Option, + pub sector_altitude_2: Option, + pub sector_bearing_3: Option, + pub sector_altitude_3: Option, + pub sector_bearing_4: Option, + pub sector_altitude_4: Option, + pub sector_bearing_5: Option, + pub sector_altitude_5: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Airports { + pub area_code: String, + pub icao_code: String, + pub airport_identifier: String, + pub airport_identifier_3letter: Option, + pub airport_name: String, + pub airport_ref_latitude: f64, + pub airport_ref_longitude: f64, + pub ifr_capability: IfrCapability, + pub longest_runway_surface_code: Option, + pub elevation: f64, + pub transition_altitude: Option, + pub transition_level: Option, + pub speed_limit: Option, + pub speed_limit_altitude: Option, + pub iata_ata_designator: Option, + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct ControlledAirspace { + pub area_code: String, + pub icao_code: String, + pub airspace_center: String, + pub controlled_airspace_name: Option, + pub airspace_type: ControlledAirspaceType, + pub airspace_classification: Option, + pub multiple_code: Option, + pub time_code: Option, + pub seqno: f64, + pub flightlevel: Option, + pub boundary_via: String, + pub latitude: Option, + pub longitude: Option, + pub arc_origin_latitude: Option, + pub arc_origin_longitude: Option, + pub arc_distance: Option, + pub arc_bearing: Option, + pub unit_indicator_lower_limit: Option, + pub lower_limit: Option, + pub unit_indicator_upper_limit: Option, + pub upper_limit: Option, +} + +#[derive(Deserialize, Debug)] +pub struct CruisingTables { + pub cruise_table_identifier: Option, + pub seqno: Option, + pub course_from: Option, + pub course_to: Option, + pub mag_true: Option, + pub cruise_level_from1: Option, + pub vertical_separation1: Option, + pub cruise_level_to1: Option, + pub cruise_level_from2: Option, + pub vertical_separation2: Option, + pub cruise_level_to2: Option, + pub cruise_level_from3: Option, + pub vertical_separation3: Option, + pub cruise_level_to3: Option, + pub cruise_level_from4: Option, + pub vertical_separation4: Option, + pub cruise_level_to4: Option, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteAirwayRestriction { + pub area_code: Option, + pub route_identifier: Option, + pub restriction_identifier: Option, + pub restriction_type: Option, + pub start_waypoint_identifier: Option, + pub start_waypoint_latitude: Option, + pub start_waypoint_longitude: Option, + pub end_waypoint_identifier: Option, + pub end_waypoint_latitude: Option, + pub end_waypoint_longitude: Option, + pub start_date: Option, + pub end_date: Option, + pub units_of_altitude: Option, + pub restriction_altitude1: Option, + pub block_indicator1: Option, + pub restriction_altitude2: Option, + pub block_indicator2: Option, + pub restriction_altitude3: Option, + pub block_indicator3: Option, + pub restriction_altitude4: Option, + pub block_indicator4: Option, + pub restriction_altitude5: Option, + pub block_indicator5: Option, + pub restriction_altitude6: Option, + pub block_indicator6: Option, + pub restriction_altitude7: Option, + pub block_indicator7: Option, + pub restriction_notes: Option, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteAirways { + pub area_code: String, + pub route_identifier: String, + pub seqno: f64, + pub icao_code: String, + pub waypoint_identifier: String, + pub waypoint_latitude: f64, + pub waypoint_longitude: f64, + pub waypoint_description_code: String, + pub route_type: AirwayRouteType, + pub flightlevel: AirwayLevel, + pub direction_restriction: Option, + pub crusing_table_identifier: Option, + pub minimum_altitude1: Option, + pub minimum_altitude2: Option, + pub maximum_altitude: Option, + pub outbound_course: f64, + pub inbound_course: f64, + pub inbound_distance: f64, + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteCommunication { + pub area_code: String, + pub fir_rdo_ident: String, + pub fir_uir_indicator: Option, + pub communication_type: CommunicationType, + pub communication_frequency: f64, + pub frequency_units: FrequencyUnits, + pub service_indicator: Option, + pub remote_name: Option, + pub callsign: Option, + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Deserialize, Debug)] +pub struct FirUir { + pub area_code: Option, + pub fir_uir_identifier: Option, + pub fir_uir_address: Option, + pub fir_uir_name: Option, + pub fir_uir_indicator: Option, + pub seqno: Option, + pub boundary_via: Option, + pub adjacent_fir_identifier: Option, + pub adjacent_uir_identifier: Option, + pub reporting_units_speed: Option, + pub reporting_units_altitude: Option, + pub fir_uir_latitude: Option, + pub fir_uir_longitude: Option, + pub arc_origin_latitude: Option, + pub arc_origin_longitude: Option, + pub arc_distance: Option, + pub arc_bearing: Option, + pub fir_upper_limit: Option, + pub uir_lower_limit: Option, + pub uir_upper_limit: Option, + pub cruise_table_identifier: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Gate { + pub area_code: String, + pub airport_identifier: String, + pub icao_code: String, + pub gate_identifier: String, + pub gate_latitude: f64, + pub gate_longitude: f64, + pub name: String, +} + +#[derive(Deserialize, Debug)] +pub struct Gls { + pub area_code: String, + pub airport_identifier: String, + pub icao_code: String, + pub gls_ref_path_identifier: String, + pub gls_category: String, + pub gls_channel: f64, + pub runway_identifier: String, + pub gls_approach_bearing: f64, + pub station_latitude: f64, + pub station_longitude: f64, + pub gls_station_ident: String, + pub gls_approach_slope: f64, + /// Yes its spelt wrong in the database + pub magentic_variation: f64, + pub station_elevation: f64, + pub station_type: Option, + + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct GridMora { + pub starting_latitude: Option, + pub starting_longitude: Option, + pub mora01: Option, + pub mora02: Option, + pub mora03: Option, + pub mora04: Option, + pub mora05: Option, + pub mora06: Option, + pub mora07: Option, + pub mora08: Option, + pub mora09: Option, + pub mora10: Option, + pub mora11: Option, + pub mora12: Option, + pub mora13: Option, + pub mora14: Option, + pub mora15: Option, + pub mora16: Option, + pub mora17: Option, + pub mora18: Option, + pub mora19: Option, + pub mora20: Option, + pub mora21: Option, + pub mora22: Option, + pub mora23: Option, + pub mora24: Option, + pub mora25: Option, + pub mora26: Option, + pub mora27: Option, + pub mora28: Option, + pub mora29: Option, + pub mora30: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Header { + pub version: String, + pub arincversion: String, + pub record_set: String, + pub current_airac: String, + pub revision: String, + pub effective_fromto: String, + pub previous_airac: String, + pub previous_fromto: String, + pub parsed_at: String, +} + +#[derive(Deserialize, Debug)] +pub struct Holdings { + pub area_code: Option, + pub region_code: Option, + pub icao_code: Option, + pub waypoint_identifier: Option, + pub holding_name: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub duplicate_identifier: Option, + pub inbound_holding_course: Option, + pub turn_direction: Option, + pub leg_length: Option, + pub leg_time: Option, + pub minimum_altitude: Option, + pub maximum_altitude: Option, + pub holding_speed: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Procedures { + pub area_code: String, + pub airport_identifier: String, + pub procedure_identifier: String, + pub route_type: String, + pub transition_identifier: Option, + pub seqno: f64, + pub waypoint_icao_code: Option, + pub waypoint_identifier: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub waypoint_description_code: Option, + pub turn_direction: Option, + pub rnp: Option, + pub path_termination: LegType, + pub recommanded_navaid: Option, + pub recommanded_navaid_latitude: Option, + pub recommanded_navaid_longitude: Option, + pub arc_radius: Option, + pub theta: Option, + pub rho: Option, + pub magnetic_course: Option, + pub route_distance_holding_distance_time: Option, + pub distance_time: Option, + pub altitude_description: Option, + pub altitude1: Option, + pub altitude2: Option, + pub transition_altitude: Option, + pub speed_limit_description: Option, + pub speed_limit: Option, + pub vertical_angle: Option, + pub center_waypoint: Option, + pub center_waypoint_latitude: Option, + pub center_waypoint_longitude: Option, + pub aircraft_category: Option, + pub id: String, + pub recommanded_id: String, + pub center_id: String, +} + +#[derive(Deserialize, Debug)] +pub struct LocalizerMarker { + pub area_code: String, + pub icao_code: String, + pub airport_identifier: String, + pub runway_identifier: String, + pub llz_identifier: String, + pub marker_identifier: String, + pub marker_type: String, + pub marker_latitude: f64, + pub marker_longitude: f64, + pub id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct LocalizersGlideslopes { + pub area_code: Option, + pub icao_code: Option, + pub airport_identifier: String, + pub runway_identifier: Option, + pub llz_identifier: String, + pub llz_latitude: Option, + pub llz_longitude: Option, + pub llz_frequency: Option, + pub llz_bearing: Option, + pub llz_width: Option, + pub ils_mls_gls_category: Option, + pub gs_latitude: Option, + pub gs_longitude: Option, + pub gs_angle: Option, + pub gs_elevation: Option, + pub station_declination: Option, + pub id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Pathpoints { + pub area_code: String, + pub airport_identifier: String, + pub icao_code: String, + pub approach_procedure_ident: String, + pub runway_identifier: String, + pub sbas_service_provider_identifier: f64, + pub reference_path_identifier: String, + pub landing_threshold_latitude: f64, + pub landing_threshold_longitude: f64, + pub ltp_ellipsoid_height: f64, + pub glidepath_angle: f64, + pub flightpath_alignment_latitude: f64, + pub flightpath_alignment_longitude: f64, + pub course_width_at_threshold: f64, + pub length_offset: f64, + pub path_point_tch: f64, + pub tch_units_indicator: String, + pub hal: f64, + pub val: f64, + pub fpap_ellipsoid_height: f64, + pub fpap_orthometric_height: Option, + pub ltp_orthometric_height: Option, + pub approach_type_identifier: ApproachTypeIdentifier, + pub gnss_channel_number: f64, +} + +#[derive(Deserialize, Debug)] +pub struct RestrictiveAirspace { + pub area_code: String, + pub icao_code: String, + pub restrictive_airspace_designation: String, + pub restrictive_airspace_name: Option, + pub restrictive_type: RestrictiveAirspaceType, + pub multiple_code: Option, + pub seqno: f64, + pub boundary_via: String, + pub flightlevel: Option, + pub latitude: Option, + pub longitude: Option, + pub arc_origin_latitude: Option, + pub arc_origin_longitude: Option, + pub arc_distance: Option, + pub arc_bearing: Option, + pub unit_indicator_lower_limit: Option, + pub lower_limit: Option, + pub unit_indicator_upper_limit: Option, + pub upper_limit: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Runways { + pub area_code: String, + pub icao_code: String, + pub airport_identifier: String, + pub runway_identifier: String, + pub runway_latitude: f64, + pub runway_longitude: f64, + pub runway_gradient: f64, + pub runway_magnetic_bearing: f64, + pub runway_true_bearing: f64, + pub landing_threshold_elevation: f64, + pub displaced_threshold_distance: f64, + pub threshold_crossing_height: f64, + pub runway_length: f64, + pub runway_width: f64, + pub llz_identifier: Option, + pub llz_mls_gls_category: Option, + pub surface_code: f64, + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct Sids { + pub area_code: Option, + pub airport_identifier: Option, + pub procedure_identifier: Option, + pub route_type: Option, + pub transition_identifier: Option, + pub seqno: Option, + pub waypoint_icao_code: Option, + pub waypoint_identifier: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub waypoint_description_code: Option, + pub turn_direction: Option, + pub rnp: Option, + pub path_termination: Option, + pub recommanded_navaid: Option, + pub recommanded_navaid_latitude: Option, + pub recommanded_navaid_longitude: Option, + pub arc_radius: Option, + pub theta: Option, + pub rho: Option, + pub magnetic_course: Option, + pub route_distance_holding_distance_time: Option, + pub distance_time: Option, + pub altitude_description: Option, + pub altitude1: Option, + pub altitude2: Option, + pub transition_altitude: Option, + pub speed_limit_description: Option, + pub speed_limit: Option, + pub vertical_angle: Option, + pub center_waypoint: Option, + pub center_waypoint_latitude: Option, + pub center_waypoint_longitude: Option, + pub aircraft_category: Option, + pub id: Option, + pub recommanded_id: Option, + pub center_id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct NdbNavaids { + pub area_code: String, + pub airport_identifier: Option, + pub icao_code: String, + pub ndb_identifier: String, + pub ndb_name: String, + pub ndb_frequency: f64, + pub navaid_class: String, + pub ndb_latitude: f64, + pub ndb_longitude: f64, + pub range: f64, + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct Waypoints { + pub area_code: String, + pub region_code: Option, + pub icao_code: String, + pub waypoint_identifier: String, + pub waypoint_name: String, + pub waypoint_type: String, + pub waypoint_usage: Option, + pub waypoint_latitude: f64, + pub waypoint_longitude: f64, + pub id: String, +} + +#[derive(Deserialize, Debug)] +pub struct VhfNavaids { + pub area_code: String, + pub airport_identifier: Option, + pub icao_code: String, + pub vor_identifier: String, + pub vor_name: String, + pub vor_frequency: f64, + pub navaid_class: String, + pub vor_latitude: f64, + pub vor_longitude: f64, + pub dme_ident: Option, + pub dme_latitude: Option, + pub dme_longitude: Option, + pub dme_elevation: Option, + pub ilsdme_bias: Option, + pub range: f64, + pub station_declination: Option, + pub magnetic_variation: f64, + pub id: String, +} diff --git a/src/database/src/traits.rs b/src/database/src/traits.rs new file mode 100644 index 00000000..793ab7cc --- /dev/null +++ b/src/database/src/traits.rs @@ -0,0 +1,276 @@ +use std::{ + error::Error, + fmt::{Display, Formatter}, +}; + +use enum_dispatch::enum_dispatch; +use rusqlite::{params_from_iter, types::ValueRef, Connection, Result}; +use serde_json::{Number, Value}; + +use super::output::airport::Airport; +use crate::{ + database::DatabaseV1, + enums::InterfaceFormat, + manual::database::DatabaseManual, + math::{Coordinates, NauticalMiles}, + output::{ + airspace::{ControlledAirspace, RestrictiveAirspace}, + airway::Airway, + communication::Communication, + database_info::DatabaseInfo, + gate::Gate, + gls_navaid::GlsNavaid, + ndb_navaid::NdbNavaid, + path_point::PathPoint, + procedure::{approach::Approach, arrival::Arrival, departure::Departure}, + runway::RunwayThreshold, + vhf_navaid::VhfNavaid, + waypoint::Waypoint, + }, + v2::database::DatabaseV2, +}; + +#[derive(Debug)] +pub struct NoDatabaseOpen; + +impl Display for NoDatabaseOpen { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "No database open") + } +} + +#[derive(Debug)] +pub struct DatabaseNotCompat; + +impl Display for DatabaseNotCompat { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Function not implemented in database type") + } +} + +impl Error for NoDatabaseOpen {} + +impl Error for DatabaseNotCompat {} + +#[derive(serde::Serialize, Clone)] +pub struct PackageInfo { + pub path: String, + pub uuid: String, + pub is_bundled: bool, + pub cycle: InstalledNavigationDataCycleInfo, +} + +#[derive(serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct InstalledNavigationDataCycleInfo { + pub cycle: String, + pub revision: String, + pub name: String, + pub format: String, + pub validity_period: String, + // The serde_with doesn't work here so this is here instead + #[serde(skip_serializing_if = "Option::is_none")] + pub database_path: Option, +} + +#[enum_dispatch] +pub enum DatabaseEnum { + DatabaseV1, + DatabaseV2, + DatabaseManual, +} + +#[allow(unused_variables)] +#[enum_dispatch(DatabaseEnum)] +pub trait DatabaseTrait { + fn get_database_type(&self) -> InterfaceFormat; + + fn get_database(&self) -> Result<&Connection, NoDatabaseOpen>; + + // Called after the gauge intializes + fn setup(&self) -> Result>; + + // Takes a pacakge and switches the 'active' connection to the requested package. + fn enable_cycle(&mut self, package: &PackageInfo) -> Result>; + + fn disable_cycle(&mut self) -> Result>; + + fn execute_sql_query(&self, sql: String, params: Vec) -> Result> { + // Execute query + let conn = self.get_database()?; + let mut stmt = conn.prepare(&sql)?; + let names = stmt + .column_names() + .into_iter() + .map(|n| n.to_string()) + .collect::>(); + + // Collect data to be returned + let data_iter = stmt.query_map(params_from_iter(params), |row| { + let mut map = serde_json::Map::new(); + for (i, name) in names.iter().enumerate() { + let value = match row.get_ref(i)? { + ValueRef::Text(text) => { + Some(Value::String(String::from_utf8(text.into()).unwrap())) + } + ValueRef::Integer(int) => Some(Value::Number(Number::from(int))), + ValueRef::Real(real) => Some(Value::Number(Number::from_f64(real).unwrap())), + ValueRef::Null => None, + ValueRef::Blob(_) => panic!("Unexpected value type Blob"), + }; + + if let Some(value) = value { + map.insert(name.to_string(), value); + } + } + Ok(Value::Object(map)) + })?; + + let mut data = Vec::new(); + for row in data_iter { + data.push(row?); + } + + let json = Value::Array(data); + + Ok(json) + } + fn get_database_info(&self) -> Result> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_airport(&self, ident: String) -> Result> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_waypoints(&self, ident: String) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_vhf_navaids(&self, ident: String) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_ndb_navaids(&self, ident: String) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_airways(&self, ident: String) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_airways_at_fix( + &self, + fix_ident: String, + fix_icao_code: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_airports_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_waypoints_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_ndb_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_vhf_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_airways_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_controlled_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_restrictive_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_communications_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_runways_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_departures_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_arrivals_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_approaches_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_waypoints_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_ndb_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_gates_at_airport(&self, airport_ident: String) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_communications_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_gls_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } + fn get_path_points_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + Err(Box::new(DatabaseNotCompat)) + } +} diff --git a/src/database/src/util.rs b/src/database/src/util.rs index 7f387eb7..9e0d2c15 100644 --- a/src/database/src/util.rs +++ b/src/database/src/util.rs @@ -1,68 +1,119 @@ -use std::{error::Error, fs, io::Read, path::Path}; - -// From 1.3.1 of https://www.sqlite.org/fileformat.html -const SQLITE_HEADER: [u8; 16] = [ - 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00, -]; - -#[derive(PartialEq, Eq)] -pub enum PathType { - File, - Directory, - DoesNotExist, -} - -/// We aren't able to get file metadata in the sim so we can't use some of the standard library file system functions -/// (like is_dir, exists, and some others) -pub fn get_path_type(path: &Path) -> PathType { - match fs::read_dir(path) { - Ok(mut dir_res) => { - let next = dir_res.next(); - - if let Some(result) = next { - if result.is_ok() { - return PathType::Directory; - } - } - }, - Err(_) => {}, - }; - - let file_res = fs::File::open(path); - if file_res.is_ok() { - return PathType::File; - } - - PathType::DoesNotExist -} - -pub fn find_sqlite_file(path: &str) -> Result> { - if get_path_type(&Path::new(path)) != PathType::Directory { - return Err("Path is not a directory".into()); - } - - // We are going to search this directory for a database - for entry in std::fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - if get_path_type(&path) == PathType::File { - let path = path.to_str().ok_or("Invalid path")?; - - if is_sqlite_file(path)? { - return Ok(path.to_string()); - } - } - } - Err("No SQL database found. Make sure the database specified is a SQL database".into()) -} - -pub fn is_sqlite_file(path: &str) -> Result> { - if get_path_type(&Path::new(path)) != PathType::File { - return Ok(false); - } - - let mut file = fs::File::open(path)?; - let mut buf = [0; 16]; - file.read_exact(&mut buf)?; - Ok(buf == SQLITE_HEADER) -} +use std::{error::Error, fs, io::Read, path::Path}; + +use crate::math::{Coordinates, NauticalMiles}; + +// From 1.3.1 of https://www.sqlite.org/fileformat.html +const SQLITE_HEADER: [u8; 16] = [ + 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00, +]; + +#[derive(PartialEq, Eq, Debug)] +pub enum PathType { + File, + Directory, + DoesNotExist, +} + +/// We aren't able to get file metadata in the sim so we can't use some of the standard library file system functions +/// (like is_dir, exists, and some others) +pub fn get_path_type(path: &Path) -> PathType { + if let Ok(mut dir_res) = fs::read_dir(path) { + let next = dir_res.next(); + + if let Some(result) = next { + if result.is_ok() { + return PathType::Directory; + } + } + }; + + let file_res = fs::File::open(path); + if file_res.is_ok() { + return PathType::File; + } + + PathType::DoesNotExist +} + +pub fn find_sqlite_file(path: &str) -> Result> { + if get_path_type(Path::new(path)) != PathType::Directory { + return Err("Path is not a directory".into()); + } + + // We are going to search this directory for a database + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if get_path_type(&path) == PathType::File { + let path = path.to_str().ok_or("Invalid path")?; + + if is_sqlite_file(path)? { + return Ok(path.to_string()); + } + } + } + Err("No SQL database found. Make sure the database specified is a SQL database".into()) +} + +pub fn is_sqlite_file(path: &str) -> Result> { + if get_path_type(Path::new(path)) != PathType::File { + return Ok(false); + } + + let mut file = fs::File::open(path)?; + let mut buf = [0; 16]; + file.read_exact(&mut buf)?; + Ok(buf == SQLITE_HEADER) +} + +pub fn range_query_where(center: Coordinates, range: NauticalMiles, prefix: &str) -> String { + let (bottom_left, top_right) = center.distance_bounds(range); + + let prefix = if prefix.is_empty() { + String::new() + } else { + format!("{prefix}_") + }; + + if bottom_left.long > top_right.long { + format!( + "{prefix}latitude BETWEEN {} AND {} AND ({prefix}longitude >= {} OR {prefix}longitude <= {})", + bottom_left.lat, top_right.lat, bottom_left.long, top_right.long + ) + } else if bottom_left.lat.max(top_right.lat) > 80.0 { + format!("{prefix}latitude >= {}", bottom_left.lat.min(top_right.lat)) + } else if bottom_left.lat.min(top_right.lat) < -80.0 { + format!("{prefix}latitude <= {}", bottom_left.lat.max(top_right.lat)) + } else { + format!( + "{prefix}latitude BETWEEN {} AND {} AND {prefix}longitude BETWEEN {} AND {}", + bottom_left.lat, top_right.lat, bottom_left.long, top_right.long + ) + } +} +pub fn fetch_row( + stmt: &mut rusqlite::Statement, + params: impl rusqlite::Params, +) -> Result> +where + T: for<'r> serde::Deserialize<'r>, +{ + let mut rows = stmt.query_and_then(params, |r| serde_rusqlite::from_row::(r))?; + let row = rows.next().ok_or("No row found")??; + Ok(row) +} + +pub fn fetch_rows( + stmt: &mut rusqlite::Statement, + params: impl rusqlite::Params, +) -> Result, Box> +where + T: for<'r> serde::Deserialize<'r>, +{ + let rows = stmt.query_and_then(params, |r| serde_rusqlite::from_row::(r))?; + let mut data = Vec::new(); + for row in rows { + data.push(row.map_err(|e| e.to_string())?); + } + Ok(data) +} diff --git a/src/database/src/v2/database.rs b/src/database/src/v2/database.rs new file mode 100644 index 00000000..76b83b5e --- /dev/null +++ b/src/database/src/v2/database.rs @@ -0,0 +1,589 @@ +use std::{error::Error, path::Path}; + +use rusqlite::{params, Connection, OpenFlags, Result}; + +use crate::{ + enums::InterfaceFormat, + math::{Coordinates, NauticalMiles}, + output::{ + airport::Airport, + airspace::{ + map_controlled_airspaces, map_restrictive_airspaces, ControlledAirspace, + RestrictiveAirspace, + }, + airway::{map_airways_v2, Airway}, + communication::Communication, + database_info::DatabaseInfo, + gate::Gate, + gls_navaid::GlsNavaid, + ndb_navaid::NdbNavaid, + path_point::PathPoint, + procedure::{ + approach::{map_approaches_v2, Approach}, + arrival::{map_arrivals_v2, Arrival}, + departure::{map_departures_v2, Departure}, + }, + runway::RunwayThreshold, + vhf_navaid::VhfNavaid, + waypoint::Waypoint, + }, + sql_structs, + traits::{DatabaseTrait, NoDatabaseOpen, PackageInfo}, + util, v2, +}; + +#[derive(Default)] +pub struct DatabaseV2 { + connection: Option, + pub path: Option, +} + +impl DatabaseTrait for DatabaseV2 { + fn get_database_type(&self) -> InterfaceFormat { + InterfaceFormat::DFDv2 + } + + fn get_database(&self) -> Result<&Connection, NoDatabaseOpen> { + self.connection.as_ref().ok_or(NoDatabaseOpen) + } + + fn setup(&self) -> Result> { + // Nothing goes here preferrably + Ok(String::from("Setup Complete")) + } + + fn enable_cycle(&mut self, package: &PackageInfo) -> Result> { + let db_path = match package.cycle.database_path { + Some(ref path) => Path::new("").join(&package.path).join(path), + None => Path::new("") + .join(&package.path) + .join(format!("ng_jeppesen_fwdfd_{}.s3db", package.cycle.cycle)), + }; + + println!("[NAVIGRAPH]: Setting active database to {:?}", db_path); + + if self.connection.is_some() { + self.disable_cycle()?; + } + + let flags = OpenFlags::SQLITE_OPEN_READ_ONLY + | OpenFlags::SQLITE_OPEN_URI + | OpenFlags::SQLITE_OPEN_NO_MUTEX; + let conn = Connection::open_with_flags(db_path.clone(), flags)?; + + self.connection = Some(conn); + self.path = Some(String::from(db_path.to_str().unwrap())); + + println!("[NAVIGRAPH]: Set active database to {:?}", db_path); + + Ok(true) + } + + fn disable_cycle(&mut self) -> Result> { + println!("[NAVIGRAPH]: Disabling active database"); + self.connection = None; + Ok(true) + } + + fn get_database_info(&self) -> Result> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_hdr_header")?; + + let header_data = util::fetch_row::(&mut stmt, params![])?; + + Ok(DatabaseInfo::from(header_data)) + } + + fn get_airport(&self, ident: String) -> Result> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pa_airports WHERE airport_identifier = (?1)")?; + + let airport_data = util::fetch_row::(&mut stmt, params![ident])?; + + Ok(Airport::from(airport_data)) + } + + fn get_waypoints(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut enroute_stmt = conn + .prepare("SELECT * FROM tbl_ea_enroute_waypoints WHERE waypoint_identifier = (?1)")?; + let mut terminal_stmt = conn + .prepare("SELECT * FROM tbl_pc_terminal_waypoints WHERE waypoint_identifier = (?1)")?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, params![ident])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, params![ident])?; + + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(Waypoint::from) + .collect()) + } + + fn get_vhf_navaids(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_d_vhfnavaids WHERE navaid_identifier = (?1)")?; + + let navaids_data = + util::fetch_rows::(&mut stmt, params![ident])?; + + Ok(navaids_data.into_iter().map(VhfNavaid::from).collect()) + } + + fn get_ndb_navaids(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut enroute_stmt = + conn.prepare("SELECT * FROM tbl_db_enroute_ndbnavaids WHERE navaid_identifier = (?1)")?; + let mut terminal_stmt = conn + .prepare("SELECT * FROM tbl_pn_terminal_ndbnavaids WHERE navaid_identifier = (?1)")?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, params![ident])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, params![ident])?; + + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(NdbNavaid::from) + .collect()) + } + + fn get_airways(&self, ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_er_enroute_airways WHERE route_identifier = (?1)")?; + + let airways_data = + util::fetch_rows::(&mut stmt, params![ident])?; + + Ok(map_airways_v2(airways_data)) + } + + fn get_airways_at_fix( + &self, + fix_ident: String, + fix_icao_code: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt: rusqlite::Statement<'_> = conn.prepare( + "SELECT * FROM tbl_er_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ + tbl_er_enroute_airways WHERE waypoint_identifier = (?1) AND icao_code = (?2))", + )?; + let all_airways = util::fetch_rows::( + &mut stmt, + params![fix_ident, fix_icao_code], + )?; + + Ok(map_airways_v2(all_airways) + .into_iter() + .filter(|airway| { + airway + .fixes + .iter() + .any(|fix| fix.ident == fix_ident && fix.icao_code == fix_icao_code) + }) + .collect()) + } + + fn get_airports_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "airport_ref"); + + let mut stmt = + conn.prepare(format!("SELECT * FROM tbl_pa_airports WHERE {where_string}").as_str())?; + + let airports_data = util::fetch_rows::(&mut stmt, [])?; + + // Filter into a circle of range + Ok(airports_data + .into_iter() + .map(Airport::from) + .filter(|airport| airport.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_waypoints_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "waypoint"); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_ea_enroute_waypoints WHERE {where_string}").as_str(), + )?; + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_pc_terminal_waypoints WHERE {where_string}").as_str(), + )?; + + let enroute_data = util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = util::fetch_rows::(&mut terminal_stmt, [])?; + + // Filter into a circle of range + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(Waypoint::from) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_ndb_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "navaid"); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_db_enroute_ndbnavaids WHERE {where_string}").as_str(), + )?; + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_pn_terminal_ndbnavaids WHERE {where_string}").as_str(), + )?; + + let enroute_data = util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, [])?; + + // Filter into a circle of range + Ok(enroute_data + .into_iter() + .chain(terminal_data) + .map(NdbNavaid::from) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_vhf_navaids_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "navaid"); + + let mut stmt = + conn.prepare(format!("SELECT * FROM tbl_d_vhfnavaids WHERE {where_string}").as_str())?; + + let navaids_data = util::fetch_rows::(&mut stmt, [])?; + + // Filter into a circle of range + Ok(navaids_data + .into_iter() + .map(VhfNavaid::from) + .filter(|navaid| navaid.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_airways_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, "waypoint"); + + let mut stmt = conn.prepare( + format!( + "SELECT * FROM tbl_er_enroute_airways WHERE route_identifier IN (SELECT route_identifier FROM \ + tbl_er_enroute_airways WHERE {where_string})" + ) + .as_str(), + )?; + + let airways_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_airways_v2(airways_data) + .into_iter() + .filter(|airway| { + airway + .fixes + .iter() + .any(|fix| fix.location.distance_to(¢er) <= range) + }) + .collect()) + } + + fn get_controlled_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + let arc_where_string = util::range_query_where(center, range, "arc_origin"); + + let range_query = format!( + "SELECT airspace_center, multiple_code FROM tbl_uc_controlled_airspace WHERE {where_string} OR \ + {arc_where_string}" + ); + + let mut stmt = conn.prepare( + format!( + "SELECT * FROM tbl_uc_controlled_airspace WHERE (airspace_center, multiple_code) IN ({range_query})" + ) + .as_str(), + )?; + + // No changes since v1, able to use same struct + let airspaces_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_controlled_airspaces(airspaces_data)) + } + + fn get_restrictive_airspaces_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + let arc_where_string = util::range_query_where(center, range, "arc_origin"); + + let range_query: String = format!( + "SELECT restrictive_airspace_designation, icao_code FROM tbl_ur_restrictive_airspace WHERE {where_string} \ + OR {arc_where_string}" + ); + + let mut stmt = conn.prepare( + format!( + "SELECT * FROM tbl_ur_restrictive_airspace WHERE (restrictive_airspace_designation, icao_code) IN \ + ({range_query})" + ) + .as_str(), + )?; + + // No changes since v1, able to use same struct + let airspaces_data = util::fetch_rows::(&mut stmt, [])?; + + Ok(map_restrictive_airspaces(airspaces_data)) + } + + fn get_communications_in_range( + &self, + center: Coordinates, + range: NauticalMiles, + ) -> Result, Box> { + let conn = self.get_database()?; + + let where_string = util::range_query_where(center, range, ""); + + let mut enroute_stmt = conn.prepare( + format!("SELECT * FROM tbl_ev_enroute_communication WHERE {where_string}").as_str(), + )?; + + let mut terminal_stmt = conn.prepare( + format!("SELECT * FROM tbl_pv_airport_communication WHERE {where_string}").as_str(), + )?; + + let enroute_data = + util::fetch_rows::(&mut enroute_stmt, [])?; + let terminal_data = + util::fetch_rows::(&mut terminal_stmt, [])?; + + Ok(enroute_data + .into_iter() + .map(Communication::from) + .chain(terminal_data.into_iter().map(Communication::from)) + .filter(|waypoint| waypoint.location.distance_to(¢er) <= range) + .collect()) + } + + fn get_runways_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pg_runways WHERE airport_identifier = (?1)")?; + + let runways_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(runways_data.into_iter().map(Into::into).collect()) + } + + fn get_departures_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut departures_stmt = + conn.prepare("SELECT * FROM tbl_pd_sids WHERE airport_identifier = (?1)")?; + + let mut runways_stmt = + conn.prepare("SELECT * FROM tbl_pg_runways WHERE airport_identifier = (?1)")?; + + let departures_data = util::fetch_rows::( + &mut departures_stmt, + params![airport_ident], + )?; + let runways_data = util::fetch_rows::( + &mut runways_stmt, + params![airport_ident], + )?; + + Ok(map_departures_v2(departures_data, runways_data)) + } + + fn get_arrivals_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut arrivals_stmt = + conn.prepare("SELECT * FROM tbl_pe_stars WHERE airport_identifier = (?1)")?; + + let mut runways_stmt = + conn.prepare("SELECT * FROM tbl_pg_runways WHERE airport_identifier = (?1)")?; + + let arrivals_data = util::fetch_rows::( + &mut arrivals_stmt, + params![airport_ident], + )?; + let runways_data = util::fetch_rows::( + &mut runways_stmt, + params![airport_ident], + )?; + + Ok(map_arrivals_v2(arrivals_data, runways_data)) + } + + fn get_approaches_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut approachs_stmt = + conn.prepare("SELECT * FROM tbl_pf_iaps WHERE airport_identifier = (?1)")?; + + let approaches_data = util::fetch_rows::( + &mut approachs_stmt, + params![airport_ident], + )?; + + Ok(map_approaches_v2(approaches_data)) + } + + fn get_waypoints_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pc_terminal_waypoints WHERE region_code = (?1)")?; + + let waypoints_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(waypoints_data.into_iter().map(Waypoint::from).collect()) + } + + fn get_ndb_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn + .prepare("SELECT * FROM tbl_pn_terminal_ndbnavaids WHERE airport_identifier = (?1)")?; + + let waypoints_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(waypoints_data.into_iter().map(NdbNavaid::from).collect()) + } + + fn get_gates_at_airport(&self, airport_ident: String) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pb_gates WHERE airport_identifier = (?1)")?; + + // Same as v1, same struct can be used + let gates_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(Gate::from).collect()) + } + + fn get_communications_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare( + "SELECT * FROM tbl_pv_airport_communication WHERE airport_identifier = (?1)", + )?; + + let gates_data = util::fetch_rows::( + &mut stmt, + params![airport_ident], + )?; + + Ok(gates_data.into_iter().map(Communication::from).collect()) + } + + fn get_gls_navaids_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = conn.prepare("SELECT * FROM tbl_pt_gls WHERE airport_identifier = (?1)")?; + + let gates_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(GlsNavaid::from).collect()) + } + + fn get_path_points_at_airport( + &self, + airport_ident: String, + ) -> Result, Box> { + let conn = self.get_database()?; + + let mut stmt = + conn.prepare("SELECT * FROM tbl_pp_pathpoint WHERE airport_identifier = (?1)")?; + + let gates_data = + util::fetch_rows::(&mut stmt, params![airport_ident])?; + + Ok(gates_data.into_iter().map(PathPoint::from).collect()) + } +} + +// Empty Connection diff --git a/src/database/src/v2/mod.rs b/src/database/src/v2/mod.rs new file mode 100644 index 00000000..79c184b2 --- /dev/null +++ b/src/database/src/v2/mod.rs @@ -0,0 +1,2 @@ +pub mod database; +pub mod sql_structs; diff --git a/src/database/src/v2/sql_structs.rs b/src/database/src/v2/sql_structs.rs new file mode 100644 index 00000000..9bbeb2e9 --- /dev/null +++ b/src/database/src/v2/sql_structs.rs @@ -0,0 +1,533 @@ +use serde::Deserialize; + +use crate::enums::{ + AirwayDirection, AirwayLevel, AirwayRouteType, AltitudeDescriptor, ApproachTypeIdentifier, + CommunicationType, FrequencyUnits, IfrCapability, LegType, RunwayLights, RunwaySurface, + RunwaySurfaceCode, SpeedDescriptor, TrafficPattern, TurnDirection, +}; + +#[derive(Deserialize, Debug)] +pub struct AirportCommunication { + pub airport_identifier: String, + pub area_code: String, + pub callsign: Option, + pub communication_frequency: f64, + pub communication_type: CommunicationType, + pub frequency_units: FrequencyUnits, + pub guard_transmit: Option, // new + pub icao_code: String, + pub latitude: f64, + pub longitude: f64, + pub narritive: Option, // new + pub remote_facility_icao_code: Option, // new + pub remote_facility: Option, // new + pub sector_facility_icao_code: Option, // new + pub sector_facility: Option, // new + pub sectorization: Option, // new + pub service_indicator: Option, + pub time_of_operation_1: Option, // new + pub time_of_operation_2: Option, // new + pub time_of_operation_3: Option, // new + pub time_of_operation_4: Option, // new + pub time_of_operation_5: Option, // new + pub time_of_operation_6: Option, // new + pub time_of_operation_7: Option, // new +} + +#[derive(Deserialize, Debug)] +#[allow(unused_variables)] +pub struct AirportMsa { + pub area_code: Option, + pub icao_code: Option, + pub airport_identifier: Option, + pub msa_center: Option, + pub msa_center_latitude: Option, + pub msa_center_longitude: Option, + pub magnetic_true_indicator: Option, + pub multiple_code: Option, + pub radius_limit: Option, + pub sector_bearing_1: Option, + pub sector_altitude_1: Option, + pub sector_bearing_2: Option, + pub sector_altitude_2: Option, + pub sector_bearing_3: Option, + pub sector_altitude_3: Option, + pub sector_bearing_4: Option, + pub sector_altitude_4: Option, + pub sector_bearing_5: Option, + pub sector_altitude_5: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Airports { + pub airport_identifier: String, + pub airport_name: String, + pub airport_ref_latitude: f64, + pub airport_ref_longitude: f64, + pub airport_type: String, + pub area_code: String, + pub ata_iata_code: Option, + pub city: Option, + pub continent: Option, + pub country: Option, + pub country_3letter: Option, + pub elevation: f64, + pub fuel: Option, + pub icao_code: String, + pub ifr_capability: Option, + pub longest_runway_surface_code: RunwaySurfaceCode, + pub magnetic_variation: Option, + pub speed_limit: Option, + pub speed_limit_altitude: Option, + pub state: Option, + pub state_2letter: Option, + pub transition_altitude: Option, + pub transition_level: Option, + pub airport_identifier_3letter: Option, +} + +#[derive(Deserialize, Debug)] +pub struct CruisingTables { + pub cruise_table_identifier: Option, + pub seqno: Option, + pub course_from: Option, + pub course_to: Option, + pub mag_true: Option, + pub cruise_level_from1: Option, + pub vertical_separation1: Option, + pub cruise_level_to1: Option, + pub cruise_level_from2: Option, + pub vertical_separation2: Option, + pub cruise_level_to2: Option, + pub cruise_level_from3: Option, + pub vertical_separation3: Option, + pub cruise_level_to3: Option, + pub cruise_level_from4: Option, + pub vertical_separation4: Option, + pub cruise_level_to4: Option, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteAirwayRestriction { + pub area_code: Option, + pub route_identifier: Option, + pub restriction_identifier: Option, + pub restriction_type: Option, + pub start_waypoint_identifier: Option, + pub start_waypoint_latitude: Option, + pub start_waypoint_longitude: Option, + pub end_waypoint_identifier: Option, + pub end_waypoint_latitude: Option, + pub end_waypoint_longitude: Option, + pub start_date: Option, + pub end_date: Option, + pub units_of_altitude: Option, + pub restriction_altitude1: Option, + pub block_indicator1: Option, + pub restriction_altitude2: Option, + pub block_indicator2: Option, + pub restriction_altitude3: Option, + pub block_indicator3: Option, + pub restriction_altitude4: Option, + pub block_indicator4: Option, + pub restriction_altitude5: Option, + pub block_indicator5: Option, + pub restriction_altitude6: Option, + pub block_indicator6: Option, + pub restriction_altitude7: Option, + pub block_indicator7: Option, + pub restriction_notes: Option, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteAirways { + pub area_code: String, + pub crusing_table_identifier: Option, + pub direction_restriction: Option, + pub flightlevel: Option, + pub icao_code: Option, + pub inbound_course: Option, + pub inbound_distance: Option, + pub maximum_altitude: Option, + pub minimum_altitude1: Option, + pub minimum_altitude2: Option, + pub outbound_course: Option, + pub route_identifier: Option, + pub route_identifier_postfix: Option, + pub route_type: Option, + pub seqno: Option, + pub waypoint_description_code: Option, + pub waypoint_identifier: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub waypoint_ref_table: String, +} + +#[derive(Deserialize, Debug)] +pub struct EnrouteCommunication { + pub area_code: String, + pub callsign: Option, + pub communication_frequency: f64, + pub communication_type: CommunicationType, + pub fir_rdo_ident: String, + pub fir_uir_indicator: Option, + pub frequency_units: FrequencyUnits, + pub latitude: f64, + pub longitude: f64, + pub remote_facility_icao_code: Option, // new + pub remote_facility: Option, // new + pub remote_name: Option, + pub service_indicator: Option, +} + +#[derive(Deserialize, Debug)] +pub struct FirUir { + pub area_code: Option, + pub fir_uir_identifier: Option, + pub fir_uir_address: Option, + pub fir_uir_name: Option, + pub fir_uir_indicator: Option, + pub seqno: Option, + pub boundary_via: Option, + pub adjacent_fir_identifier: Option, + pub adjacent_uir_identifier: Option, + pub reporting_units_speed: Option, + pub reporting_units_altitude: Option, + pub fir_uir_latitude: Option, + pub fir_uir_longitude: Option, + pub arc_origin_latitude: Option, + pub arc_origin_longitude: Option, + pub arc_distance: Option, + pub arc_bearing: Option, + pub fir_upper_limit: Option, + pub uir_lower_limit: Option, + pub uir_upper_limit: Option, + pub cruise_table_identifier: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Gate { + pub airport_identifier: String, + pub area_code: String, + pub gate_identifier: String, + pub gate_latitude: f64, + pub gate_longitude: f64, + pub icao_code: String, + pub name: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Gls { + pub area_code: String, + pub airport_identifier: String, + pub icao_code: String, + pub gls_ref_path_identifier: String, + pub gls_category: String, + pub gls_channel: f64, + pub runway_identifier: String, + pub gls_approach_bearing: f64, + pub station_latitude: f64, + pub station_longitude: f64, + pub gls_station_ident: String, + pub gls_approach_slope: f64, + pub magnetic_variation: f64, + pub station_elevation: f64, + pub station_type: Option, +} + +#[derive(Deserialize, Debug)] +pub struct GridMora { + pub starting_latitude: Option, + pub starting_longitude: Option, + pub mora01: Option, + pub mora02: Option, + pub mora03: Option, + pub mora04: Option, + pub mora05: Option, + pub mora06: Option, + pub mora07: Option, + pub mora08: Option, + pub mora09: Option, + pub mora10: Option, + pub mora11: Option, + pub mora12: Option, + pub mora13: Option, + pub mora14: Option, + pub mora15: Option, + pub mora16: Option, + pub mora17: Option, + pub mora18: Option, + pub mora19: Option, + pub mora20: Option, + pub mora21: Option, + pub mora22: Option, + pub mora23: Option, + pub mora24: Option, + pub mora25: Option, + pub mora26: Option, + pub mora27: Option, + pub mora28: Option, + pub mora29: Option, + pub mora30: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Header { + pub creator: String, + pub cycle: String, + pub data_provider: String, + pub dataset_version: String, + pub dataset: String, + pub effective_fromto: String, + pub parsed_at: String, + pub revision: String, +} + +#[derive(Deserialize, Debug)] +pub struct Holdings { + pub area_code: Option, + pub region_code: Option, + pub icao_code: Option, + pub waypoint_identifier: Option, + pub holding_name: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub duplicate_identifier: Option, + pub inbound_holding_course: Option, + pub turn_direction: Option, + pub leg_length: Option, + pub leg_time: Option, + pub minimum_altitude: Option, + pub maximum_altitude: Option, + pub holding_speed: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Procedures { + pub airport_identifier: String, + pub altitude_description: Option, + pub altitude1: Option, + pub altitude2: Option, + pub arc_radius: Option, + pub area_code: String, + pub authorization_required: Option, // new + pub center_waypoint_icao_code: Option, // new + pub center_waypoint_latitude: Option, + pub center_waypoint_longitude: Option, + pub center_waypoint_ref_table: String, // new + pub center_waypoint: Option, + pub course_flag: Option, // new + pub course: Option, // new + pub distance_time: Option, + pub path_termination: LegType, + pub procedure_identifier: String, + pub recommended_navaid_icao_code: Option, // new + pub recommended_navaid_latitude: Option, + pub recommended_navaid_longitude: Option, + pub recommended_navaid_ref_table: String, // new + pub recommended_navaid: Option, + pub rho: Option, + pub rnp: Option, + pub route_distance_holding_distance_time: Option, + pub route_type: String, + pub seqno: f64, + pub speed_limit_description: Option, + pub speed_limit: Option, + pub theta: Option, + pub transition_altitude: Option, + pub transition_identifier: Option, + pub turn_direction: Option, + pub vertical_angle: Option, + pub waypoint_description_code: Option, + pub waypoint_icao_code: Option, + pub waypoint_identifier: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub waypoint_ref_table: String, // new +} + +#[derive(Deserialize, Debug)] +pub struct LocalizerMarker { + pub area_code: String, + pub icao_code: String, + pub airport_identifier: String, + pub runway_identifier: String, + pub llz_identifier: String, + pub marker_identifier: String, + pub marker_type: String, + pub marker_latitude: f64, + pub marker_longitude: f64, +} + +#[derive(Deserialize, Debug)] +pub struct LocalizersGlideslopes { + pub area_code: Option, + pub icao_code: Option, + pub airport_identifier: String, + pub runway_identifier: Option, + pub llz_identifier: String, + pub llz_latitude: Option, + pub llz_longitude: Option, + pub llz_frequency: Option, + pub llz_bearing: Option, + pub llz_width: Option, + pub ils_mls_gls_category: Option, + pub gs_latitude: Option, + pub gs_longitude: Option, + pub gs_angle: Option, + pub gs_elevation: Option, + pub station_declination: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Pathpoints { + pub airport_icao_code: String, + pub airport_identifier: String, + pub approach_performance_designator: Option, // new + pub approach_procedure_ident: String, + pub approach_type_identifier: ApproachTypeIdentifier, + pub area_code: String, + pub course_width_at_threshold: f64, + pub flight_path_alignment_point_latitude: f64, + pub flight_path_alignment_point_longitude: f64, + pub glide_path_angle: f64, + pub gnss_channel_number: f64, + pub hal: f64, + pub landing_threshold_point_latitude: f64, + pub landing_threshold_point_longitude: f64, + pub length_offset: Option, + pub ltp_ellipsoid_height: f64, + pub operations_type: Option, // new + pub path_point_tch: f64, + pub reference_path_data_selector: Option, // new + pub reference_path_identifier: String, + pub route_indicator: Option, + pub runway_identifier: String, + pub sbas_service_provider_identifier: f64, + pub tch_units_indicator: String, + pub val: f64, +} + +#[derive(Deserialize, Debug)] +pub struct Runways { + pub airport_identifier: String, + pub area_code: Option, + pub displaced_threshold_distance: Option, + pub icao_code: Option, + pub landing_threshold_elevation: f64, + pub llz_identifier: Option, + pub llz_mls_gls_category: Option, + pub part_time_lights: Option, + pub runway_gradient: Option, + pub runway_identifier: String, + pub runway_latitude: Option, + pub runway_length: f64, + pub runway_lights: Option, // new + pub runway_longitude: Option, + pub runway_magnetic_bearing: Option, + pub runway_true_bearing: Option, + pub runway_width: f64, + pub surface_code: Option, + pub threshold_crossing_height: Option, + pub traffic_pattern: Option, // new +} + +#[derive(Deserialize, Debug)] +pub struct Sids { + pub area_code: Option, + pub airport_identifier: Option, + pub procedure_identifier: Option, + pub route_type: Option, + pub transition_identifier: Option, + pub seqno: Option, + pub waypoint_icao_code: Option, + pub waypoint_identifier: Option, + pub waypoint_latitude: Option, + pub waypoint_longitude: Option, + pub waypoint_description_code: Option, + pub turn_direction: Option, + pub rnp: Option, + pub path_termination: Option, + pub recommanded_navaid: Option, + pub recommanded_navaid_latitude: Option, + pub recommanded_navaid_longitude: Option, + pub arc_radius: Option, + pub theta: Option, + pub rho: Option, + pub magnetic_course: Option, + pub route_distance_holding_distance_time: Option, + pub distance_time: Option, + pub altitude_description: Option, + pub altitude1: Option, + pub altitude2: Option, + pub transition_altitude: Option, + pub speed_limit_description: Option, + pub speed_limit: Option, + pub vertical_angle: Option, + pub center_waypoint: Option, + pub center_waypoint_latitude: Option, + pub center_waypoint_longitude: Option, + pub aircraft_category: Option, + pub id: Option, + pub recommanded_id: Option, + pub center_id: Option, +} + +#[derive(Deserialize, Debug)] +pub struct NdbNavaids { + pub airport_identifier: Option, + pub area_code: String, + pub continent: Option, + pub country: Option, + pub datum_code: Option, + pub icao_code: Option, + pub magnetic_variation: Option, + pub navaid_class: String, + pub navaid_frequency: f64, + pub navaid_identifier: Option, + pub navaid_latitude: Option, + pub navaid_longitude: Option, + pub navaid_name: String, + pub range: Option, +} + +#[derive(Deserialize, Debug)] +pub struct Waypoints { + pub area_code: String, + pub continent: Option, + pub country: Option, + pub datum_code: Option, + pub icao_code: Option, + pub magnetic_varation: Option, + pub region_code: Option, + pub waypoint_identifier: String, + pub waypoint_latitude: f64, + pub waypoint_longitude: f64, + pub waypoint_name: String, + pub waypoint_type: String, + pub waypoint_usage: Option, +} + +#[derive(Deserialize, Debug)] +pub struct VhfNavaids { + pub airport_identifier: Option, + pub area_code: String, + pub continent: Option, + pub country: Option, + pub datum_code: Option, + pub dme_elevation: Option, + pub dme_ident: Option, + pub dme_latitude: Option, + pub dme_longitude: Option, + pub icao_code: Option, + pub ilsdme_bias: Option, + pub magnetic_variation: Option, + pub navaid_class: String, + pub navaid_frequency: f64, + pub navaid_identifier: String, + pub navaid_latitude: Option, + pub navaid_longitude: Option, + pub navaid_name: String, + pub range: Option, + pub station_declination: Option, +} diff --git a/src/js/interface/NavigationDataInterfaceTypes.ts b/src/js/interface/NavigationDataInterfaceTypes.ts index 5945cb61..26cf8058 100644 --- a/src/js/interface/NavigationDataInterfaceTypes.ts +++ b/src/js/interface/NavigationDataInterfaceTypes.ts @@ -25,7 +25,11 @@ export interface DownloadProgressData { export enum NavigraphFunction { DownloadNavigationData = "DownloadNavigationData", SetDownloadOptions = "SetDownloadOptions", - GetNavigationDataInstallStatus = "GetNavigationDataInstallStatus", + ListAvailablePackages = "ListPackages", + GetActivePackage = "GetActivePackage", + SetActivePackage = "SetActivePackage", + DeletePackage = "DeletePackage", + CleanPackages = "CleanPackages", ExecuteSQLQuery = "ExecuteSQLQuery", GetDatabaseInfo = "GetDatabaseInfo", GetAirport = "GetAirport", diff --git a/src/js/interface/NavigraphNavigationDataInterface.ts b/src/js/interface/NavigraphNavigationDataInterface.ts index 588852bc..f1310b89 100644 --- a/src/js/interface/NavigraphNavigationDataInterface.ts +++ b/src/js/interface/NavigraphNavigationDataInterface.ts @@ -12,13 +12,13 @@ import { GlsNavaid, NauticalMiles, NdbNavaid, + PackageInfo, PathPoint, RestrictiveAirspace, RunwayThreshold, VhfNavaid, Waypoint, } from "../types" -import { NavigationDataStatus } from "../types/meta" import { Callback, CommBusMessage, @@ -70,10 +70,11 @@ export class NavigraphNavigationDataInterface { * Downloads the navigation data from the given URL to the given path * * @param url - A valid signed URL to download the navigation data from + * @param setActive - Sets the newly downloaded package to active when complete * @returns A promise that resolves when the download is complete */ - public async download_navigation_data(url: string): Promise { - return await this.callWasmFunction("DownloadNavigationData", { url }) + public async download_navigation_data(url: string, setActive?: boolean): Promise { + return await this.callWasmFunction("DownloadNavigationData", { url, setActive }) } /** @@ -87,19 +88,58 @@ export class NavigraphNavigationDataInterface { } /** - * Gets the installation status of the navigation data + * Lists the available navigation data packages * - * @returns A promise that resolves with the installation status + * @param sort - Sets active package to the uuid + * @param filter - Sets active package to the uuid + * @returns A promise that resolves with the list of packages */ - public async get_navigation_data_install_status(): Promise { - return await this.callWasmFunction("GetNavigationDataInstallStatus", {}) + public async list_available_packages(sort?: boolean, filter?: boolean): Promise { + return await this.callWasmFunction("ListAvailablePackages", { sort, filter }) + } + + /** + * Sets the active package in the database + * + * @param uuid - Sets active package to the uuid + * @returns A promise that returns a bool that shows whether a new package was set or not + */ + public async set_active_package(uuid: string): Promise { + return await this.callWasmFunction("SetActivePackage", { uuid }) + } + + /** + * Deletes a package from the work folder + * + * @param uuid - UUID of the package to delete + * @returns A promise that returns void + */ + public async delete_package(uuid: string): Promise { + return await this.callWasmFunction("DeletePackage", { uuid }) + } + + /** + * Cleans up packages by deleting non activated formats (keeps bundled packages) + * + * @param count - Amount of packages of current format to leave + * @returns A promise that returns void + */ + public async clean_packages(count?: number): Promise { + return await this.callWasmFunction("CleanPackages", { count }) + } + + /** + * Gets the package information for the currently active package + */ + public async get_active_package(): Promise { + return await this.callWasmFunction("GetActivePackage", {}) } /** * Gets information about the currently active database */ - public async get_database_info(ident: string): Promise { - return await this.callWasmFunction("GetDatabaseInfo", { ident }) + public async get_database_info(): Promise { + return await this.callWasmFunction("GetDatabaseInfo", {}) } /** diff --git a/src/js/types/airport.ts b/src/js/types/airport.ts index c4cdf31b..cf9b1b92 100644 --- a/src/js/types/airport.ts +++ b/src/js/types/airport.ts @@ -1,4 +1,4 @@ -import { Coordinates, Feet, Knots } from "./math" +import { Coordinates, Degrees, Feet, Knots } from "./math" export enum IfrCapability { Yes = "Y", @@ -13,9 +13,16 @@ export enum RunwaySurfaceCode { } export interface Airport { + airport_type?: string area_code: string ident: string icao_code: string + city?: string + continent?: string + country?: string + country_3letter?: string + state?: string + state_2letter?: string location: Coordinates name: string ifr_capability: IfrCapability @@ -26,4 +33,5 @@ export interface Airport { speed_limit?: Knots speed_limit_altitude?: Feet iata_ident?: string + magnetic_variation?: Degrees } diff --git a/src/js/types/index.ts b/src/js/types/index.ts index 6e2be882..944d5503 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -14,3 +14,4 @@ export * from "./ProcedureLeg" export * from "./runway_threshold" export * from "./vhfnavaid" export * from "./waypoint" +export * from "./packages" diff --git a/src/js/types/meta.ts b/src/js/types/meta.ts deleted file mode 100644 index 79b3407a..00000000 --- a/src/js/types/meta.ts +++ /dev/null @@ -1,15 +0,0 @@ -export enum InstallStatus { - Bundled = "Bundled", - Manual = "Manual", - None = "None", -} - -export interface NavigationDataStatus { - status: InstallStatus - installedFormat: string | null - installedRevision: string | null - installedCycle: string | null - installedPath: string | null - validityPeriod: string | null - latestCycle: string | null -} diff --git a/src/js/types/ndb_navaid.ts b/src/js/types/ndb_navaid.ts index 10ad2f04..f2309226 100644 --- a/src/js/types/ndb_navaid.ts +++ b/src/js/types/ndb_navaid.ts @@ -1,11 +1,15 @@ -import { Coordinates, KiloHertz } from "./math" +import { Coordinates, KiloHertz, NauticalMiles } from "./math" export interface NdbNavaid { area_code: string + continent?: string + country?: string + datum_code?: string airport_ident?: string icao_code: string ident: string name: string frequency: KiloHertz location: Coordinates + range?: NauticalMiles } diff --git a/src/js/types/packages.ts b/src/js/types/packages.ts new file mode 100644 index 00000000..5686b895 --- /dev/null +++ b/src/js/types/packages.ts @@ -0,0 +1,14 @@ +export interface CycleInfo { + cycle: string + revision: string + name: string + format: string + validityPeriod: string +} + +export interface PackageInfo { + path: string + uuid: string + is_bundled: boolean + cycle: CycleInfo +} diff --git a/src/js/types/path_point.ts b/src/js/types/path_point.ts index 28d53cb2..398a4586 100644 --- a/src/js/types/path_point.ts +++ b/src/js/types/path_point.ts @@ -16,7 +16,7 @@ export interface PathPoint { ident: string landing_threshold_location: Coordinates ltp_ellipsoid_height: Metres - fpap_ellipsoid_height: Metres + fpap_ellipsoid_height?: Metres ltp_orthometric_height?: Metres fpap_orthometric_height?: Metres glidepath_angle: Degrees diff --git a/src/js/types/runway_threshold.ts b/src/js/types/runway_threshold.ts index a3d81dff..643dcfc1 100644 --- a/src/js/types/runway_threshold.ts +++ b/src/js/types/runway_threshold.ts @@ -7,7 +7,10 @@ export interface RunwayThreshold { width: Feet true_bearing: Degrees magnetic_bearing: Degrees + lights?: string gradient: Degrees location: Coordinates elevation: Feet + surface?: string + traffic_pattern?: string } diff --git a/src/js/types/vhfnavaid.ts b/src/js/types/vhfnavaid.ts index 24a1deca..bf3ac4db 100644 --- a/src/js/types/vhfnavaid.ts +++ b/src/js/types/vhfnavaid.ts @@ -1,12 +1,17 @@ -import { Coordinates, Degrees, MegaHertz } from "./math" +import { Coordinates, Degrees, MegaHertz, NauticalMiles } from "./math" export interface VhfNavaid { area_code: string airport_ident?: string + continent?: string + country?: string + datum_code?: string icao_code: string ident: string name: string frequency: MegaHertz location: Coordinates + magnetic_variation?: Degrees station_declination?: Degrees + range?: NauticalMiles } diff --git a/src/js/types/waypoint.ts b/src/js/types/waypoint.ts index 87d89dd5..c44f8ef8 100644 --- a/src/js/types/waypoint.ts +++ b/src/js/types/waypoint.ts @@ -1,10 +1,14 @@ -import { Coordinates } from "./math" +import { Coordinates, Degrees } from "./math" export interface Waypoint { area_code: string airport_ident?: string + continent?: string + country?: string + datum_code?: string icao_code: string ident: string name: string location: Coordinates + magnetic_variation?: Degrees } diff --git a/src/test/constants.ts b/src/test/constants.ts index 0886da0f..f1ebf7c7 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -1,2 +1,3 @@ export const WORK_FOLDER_PATH = "./test_work" export const WEBASSEMBLY_PATH = "./out/msfs_navigation_data_interface.wasm" +export const DEFAULT_DATA_PATH = "./examples/aircraft/PackageSources/bundled-navigation-data" diff --git a/src/test/package_management.spec.ts b/src/test/package_management.spec.ts new file mode 100644 index 00000000..0583b864 --- /dev/null +++ b/src/test/package_management.spec.ts @@ -0,0 +1,86 @@ +import { NavigraphNavigationDataInterface, PackageInfo } from "../js" + +const navigationDataInterface = new NavigraphNavigationDataInterface() + +describe("Package Management", () => { + // This will run once for each test file + beforeAll(async () => { + const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise => { + return new Promise((resolve, _reject) => { + navDataInterface.onReady(() => resolve()) + }) + } + + await waitForReady(navigationDataInterface) + }, 30000) + + it("List packages contains bundled items", async () => { + const packages = await navigationDataInterface.list_available_packages(); + + const bundledV1 = packages.find((item) => item.cycle.cycle === '2101' && item.cycle.format === 'dfd'); + const bundledV2 = packages.find((item) => item.cycle.cycle === '2401' && item.cycle.format === 'dfdv2'); + + expect(bundledV1).toStrictEqual({ + cycle: { + cycle: '2101', + format: 'dfd', + name: "Navigraph Avionics", + revision: "1", + validityPeriod: "2021-01-25/2021-02-20", + }, + is_bundled: true, + path: "\\work/navigation-data/269b26b0-ba1b-3859-a9c0-4484dc766233", + uuid: "269b26b0-ba1b-3859-a9c0-4484dc766233" + } satisfies PackageInfo) + + expect(bundledV2).toStrictEqual({ + cycle: { + cycle: '2401', + format: 'dfdv2', + name: "Navigraph Avionics", + revision: "1", + validityPeriod: "2024-01-25/2024-02-21", + }, + is_bundled: true, + path: "\\work/navigation-data/b8a9ecaa-a137-3059-b9f1-b7f2d5ddac37", + uuid: "b8a9ecaa-a137-3059-b9f1-b7f2d5ddac37" + } satisfies PackageInfo) + }) + + it("Clean up Packages", async () => { + const active = await navigationDataInterface.get_active_package(); + + await navigationDataInterface.clean_packages(); + + let packages = await navigationDataInterface.list_available_packages(); + + for (const item of packages) { + expect(item.is_bundled == true || item.cycle.format == active?.cycle.format).toBe(true); + } + + await navigationDataInterface.clean_packages(0); + + packages = await navigationDataInterface.list_available_packages(); + + for (const item of packages) { + expect(item.is_bundled == true || item.uuid == active?.uuid).toBe(true); + } + }, 40000); + + it("Delete packages", async () => { + const intialPackages = await navigationDataInterface.list_available_packages(); + const activePackage = await navigationDataInterface.get_active_package(); + + for (const item of intialPackages) { + await navigationDataInterface.delete_package(item.uuid); + + const newPackages = await navigationDataInterface.list_available_packages(); + + for (const newPackage of newPackages) { + expect(newPackage.uuid !== item.uuid || item.uuid === activePackage?.uuid).toBe(true) + } + } + + expect(await navigationDataInterface.list_available_packages()).toHaveLength(activePackage ? 1 : 0); + }, 30000); +}) \ No newline at end of file diff --git a/src/test/setup.ts b/src/test/setup.ts index 09588259..1d0f0dbb 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -3,7 +3,7 @@ import { argv, env } from "node:process" import { WASI } from "wasi" import { v4 } from "uuid" import { NavigraphNavigationDataInterface } from "../js" -import { WEBASSEMBLY_PATH, WORK_FOLDER_PATH } from "./constants" +import { DEFAULT_DATA_PATH, WEBASSEMBLY_PATH, WORK_FOLDER_PATH } from "./constants" import "dotenv/config" import { random } from "./randomBigint" @@ -84,6 +84,10 @@ function malloc(size: number): number { function readString(pointer: number): string { let lastChar = pointer + if (memoryBuffer.length == 0) { + return "" + } + while (memoryBuffer[lastChar] !== 0) { lastChar++ } @@ -144,6 +148,7 @@ const wasiSystem = new WASI({ env, preopens: { "\\work": WORK_FOLDER_PATH, + ".\\bundled-navigation-data": DEFAULT_DATA_PATH, }, }) @@ -161,7 +166,7 @@ const failedRequests: bigint[] = [] wasmInstance = new WebAssembly.Instance(wasmModule, { wasi_snapshot_preview1: Object.assign(wasiSystem.wasiImport, { - commit_pages: () => { }, // Empty implementation of this function as it is needed for the WASM module to properly load + commit_pages: () => {}, // Empty implementation of this function as it is needed for the WASM module to properly load }), env: { fsCommBusCall: (eventNamePointer: number, args: number) => { @@ -240,7 +245,7 @@ wasmInstance = new WebAssembly.Instance(wasmModule, { return 2 // FS_NETWORK_HTTP_REQUEST_STATE_WAITING_FOR_DATA } }, -}) as WasmInstance +}) as unknown as WasmInstance // Initially assign `memoryBuffer` to a new Uint8Array linked to the exported memoryBuffer memoryBuffer = new Uint8Array(wasmInstance.exports.memory.buffer) @@ -254,10 +259,10 @@ const fsContext = BigInt(0) wasmInstance.exports.navigation_data_interface_gauge_callback(fsContext, PanelService.PRE_INSTALL, 0) wasmInstance.exports.navigation_data_interface_gauge_callback(fsContext, PanelService.POST_INITIALIZE, 0) -const drawRate = 30 - let runLifecycle = true +const drawRate = 30 + /** * Runs the life cycle loop for the gauge * This only calls the PANEL_SERVICE_PRE_DRAW as of now as its the only function our wasm instance uses @@ -282,30 +287,12 @@ async function lifeCycle() { } } -// This will run once for each test file -beforeAll(async () => { - const navigationDataInterface = new NavigraphNavigationDataInterface() - - const downloadUrl = process.env.NAVIGATION_DATA_SIGNED_URL - - if (!downloadUrl) { - throw new Error("Please specify the env var `NAVIGATION_DATA_SIGNED_URL`") - } - - // Utility function to convert onReady to a promise - const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise => { - return new Promise((resolve, _reject) => { - navDataInterface.onReady(() => resolve()) - }) - } - - await waitForReady(navigationDataInterface) - - await navigationDataInterface.download_navigation_data(downloadUrl) -}, 30000) - void lifeCycle() +beforeAll(() => { + runLifecycle = true; +}) + // Cancel the lifeCycle after all tests have completed afterAll(() => { runLifecycle = false diff --git a/src/test/index.spec.ts b/src/test/v1.spec.ts similarity index 75% rename from src/test/index.spec.ts rename to src/test/v1.spec.ts index f29b38d3..0971a0fa 100644 --- a/src/test/index.spec.ts +++ b/src/test/v1.spec.ts @@ -6,6 +6,7 @@ import { FixType, IfrCapability, NavigraphNavigationDataInterface, + PackageInfo, RunwaySurfaceCode, } from "../js" import { ControlledAirspaceType, Path, PathType, RestrictiveAirspaceType } from "../js/types/airspace" @@ -24,14 +25,58 @@ import { Waypoint } from "../js/types/waypoint" const navigationDataInterface = new NavigraphNavigationDataInterface() -describe("test", () => { +describe("DFDv1", () => { + // This will run once for each test file + beforeAll(async () => { + const downloadUrl = process.env.NAVIGATION_DATA_SIGNED_URL ?? "local" + + const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise => { + return new Promise((resolve, _reject) => { + navDataInterface.onReady(() => resolve()) + }) + } + + await waitForReady(navigationDataInterface) + + if(downloadUrl === "local") { + let pkgs = await navigationDataInterface.list_available_packages(true, false) + + const target_package = pkgs.find((info) => info.cycle.format === 'dfd' && info.cycle.cycle === '2410') + + if(!target_package) { + throw new Error('V1 Database with cycle 2410 was not found in available packages') + } + + navigationDataInterface.set_active_package(target_package.uuid); + } else { + await navigationDataInterface.download_navigation_data(downloadUrl, true) + } + }, 30000) + + it("Active database", async () => { + const packageInfo = await navigationDataInterface.get_active_package() + + expect(packageInfo).toStrictEqual({ + is_bundled: !process.env.NAVIGATION_DATA_SIGNED_URL, + path: "\\work/navigation-data/active", + uuid: "481bc1fd-2712-3e42-9183-c4463ad1d952", + cycle: { + cycle: "2410", + revision: "1", + name: "Navigraph Avionics", + format: "dfd", + validityPeriod: "2024-10-03/2024-10-30", + }, + } satisfies PackageInfo) + }) + it("Database info", async () => { - const info = await navigationDataInterface.get_database_info("KJFK") + const info = await navigationDataInterface.get_database_info() expect(info).toStrictEqual({ - airac_cycle: "2313", - effective_from_to: ["28-12-2023", "25-01-2024"], - previous_from_to: ["30-11-2023", "28-12-2023"], + airac_cycle: "2410", + effective_from_to: ["03-10-2024", "31-10-2024"], + previous_from_to: ["05-09-2024", "03-10-2024"], } satisfies DatabaseInfo) }) @@ -89,6 +134,7 @@ describe("test", () => { long: 12.60829167, }, frequency: 110.5, + magnetic_variation: 5.1, name: "KASTRUP", } satisfies VhfNavaid) }) @@ -114,7 +160,7 @@ describe("test", () => { it("Get airports in range", async () => { const airports = await navigationDataInterface.get_airports_in_range({ lat: 51.468, long: -0.4551 }, 640) - expect(airports.length).toBe(1686) + expect(airports.length).toBe(1688) }) it("Get waypoints in range", async () => { @@ -136,7 +182,10 @@ describe("test", () => { }) it("Get controlled airspaces in range", async () => { - const airspaces = await navigationDataInterface.get_controlled_airspaces_in_range({ lat: -43.4876, long: 172.5374 }, 10) + const airspaces = await navigationDataInterface.get_controlled_airspaces_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) expect(airspaces.length).toBe(17) @@ -176,7 +225,10 @@ describe("test", () => { }) it("Get restrictive airspaces in range", async () => { - const airspaces = await navigationDataInterface.get_restrictive_airspaces_in_range({ lat: -43.4876, long: 172.5374 }, 10) + const airspaces = await navigationDataInterface.get_restrictive_airspaces_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) expect(airspaces.length).toBe(5) @@ -198,9 +250,12 @@ describe("test", () => { }) it("Get communications in range", async () => { - const communications = await navigationDataInterface.get_communications_in_range({ lat: -43.4876, long: 172.5374 }, 10) + const communications = await navigationDataInterface.get_communications_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) - expect(communications.length).toBe(46) + expect(communications.length).toBe(48) }) it("Get airways", async () => { @@ -208,9 +263,9 @@ describe("test", () => { const target_airway = airways[0] - expect(airways.length).toBe(5) + expect(airways.length).toBe(1) expect(airways[0].direction).toBeUndefined() - expect(target_airway.fixes.length).toBe(52) + expect(target_airway.fixes.length).toBe(53) expect(target_airway.ident).toBe("A1") expect(target_airway.level).toBe(AirwayLevel.Both) expect(target_airway.route_type).toBe(AirwayRouteType.OfficialDesignatedAirwaysExpectRnavAirways) @@ -263,18 +318,18 @@ describe("test", () => { it("Get departures", async () => { const departures = await navigationDataInterface.get_departures_at_airport("KLAX") - expect(departures.length).toBe(22) + expect(departures.length).toBe(24) const target_departure = departures.find(departure => departure.ident === "PNDAH2") - expect(target_departure.ident).toBe("PNDAH2") - expect(target_departure.runway_transitions.length).toBe(4) - expect(target_departure.enroute_transitions.length).toBe(2) - expect(target_departure.common_legs.length).toBe(4) - expect(target_departure.runway_transitions[0].ident).toBe("RW24L") - expect(target_departure.runway_transitions[0].legs.length).toBe(6) - expect(target_departure.enroute_transitions[0].ident).toBe("OTAYY") - expect(target_departure.enroute_transitions[0].legs.length).toBe(2) + expect(target_departure?.ident).toBe("PNDAH2") + expect(target_departure?.runway_transitions.length).toBe(4) + expect(target_departure?.enroute_transitions.length).toBe(2) + expect(target_departure?.common_legs.length).toBe(4) + expect(target_departure?.runway_transitions[0].ident).toBe("RW24L") + expect(target_departure?.runway_transitions[0].legs.length).toBe(6) + expect(target_departure?.enroute_transitions[0].ident).toBe("OTAYY") + expect(target_departure?.enroute_transitions[0].legs.length).toBe(2) }) it("Get Arrivals", async () => { @@ -284,14 +339,14 @@ describe("test", () => { const target_arrival = arrivals.find(arrival => arrival.ident === "BRUEN2") - expect(target_arrival.ident).toBe("BRUEN2") - expect(target_arrival.enroute_transitions.length).toBe(4) - expect(target_arrival.runway_transitions.length).toBe(4) - expect(target_arrival.common_legs.length).toBe(7) - expect(target_arrival.enroute_transitions[0].ident).toBe("ESTWD") - expect(target_arrival.enroute_transitions[0].legs.length).toBe(5) - expect(target_arrival.runway_transitions[0].ident).toBe("RW06L") - expect(target_arrival.runway_transitions[0].legs.length).toBe(8) + expect(target_arrival?.ident).toBe("BRUEN2") + expect(target_arrival?.enroute_transitions.length).toBe(4) + expect(target_arrival?.runway_transitions.length).toBe(4) + expect(target_arrival?.common_legs.length).toBe(7) + expect(target_arrival?.enroute_transitions[0].ident).toBe("ESTWD") + expect(target_arrival?.enroute_transitions[0].legs.length).toBe(5) + expect(target_arrival?.runway_transitions[0].ident).toBe("RW06L") + expect(target_arrival?.runway_transitions[0].legs.length).toBe(8) }) it("Get Approaches", async () => { @@ -301,20 +356,20 @@ describe("test", () => { const target_approach = approaches.find(approach => approach.ident === "I06L") - expect(target_approach.ident).toBe("I06L") - expect(target_approach.legs.length).toBe(3) - expect(target_approach.missed_legs.length).toBe(3) - expect(target_approach.runway_ident).toBe("RW06L") - expect(target_approach.approach_type).toBe(ApproachType.Ils) - expect(target_approach.transitions.length).toBe(3) - expect(target_approach.transitions[0].ident).toBe("CLVVR") - expect(target_approach.transitions[0].legs.length).toBe(2) + expect(target_approach?.ident).toBe("I06L") + expect(target_approach?.legs.length).toBe(3) + expect(target_approach?.missed_legs.length).toBe(3) + expect(target_approach?.runway_ident).toBe("RW06L") + expect(target_approach?.approach_type).toBe(ApproachType.Ils) + expect(target_approach?.transitions.length).toBe(3) + expect(target_approach?.transitions[0].ident).toBe("CLVVR") + expect(target_approach?.transitions[0].legs.length).toBe(2) }) it("Get waypoints at airport", async () => { const waypoints = await navigationDataInterface.get_waypoints_at_airport("NZCH") - expect(waypoints.length).toBe(200) + expect(waypoints.length).toBe(201) }) it("Get ndb navaids at airport", async () => { @@ -329,7 +384,7 @@ describe("test", () => { const approach1 = approaches.find(approach => approach.ident == "L21RZ") - const IF = approach1.legs[0] + const IF = approach1?.legs[0] expect(IF).toStrictEqual({ leg_type: LegType.IF, @@ -337,11 +392,11 @@ describe("test", () => { fix: { airport_ident: "GCLP", fix_type: FixType.Waypoint, - ident: "CF21R", + ident: "TIPUX", icao_code: "GC", location: { lat: 28.116, - long: -15.30502778, + long: -15.30505556, }, }, theta: 25.4, @@ -371,21 +426,21 @@ describe("test", () => { expect(gates[0]).toStrictEqual({ area_code: "SPA", icao_code: "NZ", - ident: "10", + ident: "45", location: { - lat: -43.49016944, - long: 172.53940833, + lat: -43.49171111, + long: 172.54092222, }, - name: "10", + name: "45", } satisfies Gate) }) it("Get communications at airport", async () => { const communications = await navigationDataInterface.get_communications_at_airport("NZCH") - expect(communications.length).toBe(14) + expect(communications.length).toBe(17) - expect(communications[0]).toStrictEqual({ + expect(communications[3]).toStrictEqual({ area_code: "SPA", airport_ident: "NZCH", communication_type: CommunicationType.ApproachControl, diff --git a/src/test/v2.spec.ts b/src/test/v2.spec.ts new file mode 100644 index 00000000..b66389b5 --- /dev/null +++ b/src/test/v2.spec.ts @@ -0,0 +1,535 @@ +import { + Airport, + AirwayLevel, + AirwayRouteType, + Fix, + FixType, + IfrCapability, + NavigraphNavigationDataInterface, + PackageInfo, + RunwaySurfaceCode, +} from "../js" +import { ControlledAirspaceType, Path, PathType, RestrictiveAirspaceType } from "../js/types/airspace" +import { Communication, CommunicationType, FrequencyUnits } from "../js/types/communication" +import { DatabaseInfo } from "../js/types/database_info" +import { Gate } from "../js/types/gate" +import { GlsNavaid } from "../js/types/gls_navaid" +import { NdbNavaid } from "../js/types/ndb_navaid" +import { ApproachTypeIdentifier, PathPoint } from "../js/types/path_point" +import { ApproachType } from "../js/types/procedure" +import { AltitudeDescriptor, LegType, TurnDirection } from "../js/types/ProcedureLeg" +import { IFLegData } from "../js/types/ProcedureLeg/IFLeg" +import { RunwayThreshold } from "../js/types/runway_threshold" +import { VhfNavaid } from "../js/types/vhfnavaid" +import { Waypoint } from "../js/types/waypoint" + +const navigationDataInterface = new NavigraphNavigationDataInterface() + +describe("DFDv2", () => { + // This will run once for each test file + beforeAll(async () => { + const downloadUrl = process.env.NAVIGATION_DATA_SIGNED_URL_V2 ?? "local" + + const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise => { + return new Promise((resolve, _reject) => { + navDataInterface.onReady(() => resolve()) + }) + } + + await waitForReady(navigationDataInterface) + + if(downloadUrl === "local") { + let pkgs = await navigationDataInterface.list_available_packages(true, false) + + const target_package = pkgs.find((info) => info.cycle.format === 'dfdv2' && info.cycle.cycle === '2410') + + if(!target_package) { + throw new Error('V2 Database with cycle 2410 was not found in available packages') + } + + navigationDataInterface.set_active_package(target_package.uuid); + } else { + await navigationDataInterface.download_navigation_data(downloadUrl, true) + } + }, 30000) + + it("Active database", async () => { + const packageInfo = await navigationDataInterface.get_active_package() + + expect(packageInfo).toStrictEqual({ + is_bundled: !process.env.NAVIGATION_DATA_SIGNED_URL, + path: "\\work/navigation-data/active", + uuid: "37735bb9-635b-37be-be1b-c5f9a89b7672", + cycle: { + cycle: "2410", + revision: "1", + name: "Navigraph Avionics", + format: "dfdv2", + validityPeriod: "2024-10-03/2024-10-30" + }, + } satisfies PackageInfo) + }) + + it("Database info", async () => { + const info = await navigationDataInterface.get_database_info() + + expect(info).toStrictEqual({ + airac_cycle: "2410", + effective_from_to: ["03-10-2024", "30-10-2024"], + previous_from_to: ["depricated", "depricated"], + } satisfies DatabaseInfo) + }) + + it("Fetch airport", async () => { + const airport = await navigationDataInterface.get_airport("KJFK") + + expect(airport).toStrictEqual({ + airport_type: "C", + area_code: "USA", + city: "NEW YORK", + continent: "NORTH AMERICA", + country: "UNITED STATES", + country_3letter: "USA", + ident: "KJFK", + icao_code: "K6", + location: { + lat: 40.63992777777778, + long: -73.77869166666666, + }, + name: "KENNEDY INTL", + ifr_capability: IfrCapability.Yes, + longest_runway_surface_code: RunwaySurfaceCode.Hard, + magnetic_variation: -13, + elevation: 13, + transition_altitude: 18000, + transition_level: 18000, + speed_limit: 250, + speed_limit_altitude: 10000, + state: "NEW YORK", + state_2letter: "NY", + iata_ident: "JFK", + } satisfies Airport) + }) + + it("Get waypoints", async () => { + const waypoints = await navigationDataInterface.get_waypoints("GLENN") + + expect(waypoints.length).toBe(3) + + expect(waypoints[0]).toStrictEqual({ + area_code: "SPA", + continent: "PACIFIC", + country: "NEW ZEALAND", + datum_code: "WGE", + icao_code: "NZ", + ident: "GLENN", + location: { + lat: -42.88116388888889, + long: 172.8397388888889, + }, + name: "GLENN", + } satisfies Waypoint) + }) + + it("Get vhf navaids", async () => { + const navaids = await navigationDataInterface.get_vhf_navaids("CH") + + expect(navaids.length).toBe(3) + + expect(navaids[0]).toStrictEqual({ + airport_ident: "EKCH", + area_code: "EUR", + continent: "EUROPE", + country: "DENMARK", + datum_code: "WGE", + icao_code: "EK", + ident: "CH", + location: { + lat: 55.59326388888889, + long: 12.608291666666666, + }, + frequency: 110.5, + name: "KASTRUP", + magnetic_variation: 5.1, + range: 25, + } satisfies VhfNavaid) + }) + + it("Get ndb navaids", async () => { + const navaids = await navigationDataInterface.get_ndb_navaids("CH") + + expect(navaids.length).toBe(4) + + expect(navaids[0]).toStrictEqual({ + area_code: "AFR", + continent: "AFRICA", + country: "MOZAMBIQUE", + datum_code: "WGE", + icao_code: "FQ", + ident: "CH", + location: { + lat: -19.10385, + long: 33.432947222222225, + }, + frequency: 282, + name: "CHIMOIO", + range: 75, + } satisfies NdbNavaid) + }) + + it("Get airports in range", async () => { + const airports = await navigationDataInterface.get_airports_in_range({ lat: 51.468, long: -0.4551 }, 640) + + expect(airports.length).toBe(1506) + }) + + it("Get waypoints in range", async () => { + const waypoints = await navigationDataInterface.get_waypoints_in_range({ lat: -43.4876, long: 172.5374 }, 10) + + expect(waypoints.length).toBe(126) + }) + + it("Get vhf navaids in range", async () => { + const vhf_navaids = await navigationDataInterface.get_vhf_navaids_in_range({ lat: -43.4876, long: 172.5374 }, 10) + + expect(vhf_navaids.length).toBe(1) + }) + + it("Get ndb navaids in range", async () => { + const ndb_navaids = await navigationDataInterface.get_ndb_navaids_in_range({ lat: -45.9282, long: 170.1981 }, 5) + + expect(ndb_navaids.length).toBe(1) + }) + + it("Get controlled airspaces in range", async () => { + const airspaces = await navigationDataInterface.get_controlled_airspaces_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) + + expect(airspaces.length).toBe(17) + + const target_airspace = airspaces[1] + + expect(target_airspace.airspace_center).toBe("NZCH") + expect(target_airspace.airspace_type).toBe(ControlledAirspaceType.TmaOrTca) + expect(target_airspace.area_code).toBe("SPA") + expect(target_airspace.icao_code).toBe("NZ") + expect(target_airspace.name).toBe("CHRISTCHURCH CTA/C") + expect(target_airspace.boundary_paths.length).toBe(11) + + expect(target_airspace.boundary_paths[0]).toStrictEqual({ + location: { + lat: -39.03916666666667, + long: 173.5413888888889, + }, + path_type: PathType.GreatCircle, + } satisfies Path) + + expect(target_airspace.boundary_paths[1]).toStrictEqual({ + location: { + lat: -40.77753611111111, + long: 172.74154166666668, + }, + arc: { + bearing: 288.9, + direction: TurnDirection.Left, + distance: 100, + origin: { + lat: -41.33722777777778, + long: 174.8169611111111, + }, + }, + path_type: PathType.Arc, + } satisfies Path) + }) + + it("Get restrictive airspaces in range", async () => { + const airspaces = await navigationDataInterface.get_restrictive_airspaces_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) + + expect(airspaces.length).toBe(5) + + const target_airspace = airspaces[0] + + expect(target_airspace.area_code).toBe("SPA") + expect(target_airspace.icao_code).toBe("NZ") + expect(target_airspace.name).toBe("WEST MELTON, CANTERBURY") + expect(target_airspace.airspace_type).toBe(RestrictiveAirspaceType.Danger) + expect(target_airspace.designation).toBe("827") + expect(target_airspace.boundary_paths.length).toBe(8) + expect(target_airspace.boundary_paths[0]).toStrictEqual({ + location: { + lat: -43.46666666666667, + long: 172.36977777777778, + }, + path_type: PathType.GreatCircle, + } satisfies Path) + }) + + it("Get communications in range", async () => { + const communications = await navigationDataInterface.get_communications_in_range( + { lat: -43.4876, long: 172.5374 }, + 10, + ) + + expect(communications.length).toBe(48) + }) + + it("Get airways", async () => { + const airways = await navigationDataInterface.get_airways("A1") + + const target_airway = airways[1] + + expect(airways.length).toBe(3) + expect(airways[0].direction).toBeUndefined() + expect(target_airway.fixes.length).toBe(36) + expect(target_airway.ident).toBe("A1") + expect(target_airway.level).toBe(AirwayLevel.Both) + expect(target_airway.route_type).toBe(AirwayRouteType.OfficialDesignatedAirwaysExpectRnavAirways) + expect(target_airway.fixes[0]).toStrictEqual({ + fix_type: FixType.VhfNavaid, + ident: "KEC", + icao_code: "RJ", + location: { + lat: 33.447741666666666, + long: 135.79449444444444, + }, + } satisfies Fix) + }) + + it("Get airways at fix", async () => { + const airways = await navigationDataInterface.get_airways_at_fix("ODOWD", "NZ") + + expect(airways.length).toBe(4) + }) + + it("Get airways in range", async () => { + const airways = await navigationDataInterface.get_airways_in_range({ lat: -43.4876, long: 172.5374 }, 10) + + expect(airways.length).toBe(27) + }) + + it("Get runways at airport", async () => { + const runways = await navigationDataInterface.get_runways_at_airport("NZCH") + + expect(runways.length).toBe(4) + + const target_runway = runways[0] + + expect(target_runway).toStrictEqual({ + icao_code: "NZ", + ident: "RW02", + elevation: 123, + gradient: -0.28, + length: 10787, + width: 148, + lights: "Y", + location: { + lat: -43.49763055555555, + long: 172.5221138888889, + }, + magnetic_bearing: 16, + true_bearing: 40.0, + surface: "BITU", + traffic_pattern: "L", + } satisfies RunwayThreshold) + }) + + it("Get departures", async () => { + const departures = await navigationDataInterface.get_departures_at_airport("KLAX") + + expect(departures.length).toBe(24) + + const target_departure = departures.find(departure => departure.ident === "PNDAH2") + + expect(target_departure?.ident).toBe("PNDAH2") + expect(target_departure?.runway_transitions.length).toBe(4) + expect(target_departure?.enroute_transitions.length).toBe(2) + expect(target_departure?.common_legs.length).toBe(4) + expect(target_departure?.runway_transitions[0].ident).toBe("RW24L") + expect(target_departure?.runway_transitions[0].legs.length).toBe(6) + expect(target_departure?.enroute_transitions[0].ident).toBe("OTAYY") + expect(target_departure?.enroute_transitions[0].legs.length).toBe(2) + }) + + it("Get Arrivals", async () => { + const arrivals = await navigationDataInterface.get_arrivals_at_airport("KLAX") + + expect(arrivals.length).toBe(24) + + const target_arrival = arrivals.find(arrival => arrival.ident === "BRUEN2") + + expect(target_arrival?.ident).toBe("BRUEN2") + expect(target_arrival?.enroute_transitions.length).toBe(4) + expect(target_arrival?.runway_transitions.length).toBe(4) + expect(target_arrival?.common_legs.length).toBe(7) + expect(target_arrival?.enroute_transitions[0].ident).toBe("ESTWD") + expect(target_arrival?.enroute_transitions[0].legs.length).toBe(5) + expect(target_arrival?.runway_transitions[0].ident).toBe("RW06L") + expect(target_arrival?.runway_transitions[0].legs.length).toBe(8) + }) + + it("Get Approaches", async () => { + const approaches = await navigationDataInterface.get_approaches_at_airport("KLAX") + + expect(approaches.length).toBe(24) + + const target_approach = approaches.find(approach => approach.ident === "I06L") + + expect(target_approach?.ident).toBe("I06L") + expect(target_approach?.legs.length).toBe(3) + expect(target_approach?.missed_legs.length).toBe(3) + expect(target_approach?.runway_ident).toBe("RW06L") + expect(target_approach?.approach_type).toBe(ApproachType.Ils) + expect(target_approach?.transitions.length).toBe(3) + expect(target_approach?.transitions[0].ident).toBe("CLVVR") + expect(target_approach?.transitions[0].legs.length).toBe(2) + }) + + it("Get waypoints at airport", async () => { + const waypoints = await navigationDataInterface.get_waypoints_at_airport("NZCH") + + expect(waypoints.length).toBe(201) + }) + + it("Get ndb navaids at airport", async () => { + const navaids = await navigationDataInterface.get_ndb_navaids_at_airport("EDDM") + + expect(navaids.length).toBe(4) + }) + + it("Check procedure leg types", async () => { + // This airport has the most different leg types + const approaches = await navigationDataInterface.get_approaches_at_airport("GCLP") + + const approach1 = approaches.find(approach => approach.ident == "L21RZ") + + const IF = approach1?.legs[0] + + expect(IF).toStrictEqual({ + leg_type: LegType.IF, + overfly: false, + fix: { + airport_ident: "GCLP", + fix_type: FixType.Waypoint, + ident: "TIPUX", + icao_code: "GC", + location: { + lat: 28.116, + long: -15.305055555555555, + }, + }, + theta: 25.4, + rho: 12.9, + altitude: { + altitude1: 2500, + descriptor: AltitudeDescriptor.AtOrAboveAlt1, + }, + recommended_navaid: { + airport_ident: "GCLP", + fix_type: FixType.IlsNavaid, + ident: "RLP", + icao_code: "GC", + location: { + lat: 27.915944444444445, + long: -15.393638888888889, + }, + }, + }) + }) + + it("Get gates at airport", async () => { + const gates = await navigationDataInterface.get_gates_at_airport("NZCH") + + expect(gates.length).toBe(48) + + expect(gates[0]).toStrictEqual({ + area_code: "SPA", + icao_code: "NZ", + ident: "10", + location: { + lat: -43.49016944444445, + long: 172.53940833333334, + }, + name: "N/A", + } satisfies Gate) + }) + + it("Get communications at airport", async () => { + const communications = await navigationDataInterface.get_communications_at_airport("NZCH") + + expect(communications.length).toBe(17) + + expect(communications[3]).toStrictEqual({ + area_code: "SPA", + airport_ident: "NZCH", + communication_type: CommunicationType.ApproachControl, + frequency: 126.1, + frequency_units: FrequencyUnits.VeryHigh, + callsign: "CHRISTCHURCH", + location: { + lat: -43.489444444444445, + long: 172.53444444444443, + }, + } satisfies Communication) + }) + + it("Get GlsNavaids at airport", async () => { + const communications = await navigationDataInterface.get_gls_navaids_at_airport("YSSY") + + expect(communications.length).toBe(6) + + expect(communications[0]).toStrictEqual({ + area_code: "SPA", + airport_ident: "YSSY", + icao_code: "YM", + ident: "G07A", + category: "1", + channel: 22790, + runway_ident: "RW07", + approach_angle: 3, + elevation: 21, + location: { + lat: -33.96333333333333, + long: 151.18477777777778, + }, + magnetic_approach_bearing: 62, + magnetic_variation: 13, + } satisfies GlsNavaid) + }) + + it("Get PathPoints at airport", async () => { + const pathpoints = await navigationDataInterface.get_path_points_at_airport("KLAX") + + expect(pathpoints.length).toBe(8) + + expect(pathpoints[0]).toStrictEqual({ + area_code: "USA", + airport_ident: "KLAX", + icao_code: "K2", + ident: "W06A", + runway_ident: "RW06L", + approach_ident: "R06LY", + approach_type: ApproachTypeIdentifier.LocalizerPerformanceVerticalGuidance, + course_width: 106.75, + flightpath_alignment_location: { + lat: 33.952133333333336, + long: -118.40162777777778, + }, + glidepath_angle: 3, + gnss_channel_number: 82507, + horizontal_alert_limit: 40, + vertical_alert_limit: 50, + landing_threshold_location: { + lat: 33.94911111111111, + long: -118.43115833333333, + }, + length_offset: 32, + ltp_ellipsoid_height: -1.5, + path_point_tch: 16.6725594664781, + } satisfies PathPoint) + }) +}) diff --git a/src/wasm/Cargo.toml b/src/wasm/Cargo.toml index 27b37d95..5a21f997 100644 --- a/src/wasm/Cargo.toml +++ b/src/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "msfs-navigation-data-interface" -version = "1.0.0-alpha.1" +version = "2.0.0" edition = "2021" [lib] @@ -10,5 +10,8 @@ crate-type = ["cdylib"] msfs = { git = "https://github.com/flybywiresim/msfs-rs.git", rev = "b438d3e" } serde = { version = "1.0.190", features = ["derive"] } serde_json = "1.0.108" +uuid = { version = "1.10.0", features = ["v3"] } zip = { version = "0.6.4", default-features = false, features = ["deflate"] } +enum_dispatch = "0.3.13" navigation_database = { path = "../database" } +libc = { version = "=0.2.164" } diff --git a/src/wasm/build.rs b/src/wasm/build.rs new file mode 100644 index 00000000..e0cc9a10 --- /dev/null +++ b/src/wasm/build.rs @@ -0,0 +1,10 @@ +use std::process::Command; + +fn main() { + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("Git is required to build"); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); +} diff --git a/src/wasm/src/consts.rs b/src/wasm/src/consts.rs index d43536a2..2930e7af 100644 --- a/src/wasm/src/consts.rs +++ b/src/wasm/src/consts.rs @@ -1,3 +1,4 @@ -pub const NAVIGATION_DATA_DEFAULT_LOCATION: &str = ".\\NavigationData"; -pub const NAVIGATION_DATA_WORK_LOCATION: &str = "\\work/NavigationData"; -pub const NAVIGATION_DATA_INTERNAL_CONFIG_LOCATION: &str = "\\work/navigraph_config.json"; +pub const NAVIGATION_DATA_DEFAULT_LOCATION: &str = ".\\bundled-navigation-data"; +pub const NAVIGATION_DATA_WORK_LOCATION: &str = "\\work/navigation-data"; +pub const NAVIGATION_DATA_OLD_WORK_LOCATION: &str = "\\work/NavigationData"; +pub const NAVIGATION_TEST_LOCATION: &str = "\\work/navigraph-test"; diff --git a/src/wasm/src/dispatcher.rs b/src/wasm/src/dispatcher.rs index 88edd976..eff26ffc 100644 --- a/src/wasm/src/dispatcher.rs +++ b/src/wasm/src/dispatcher.rs @@ -1,560 +1,966 @@ -use std::{cell::RefCell, path::Path, rc::Rc}; - -use msfs::{commbus::*, network::NetworkRequestState, sys::sGaugeDrawData, MSFSEvent}; -use navigation_database::database::Database; - -use crate::{ - consts, - download::downloader::{DownloadStatus, NavigationDataDownloader}, - json_structs::{ - events, - functions::{CallFunction, FunctionResult, FunctionStatus, FunctionType}, - params, - }, - meta::{self, InternalState}, - network_helper::NetworkHelper, - util::{self, path_exists}, -}; - -#[derive(PartialEq, Eq)] -pub enum TaskStatus { - NotStarted, - InProgress, - Success(Option), - Failure(String), -} - -pub struct Task { - pub function_type: FunctionType, - pub id: String, - pub data: Option, - pub status: TaskStatus, - pub associated_network_request: Option, -} - -impl Task { - pub fn parse_data_as(&self) -> Result> - where - T: serde::de::DeserializeOwned, - { - let data = self.data.clone().ok_or("No data provided")?; - let params = serde_json::from_value::(data)?; - Ok(params) - } -} - -pub struct Dispatcher<'a> { - commbus: CommBus<'a>, - downloader: Rc, - database: Database, - delta_time: std::time::Duration, - queue: Rc>>>>, -} - -impl<'a> Dispatcher<'a> { - pub fn new() -> Self { - Dispatcher { - commbus: CommBus::default(), - downloader: Rc::new(NavigationDataDownloader::new()), - database: Database::new(), - delta_time: std::time::Duration::from_secs(u64::MAX), /* Initialize to max so that we send a heartbeat on - * the first update */ - queue: Rc::new(RefCell::new(Vec::new())), - } - } - - pub fn on_msfs_event(&mut self, event: MSFSEvent) { - match event { - MSFSEvent::PostInitialize => { - self.handle_initialized(); - }, - MSFSEvent::PreDraw(data) => { - self.handle_update(data); - }, - MSFSEvent::PreKill => { - self.commbus.unregister_all(); - }, - - _ => {}, - } - } - - fn handle_initialized(&mut self) { - self.load_database(); - // We need to clone twice because we need to move the queue into the closure and then clone it again - // whenever it gets called - let captured_queue = Rc::clone(&self.queue); - self.commbus - .register("NAVIGRAPH_CallFunction", move |args| { - // TODO: maybe send error back to sim? - match Dispatcher::add_to_queue(Rc::clone(&captured_queue), args) { - Ok(_) => (), - Err(e) => println!("[NAVIGRAPH] Failed to add to queue: {}", e), - } - }) - .expect("Failed to register NAVIGRAPH_CallFunction"); - } - - fn handle_update(&mut self, data: &sGaugeDrawData) { - // Accumulate delta time for heartbeat - self.delta_time += data.delta_time(); - - // Send heartbeat every 5 seconds - if self.delta_time >= std::time::Duration::from_secs(5) { - Dispatcher::send_event(events::EventType::Heartbeat, None); - self.delta_time = std::time::Duration::from_secs(0); - } - - self.process_queue(); - self.downloader.on_update(); - - // Because the download process doesn't finish in the function call, we need to check if the download is finished to call the on_download_finish function - if *self.downloader.download_status.borrow() == DownloadStatus::Downloaded { - self.on_download_finish(); - self.downloader.acknowledge_download(); - } - } - fn load_database(&mut self) { - println!("[NAVIGRAPH] Loading database"); - - // Go through logic to determine which database to load - - // Are we bundled? None means we haven't installed anything yet - let is_bundled = meta::get_internal_state() - .map(|internal_state| Some(internal_state.is_bundled)) - .unwrap_or(None); - - // Get the installed cycle (if it exists) - let installed_cycle = match meta::get_installed_cycle_from_json( - &Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join("cycle.json"), - ) { - Ok(cycle) => Some(cycle.cycle), - Err(_) => None, - }; - - // Get the bundled cycle (if it exists) - let bundled_cycle = match meta::get_installed_cycle_from_json( - &Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION).join("cycle.json"), - ) { - Ok(cycle) => Some(cycle.cycle), - Err(_) => None, - }; - - // Determine if we are bundled ONLY and the bundled cycle is newer than the installed (old bundled) cycle - let bundled_updated = if is_bundled.is_some() && is_bundled.unwrap() { - if installed_cycle.is_some() && bundled_cycle.is_some() { - bundled_cycle.unwrap() > installed_cycle.unwrap() - } else { - false - } - } else { - false - }; - - // If there is no addon config, we can assume that we need to copy the bundled database to the work location - let need_to_copy = is_bundled.is_none(); - - // If we are bundled and the installed cycle is older than the bundled cycle, we need to copy the bundled database to the work location. Or if we haven't installed anything yet, we need to copy the bundled database to the work location - if bundled_updated || need_to_copy { - match util::copy_files_to_folder( - &Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION), - &Path::new(consts::NAVIGATION_DATA_WORK_LOCATION), - ) { - Ok(_) => { - // Set the internal state to bundled - let res = meta::set_internal_state(InternalState { is_bundled: true }); - if let Err(e) = res { - println!("[NAVIGRAPH] Failed to set internal state: {}", e); - } - }, - Err(e) => { - println!( - "[NAVIGRAPH] Failed to copy database from default location to work location: {}", - e - ); - return; - }, - } - } - - // Finally, set the active database - if path_exists(&Path::new(consts::NAVIGATION_DATA_WORK_LOCATION)) { - match self.database.set_active_database(consts::NAVIGATION_DATA_WORK_LOCATION.to_owned()) { - Ok(_) => { - println!("[NAVIGRAPH] Loaded database"); - }, - Err(e) => { - println!("[NAVIGRAPH] Failed to load database: {}", e); - }, - } - } else { - println!("[NAVIGRAPH] Failed to load database: there is no installed database"); - } - } - - fn on_download_finish(&mut self) { - match navigation_database::util::find_sqlite_file(consts::NAVIGATION_DATA_WORK_LOCATION) { - Ok(path) => { - match self.database.set_active_database(path) { - Ok(_) => {}, - Err(e) => { - println!("[NAVIGRAPH] Failed to set active database: {}", e); - }, - }; - }, - Err(_) => {}, - } - } - - fn process_queue(&mut self) { - let mut queue = self.queue.borrow_mut(); - - // Filter and update the status of the task that haven't started yet - for task in queue - .iter() - .filter(|task| task.borrow().status == TaskStatus::NotStarted) - { - task.borrow_mut().status = TaskStatus::InProgress; - - let function_type = task.borrow().function_type; - - match function_type { - FunctionType::DownloadNavigationData => { - // We can't use the execute_task function here because the download process doesn't finish in the - // function call, which results in slightly "messier" code - - // Close the database connection if it's open so we don't get any errors if we are replacing the - // database - self.database.close_connection(); - - // Now we can download the navigation data - self.downloader.download(Rc::clone(task)); - }, - FunctionType::SetDownloadOptions => { - Dispatcher::execute_task(task.clone(), |t| self.downloader.set_download_options(t)) - }, - FunctionType::GetNavigationDataInstallStatus => { - // We can't use the execute_task function here because the download process doesn't finish in the - // function call, which results in slightly "messier" code - - // We first need to initialize the network request and then wait for the response - meta::start_network_request(Rc::clone(task)) - }, - FunctionType::ExecuteSQLQuery => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let data = self.database.execute_sql_query(params.sql, params.params)?; - - t.borrow_mut().status = TaskStatus::Success(Some(data)); - - Ok(()) - }), - FunctionType::GetDatabaseInfo => Dispatcher::execute_task(task.clone(), |t| { - let info = self.database.get_database_info()?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(info)?)); - - Ok(()) - }), - FunctionType::GetAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airport = self.database.get_airport(params.ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airport)?)); - - Ok(()) - }), - FunctionType::GetWaypoints => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let waypoints = self.database.get_waypoints(params.ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); - - Ok(()) - }), - FunctionType::GetVhfNavaids => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let vhf_navaids = self.database.get_vhf_navaids(params.ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(vhf_navaids)?)); - - Ok(()) - }), - FunctionType::GetNdbNavaids => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let ndb_navaids = self.database.get_ndb_navaids(params.ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(ndb_navaids)?)); - - Ok(()) - }), - FunctionType::GetAirways => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airways = self.database.get_airways(params.ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airways)?)); - - Ok(()) - }), - FunctionType::GetAirwaysAtFix => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airways = self - .database - .get_airways_at_fix(params.fix_ident, params.fix_icao_code)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airways)?)); - - Ok(()) - }), - FunctionType::GetAirportsInRange => Dispatcher::execute_task(task.clone(), |t: Rc>| { - let params = t.borrow().parse_data_as::()?; - let airports = self.database.get_airports_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airports)?)); - - Ok(()) - }), - FunctionType::GetWaypointsInRange => Dispatcher::execute_task(task.clone(), |t: Rc>| { - let params = t.borrow().parse_data_as::()?; - let waypoints = self.database.get_waypoints_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); - - Ok(()) - }), - FunctionType::GetVhfNavaidsInRange => Dispatcher::execute_task(task.clone(), |t: Rc>| { - let params = t.borrow().parse_data_as::()?; - let navaids = self.database.get_vhf_navaids_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(navaids)?)); - - Ok(()) - }), - FunctionType::GetNdbNavaidsInRange => Dispatcher::execute_task(task.clone(), |t: Rc>| { - let params = t.borrow().parse_data_as::()?; - let navaids = self.database.get_ndb_navaids_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(navaids)?)); - - Ok(()) - }), - FunctionType::GetAirwaysInRange => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airways = self.database.get_airways_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airways)?)); - - Ok(()) - }), - FunctionType::GetControlledAirspacesInRange => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airspaces = self - .database - .get_controlled_airspaces_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airspaces)?)); - - Ok(()) - }), - FunctionType::GetRestrictiveAirspacesInRange => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let airspaces = self - .database - .get_restrictive_airspaces_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(airspaces)?)); - - Ok(()) - }), - FunctionType::GetCommunicationsInRange => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let communications = self.database.get_communications_in_range(params.center, params.range)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(communications)?)); - - Ok(()) - }), - FunctionType::GetRunwaysAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let runways = self.database.get_runways_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(runways)?)); - - Ok(()) - }), - FunctionType::GetDeparturesAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let departures = self.database.get_departures_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(departures)?)); - - Ok(()) - }), - FunctionType::GetArrivalsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let arrivals = self.database.get_arrivals_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(arrivals)?)); - - Ok(()) - }), - FunctionType::GetApproachesAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let arrivals = self.database.get_approaches_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(arrivals)?)); - - Ok(()) - }), - FunctionType::GetWaypointsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let waypoints = self.database.get_waypoints_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); - - Ok(()) - }), - FunctionType::GetNdbNavaidsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let navaids = self.database.get_ndb_navaids_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(navaids)?)); - - Ok(()) - }), - FunctionType::GetGatesAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let gates = self.database.get_gates_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(gates)?)); - - Ok(()) - }), - FunctionType::GetCommunicationsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let communications = self.database.get_communications_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(communications)?)); - - Ok(()) - }), - FunctionType::GetGlsNavaidsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let navaids = self.database.get_gls_navaids_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(navaids)?)); - - Ok(()) - }), - FunctionType::GetPathPointsAtAirport => Dispatcher::execute_task(task.clone(), |t| { - let params = t.borrow().parse_data_as::()?; - let pathpoints = self.database.get_path_points_at_airport(params.airport_ident)?; - - t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(pathpoints)?)); - - Ok(()) - }), - } - } - - // Network request tasks - for task in queue - .iter() - .filter(|task| task.borrow().status == TaskStatus::InProgress) - { - let response_state = match task.borrow().associated_network_request { - Some(ref request) => request.response_state(), - None => continue, - }; - let function_type = task.borrow().function_type; - if response_state == NetworkRequestState::DataReady { - match function_type { - FunctionType::GetNavigationDataInstallStatus => { - println!("[NAVIGRAPH] Network request completed, getting install status"); - meta::get_navigation_data_install_status(Rc::clone(task)); - println!("[NAVIGRAPH] Install status task completed"); - }, - _ => { - // Should not happen for now - println!("[NAVIGRAPH] Network request completed but no handler for this type of request"); - }, - } - } else if response_state == NetworkRequestState::Failed { - task.borrow_mut().status = TaskStatus::Failure("Network request failed".to_owned()); - } - } - - // Process completed tasks (everything should at least be in progress at this point) - queue.retain(|task| { - if let TaskStatus::InProgress = task.borrow().status { - return true; - } - - let status: FunctionStatus; - let data: Option; - - let (status, data) = match task.borrow().status { - TaskStatus::Success(ref result) => { - status = FunctionStatus::Success; - data = result.clone(); - (status, data) - }, - TaskStatus::Failure(ref error) => { - status = FunctionStatus::Error; - data = Some(error.clone().into()); - (status, data) - }, - _ => unreachable!(), // This should never happen - }; - - let json = FunctionResult { - id: task.borrow().id.clone(), - status, - data, - }; - - if let Ok(serialized_json) = serde_json::to_string(&json) { - CommBus::call("NAVIGRAPH_FunctionResult", &serialized_json, CommBusBroadcastFlags::All); - } - false - }); - } - - /// Executes a task and handles the result (sets the status of the task) - fn execute_task(task: Rc>, task_operation: F) - where - F: FnOnce(Rc>) -> Result<(), Box>, - { - match task_operation(task.clone()) { - Ok(_) => (), - Err(e) => { - println!("[NAVIGRAPH] Task failed: {}", e); - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - }, - } - } - - fn add_to_queue(queue: Rc>>>>, args: &str) -> Result<(), Box> { - let args = util::trim_null_terminator(args); - let json_result: CallFunction = serde_json::from_str(args)?; - - queue.borrow_mut().push(Rc::new(RefCell::new(Task { - function_type: json_result.function, - id: json_result.id, - data: json_result.data, - status: TaskStatus::NotStarted, - associated_network_request: None, - }))); - - Ok(()) - } - - pub fn send_event(event: events::EventType, data: Option) { - let json = events::Event { event, data }; - - if let Ok(serialized_json) = serde_json::to_string(&json) { - CommBus::call("NAVIGRAPH_Event", &serialized_json, CommBusBroadcastFlags::All); - } else { - println!("[NAVIGRAPH] Failed to serialize event"); - } - } -} +use std::{cell::RefCell, error::Error, fs, path::Path, rc::Rc}; + +use msfs::{commbus::*, sys::sGaugeDrawData, MSFSEvent}; +use navigation_database::{ + database::DatabaseV1, + enums::InterfaceFormat, + manual::database::DatabaseManual, + traits::{DatabaseEnum, DatabaseTrait, InstalledNavigationDataCycleInfo, PackageInfo}, + v2::database::DatabaseV2, +}; + +use crate::{ + consts, + download::downloader::{DownloadStatus, NavigationDataDownloader}, + json_structs::{ + events, + functions::{CallFunction, FunctionResult, FunctionStatus, FunctionType}, + params, + }, + util::{self, generate_uuid_from_cycle, generate_uuid_from_path, path_exists}, +}; + +#[derive(PartialEq, Eq)] +pub enum TaskStatus { + NotStarted, + InProgress, + Success(Option), + Failure(String), +} + +pub struct Task { + pub function_type: FunctionType, + pub id: String, + pub data: Option, + pub status: TaskStatus, +} + +impl Task { + pub fn parse_data_as(&self) -> Result> + where + T: serde::de::DeserializeOwned, + { + let data = self.data.clone().ok_or("No data provided")?; + let params = serde_json::from_value::(data)?; + Ok(params) + } +} + +pub struct Dispatcher<'a> { + commbus: CommBus<'a>, + downloader: Rc, + database: RefCell, + delta_time: std::time::Duration, + queue: Rc>>>>, + set_active_on_finish: RefCell, +} + +impl<'a> Dispatcher<'a> { + pub fn new(format: InterfaceFormat) -> Dispatcher<'a> { + Dispatcher { + commbus: CommBus::default(), + downloader: Rc::new(NavigationDataDownloader::new()), + database: match format { + InterfaceFormat::DFDv1 => RefCell::new(DatabaseV1::default().into()), + InterfaceFormat::DFDv2 => RefCell::new(DatabaseV2::default().into()), + InterfaceFormat::Custom => RefCell::new(DatabaseManual::default().into()), + }, + delta_time: std::time::Duration::from_secs(u64::MAX), /* Initialize to max so that we send a heartbeat on + * the first update */ + queue: Rc::new(RefCell::new(Vec::new())), + set_active_on_finish: RefCell::new(false), + } + } + + fn get_package_info(&self, path: &Path) -> Result> { + let cycle_file = fs::File::open(path.join("cycle.json"))?; + + let cycle: InstalledNavigationDataCycleInfo = serde_json::from_reader(cycle_file)?; + + let uuid = generate_uuid_from_cycle(&cycle); + + let bundled_path = Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION); + + let is_bundled = fs::read_dir(bundled_path).map_or(false, |mut directory| { + directory.any(|folder| { + folder.is_ok_and(|folder| { + generate_uuid_from_path(folder.path().join("cycle.json")) + .map_or(false, |x| x == uuid) + }) + }) + }); + + Ok(PackageInfo { + path: String::from(path.to_string_lossy()), + uuid, + is_bundled, + cycle, + }) + } + + fn list_packages(&self, sort: bool, filter: bool) -> Vec { + let navigation_data_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION); + + if !util::path_exists(navigation_data_path) { + fs::create_dir(navigation_data_path).unwrap(); + } + + let navigation_data_folder = fs::read_dir(navigation_data_path); + + let mut packages = vec![]; + + for file in navigation_data_folder.unwrap() { + let Ok(file) = file else { + continue; + }; + + match self.get_package_info(&file.path()) { + Ok(package_info) => packages.push(package_info), + Err(err) => eprintln!("{:?}", err), + } + } + + if filter { + let interface_type = self + .database + .borrow() + .get_database_type() + .as_str() + .to_string(); + + packages.retain(|package| *package.cycle.format == interface_type); + } + + if sort { + packages.sort_by(|a: &PackageInfo, b| { + b.cycle + .cycle + .cmp(&a.cycle.cycle) + .then(b.cycle.revision.cmp(&a.cycle.revision)) + .then(a.cycle.format.cmp(&b.cycle.format)) + }); + } + + packages + } + + fn set_package(&self, uuid: String) -> Result> { + let download_status = self.downloader.download_status.borrow().clone(); + + if download_status == DownloadStatus::Downloading { + return Err("Cannot do operations while downloading!".into()); + } + + let base_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION); + + let active_path = base_path.join("active"); + + let uuid_path = base_path.join(&uuid); + + if path_exists(&active_path) { + let package_info = self.get_package_info(&active_path)?; + + let hash = generate_uuid_from_cycle(&package_info.cycle); + + if hash == uuid { + return Ok(false); + } + + self.database.borrow_mut().disable_cycle()?; + + // Yes, this is really required for jest not to freeze + if path_exists(Path::new(consts::NAVIGATION_TEST_LOCATION)) { + util::delete_folder_recursively(&active_path, None)?; + } else { + // Disables the old path + match fs::rename(&active_path, base_path.join(hash)) { + Ok(_) => (), + Err(err) => eprintln!("{}", err), + } + } + } + + if !path_exists(&uuid_path) { + return Err("Package does not exist".into()); + } + + fs::rename(uuid_path, &active_path)?; + + let package_info = self.get_package_info(&active_path)?; + + // Check for format change and updates the used interface + if package_info.cycle.format != self.database.borrow().get_database_type().as_str() { + println!("Changing to: {}", package_info.cycle.format); + + let new_format = InterfaceFormat::from(&package_info.cycle.format); + + self.database.replace(match new_format { + InterfaceFormat::DFDv1 => DatabaseV1::default().into(), + InterfaceFormat::DFDv2 => DatabaseV2::default().into(), + InterfaceFormat::Custom => DatabaseManual::default().into(), + }); + } + + let db_set = self.database.borrow_mut().enable_cycle(&package_info)?; + + if db_set { + println!("[NAVIGRAPH]: Set Successful"); + } else { + println!("[NAVIGRAPH]: Set Unsuccessful"); + }; + + Ok(db_set) + } + + fn setup_packages(&self) -> Result> { + self.copy_old_data()?; + + self.copy_bundles()?; + + // Auto enable already activated cycle + let work_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION); + let active_path = work_path.join("active"); + + if path_exists(Path::new(consts::NAVIGATION_TEST_LOCATION)) { + // Testing shim + return Ok(String::from("Test Initalized")); + } else if path_exists(&active_path) { + let package = self.get_package_info(&active_path)?; + + if package.cycle.format != self.database.borrow().get_database_type().as_str() { + let new_format = InterfaceFormat::from(&package.cycle.format); + + self.database.replace(match new_format { + InterfaceFormat::DFDv1 => DatabaseV1::default().into(), + InterfaceFormat::DFDv2 => DatabaseV2::default().into(), + InterfaceFormat::Custom => DatabaseManual::default().into(), + }); + } + + self.database.borrow_mut().enable_cycle(&package)?; + } else { + let packages = self.list_packages(true, false); + + if packages.is_empty() { + return Err("No packages found to initialize".into()); + } + + // Unwrap here is protected + self.set_package(packages.into_iter().nth(0).unwrap().uuid)?; + } + + Ok(String::from("Packages Setup")) + } + + fn copy_bundles(&self) -> Result> { + let bundled_path = Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION); + + let package_list = self.list_packages(false, false); + + let uuid_list: Vec = package_list + .into_iter() + .map(|package| package.uuid) + .collect(); + + let mut active_uuid: String = String::new(); + + let active_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join("active"); + + if path_exists(&active_path) { + active_uuid = generate_uuid_from_path(active_path.join("cycle.json"))?; + } + + let Ok(bundled_dir) = fs::read_dir(bundled_path) else { + println!("[NAVIGRAPH]: No Bundled Data"); + return Ok(false); + }; + + for file in bundled_dir { + let Ok(file) = file else { + continue; + }; + + let cycle_path = file.path().join("cycle.json"); + + if !path_exists(&cycle_path) { + println!( + "[NAVIGRAPH]: Can't find cycle.json in {}", + file.path().to_string_lossy() + ); + continue; + } + + let cycle_hypenated = generate_uuid_from_path(cycle_path)?; + + // This should work, however it does not + if uuid_list.contains(&cycle_hypenated) { + continue; + } + + // This shall exist until I fix the copying bug, crashes sim, hard to debug manually. + if cycle_hypenated == active_uuid { + continue; + } + + let work_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join(cycle_hypenated); + + util::copy_files_to_folder(&file.path(), &work_path)?; + } + + Ok(true) + } + + fn copy_old_data(&self) -> Result<(), Box> { + let old_path = Path::new(consts::NAVIGATION_DATA_OLD_WORK_LOCATION); + + if !path_exists(old_path) { + return Ok(()); + } + + let new_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION); + + let old_uuid = util::generate_uuid_from_path(&old_path.join("cycle.json"))?; + + let package_list = self.list_packages(false, false); + + let uuid_list: Vec = package_list + .into_iter() + .map(|package| package.uuid) + .collect(); + + if uuid_list.contains(&old_uuid) { + return Ok(()); + } + + let fix_file = old_path.join("filethatfixeseverything"); + + if !util::path_exists(&fix_file) { + fs::File::create(fix_file)?; + } + + util::copy_files_to_folder(old_path, &new_path.join(old_uuid))?; + + util::delete_folder_recursively(old_path, None)?; + + Ok(()) + } + + fn delete_package(&self, uuid: String) -> Result<(), Box> { + let download_status = self.downloader.download_status.borrow().clone(); + + if download_status == DownloadStatus::Downloading { + return Err("Cannot do operations while downloading!".into()); + } + + let package_path = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join(uuid); + + match util::delete_folder_recursively(&package_path, None) { + Err(err) => Err(err.into()), + Ok(_) => Ok(()), + } + } + + fn clean_up_packages(&self, count_max: Option) -> Result<(), Box> { + let download_status = self.downloader.download_status.borrow().clone(); + + if download_status == DownloadStatus::Downloading { + return Err("Cannot do operations while downloading!".into()); + } + + let bundle_path = Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION); + + let mut bundle_ids = vec![]; + + for dir in bundle_path.read_dir()? { + let Ok(dir) = dir else { + continue; + }; + + bundle_ids.push(generate_uuid_from_path(dir.path().join("cycle.json"))?); + } + + let packages = self.list_packages(true, false); + + let mut count = 0; + + let (_keep, delete): (Vec, Vec) = + packages.into_iter().partition(|pkg| { + if (self.database.borrow().get_database_type().as_str() == pkg.cycle.format) + && (count <= count_max.unwrap_or(3)) + { + count += 1; + return true; + } else if bundle_ids.contains(&pkg.uuid) || pkg.path.contains("active") { + return true; + } + false + }); + + for pkg in delete { + self.delete_package(pkg.uuid)?; + } + + Ok(()) + } + + pub fn on_msfs_event(&mut self, event: MSFSEvent) { + match event { + MSFSEvent::PostInitialize => { + self.handle_initialized(); + } + MSFSEvent::PreDraw(data) => { + self.handle_update(data); + } + MSFSEvent::PreKill => { + self.commbus.unregister_all(); + } + + _ => {} + } + } + + fn handle_initialized(&mut self) { + // Runs before everything, used to set up the navdata in the right places. + match self.setup_packages() { + Ok(_) => (), + Err(x) => eprintln!("Packages failed to setup, Err: {}", x), + } + + // Runs extra setup on the configured database format handler + self.database.borrow().setup().unwrap(); + + // We need to clone twice because we need to move the queue into the closure and then clone it again + // whenever it gets called + let captured_queue = Rc::clone(&self.queue); + self.commbus + .register("NAVIGRAPH_CallFunction", move |args| { + // TODO: maybe send error back to sim? + match Dispatcher::add_to_queue(Rc::clone(&captured_queue), args) { + Ok(_) => (), + Err(e) => println!("[NAVIGRAPH] Failed to add to queue: {}", e), + } + }) + .expect("Failed to register NAVIGRAPH_CallFunction"); + } + + fn handle_update(&mut self, data: &sGaugeDrawData) { + // Accumulate delta time for heartbeat + self.delta_time += data.delta_time(); + + // Send heartbeat every 5 seconds + if self.delta_time >= std::time::Duration::from_secs(5) { + Dispatcher::send_event(events::EventType::Heartbeat, None); + self.delta_time = std::time::Duration::from_secs(0); + } + + self.process_queue(); + self.downloader.on_update(); + + // Because the download process doesn't finish in the function call, we need to check if the download is + // finished to call the on_download_finish function + let download_status = self.downloader.download_status.borrow().clone(); + + if let DownloadStatus::Downloaded(package_uuid) = download_status { + self.on_download_finish(package_uuid); + self.downloader.acknowledge_download(); + } + } + + fn on_download_finish(&mut self, package_uuid: String) { + if *self.set_active_on_finish.borrow() { + self.set_package(package_uuid).unwrap_or_default(); + self.set_active_on_finish.replace(false); + } + } + + fn process_queue(&mut self) { + let mut queue = self.queue.borrow_mut(); + + // Filter and update the status of the task that haven't started yet + for task in queue + .iter() + .filter(|task| task.borrow().status == TaskStatus::NotStarted) + { + task.borrow_mut().status = TaskStatus::InProgress; + + let function_type = task.borrow().function_type; + + match function_type { + FunctionType::DownloadNavigationData => { + // We can't use the execute_task function here because the download process doesn't finish in the + // function call, which results in slightly "messier" code + + // Get params for the set active when the download is finished + let params = task + .borrow() + .parse_data_as::() + .unwrap(); + + self.set_active_on_finish + .replace(params.set_active.unwrap_or(false)); + + self.downloader.download(Rc::clone(task)); + } + FunctionType::SetDownloadOptions => Dispatcher::execute_task(task.clone(), |t| { + self.downloader.set_download_options(t) + }), + FunctionType::GetActivePackage => Dispatcher::execute_task(task.clone(), |t| { + let active_path = + Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join("active"); + + let package = self.get_package_info(&active_path); + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(package.ok())?)); + + Ok(()) + }), + FunctionType::ListAvailablePackages => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t + .borrow() + .parse_data_as::()?; + + let packages = self.list_packages( + params.sort.unwrap_or(false), + params.filter.unwrap_or(false), + ); + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(packages)?)); + + Ok(()) + }) + } + FunctionType::SetActivePackage => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let data = self.set_package(params.uuid)?; + + t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(data)?)); + + Ok(()) + }), + FunctionType::DeletePackage => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + self.delete_package(params.uuid)?; + + t.borrow_mut().status = TaskStatus::Success(Some(().into())); + + Ok(()) + }), + FunctionType::CleanPackages => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + self.clean_up_packages(params.count)?; + + t.borrow_mut().status = TaskStatus::Success(Some(().into())); + + Ok(()) + }), + FunctionType::ExecuteSQLQuery => Dispatcher::execute_task(task.clone(), |t| { + let params = t + .borrow() + .parse_data_as::()?; + let data = self + .database + .borrow() + .execute_sql_query(params.sql, params.params)?; + + t.borrow_mut().status = TaskStatus::Success(Some(data)); + + Ok(()) + }), + FunctionType::GetDatabaseInfo => Dispatcher::execute_task(task.clone(), |t| { + let info = self.database.borrow().get_database_info()?; + + t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(info)?)); + + Ok(()) + }), + FunctionType::GetAirport => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airport = self.database.borrow().get_airport(params.ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airport)?)); + + Ok(()) + }), + FunctionType::GetWaypoints => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let waypoints = self.database.borrow().get_waypoints(params.ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); + + Ok(()) + }), + FunctionType::GetVhfNavaids => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let vhf_navaids = self.database.borrow().get_vhf_navaids(params.ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(vhf_navaids)?)); + + Ok(()) + }), + FunctionType::GetNdbNavaids => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let ndb_navaids = self.database.borrow().get_ndb_navaids(params.ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(ndb_navaids)?)); + + Ok(()) + }), + FunctionType::GetAirways => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airways = self.database.borrow().get_airways(params.ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airways)?)); + + Ok(()) + }), + FunctionType::GetAirwaysAtFix => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airways = self + .database + .borrow() + .get_airways_at_fix(params.fix_ident, params.fix_icao_code)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airways)?)); + + Ok(()) + }), + FunctionType::GetAirportsInRange => { + Dispatcher::execute_task(task.clone(), |t: Rc>| { + let params = t.borrow().parse_data_as::()?; + let airports = self + .database + .borrow() + .get_airports_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airports)?)); + + Ok(()) + }) + } + FunctionType::GetWaypointsInRange => { + Dispatcher::execute_task(task.clone(), |t: Rc>| { + let params = t.borrow().parse_data_as::()?; + let waypoints = self + .database + .borrow() + .get_waypoints_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); + + Ok(()) + }) + } + FunctionType::GetVhfNavaidsInRange => { + Dispatcher::execute_task(task.clone(), |t: Rc>| { + let params = t.borrow().parse_data_as::()?; + let navaids = self + .database + .borrow() + .get_vhf_navaids_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(navaids)?)); + + Ok(()) + }) + } + FunctionType::GetNdbNavaidsInRange => { + Dispatcher::execute_task(task.clone(), |t: Rc>| { + let params = t.borrow().parse_data_as::()?; + let navaids = self + .database + .borrow() + .get_ndb_navaids_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(navaids)?)); + + Ok(()) + }) + } + FunctionType::GetAirwaysInRange => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airways = self + .database + .borrow() + .get_airways_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airways)?)); + + Ok(()) + }), + FunctionType::GetControlledAirspacesInRange => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airspaces = self + .database + .borrow() + .get_controlled_airspaces_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airspaces)?)); + + Ok(()) + }) + } + FunctionType::GetRestrictiveAirspacesInRange => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let airspaces = self + .database + .borrow() + .get_restrictive_airspaces_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(airspaces)?)); + + Ok(()) + }) + } + FunctionType::GetCommunicationsInRange => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let communications = self + .database + .borrow() + .get_communications_in_range(params.center, params.range)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(communications)?)); + + Ok(()) + }) + } + FunctionType::GetRunwaysAtAirport => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let runways = self + .database + .borrow() + .get_runways_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(runways)?)); + + Ok(()) + }), + FunctionType::GetDeparturesAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let departures = self + .database + .borrow() + .get_departures_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(departures)?)); + + Ok(()) + }) + } + FunctionType::GetArrivalsAtAirport => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let arrivals = self + .database + .borrow() + .get_arrivals_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(arrivals)?)); + + Ok(()) + }), + FunctionType::GetApproachesAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let arrivals = self + .database + .borrow() + .get_approaches_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(arrivals)?)); + + Ok(()) + }) + } + FunctionType::GetWaypointsAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let waypoints = self + .database + .borrow() + .get_waypoints_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(waypoints)?)); + + Ok(()) + }) + } + FunctionType::GetNdbNavaidsAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let navaids = self + .database + .borrow() + .get_ndb_navaids_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(navaids)?)); + + Ok(()) + }) + } + FunctionType::GetGatesAtAirport => Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let gates = self + .database + .borrow() + .get_gates_at_airport(params.airport_ident)?; + + t.borrow_mut().status = TaskStatus::Success(Some(serde_json::to_value(gates)?)); + + Ok(()) + }), + FunctionType::GetCommunicationsAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let communications = self + .database + .borrow() + .get_communications_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(communications)?)); + + Ok(()) + }) + } + FunctionType::GetGlsNavaidsAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let navaids = self + .database + .borrow() + .get_gls_navaids_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(navaids)?)); + + Ok(()) + }) + } + FunctionType::GetPathPointsAtAirport => { + Dispatcher::execute_task(task.clone(), |t| { + let params = t.borrow().parse_data_as::()?; + let pathpoints = self + .database + .borrow() + .get_path_points_at_airport(params.airport_ident)?; + + t.borrow_mut().status = + TaskStatus::Success(Some(serde_json::to_value(pathpoints)?)); + + Ok(()) + }) + } + } + } + + // Process completed tasks (everything should at least be in progress at this point) + queue.retain(|task| { + if let TaskStatus::InProgress = task.borrow().status { + return true; + } + + let status: FunctionStatus; + let data: Option; + + let (status, data) = match task.borrow().status { + TaskStatus::Success(ref result) => { + status = FunctionStatus::Success; + data = result.clone(); + (status, data) + } + TaskStatus::Failure(ref error) => { + status = FunctionStatus::Error; + data = Some(error.clone().into()); + (status, data) + } + _ => unreachable!(), // This should never happen + }; + + let json = FunctionResult { + id: task.borrow().id.clone(), + status, + data, + }; + + if let Ok(serialized_json) = serde_json::to_string(&json) { + CommBus::call( + "NAVIGRAPH_FunctionResult", + &serialized_json, + CommBusBroadcastFlags::All, + ); + } + false + }); + } + + /// Executes a task and handles the result (sets the status of the task) + fn execute_task(task: Rc>, task_operation: F) + where + F: FnOnce(Rc>) -> Result<(), Box>, + { + match task_operation(task.clone()) { + Ok(_) => (), + Err(e) => { + println!("[NAVIGRAPH] Task failed: {}", e); + task.borrow_mut().status = TaskStatus::Failure(e.to_string()); + } + } + } + + fn add_to_queue( + queue: Rc>>>>, + args: &str, + ) -> Result<(), Box> { + let args = util::trim_null_terminator(args); + let json_result: CallFunction = serde_json::from_str(args)?; + + queue.borrow_mut().push(Rc::new(RefCell::new(Task { + function_type: json_result.function, + id: json_result.id, + data: json_result.data, + status: TaskStatus::NotStarted, + }))); + + Ok(()) + } + + pub fn send_event(event: events::EventType, data: Option) { + let json = events::Event { event, data }; + + if let Ok(serialized_json) = serde_json::to_string(&json) { + CommBus::call( + "NAVIGRAPH_Event", + &serialized_json, + CommBusBroadcastFlags::All, + ); + } else { + println!("[NAVIGRAPH] Failed to serialize event"); + } + } +} diff --git a/src/wasm/src/download/downloader.rs b/src/wasm/src/download/downloader.rs index ab651d60..66075679 100644 --- a/src/wasm/src/download/downloader.rs +++ b/src/wasm/src/download/downloader.rs @@ -1,273 +1,294 @@ -use std::{cell::RefCell, io::Cursor, path::PathBuf, rc::Rc}; - -use msfs::network::*; - -use crate::{ - consts, - dispatcher::{Dispatcher, Task, TaskStatus}, - download::zip_handler::{BatchReturn, ZipFileHandler}, - json_structs::{events, params}, - meta::{self, InternalState}, -}; - -pub struct DownloadOptions { - batch_size: usize, -} - -#[derive(PartialEq, Eq, Clone)] -pub enum DownloadStatus { - NoDownload, - Downloading, - CleaningDestination, - Extracting, - Downloaded, - Failed(String), -} - -pub struct NavigationDataDownloader { - zip_handler: RefCell>>>>, - pub download_status: RefCell, - options: RefCell, - task: RefCell>>>, -} - -impl NavigationDataDownloader { - pub fn new() -> Self { - NavigationDataDownloader { - zip_handler: RefCell::new(None), - download_status: RefCell::new(DownloadStatus::NoDownload), - options: RefCell::new(DownloadOptions { batch_size: 10 }), // default batch size - task: RefCell::new(None), - } - } - - pub fn on_update(&self) { - // Check if we failed and need to send an error message - if let Some(message) = self.check_failed_and_get_message() { - self.report_error(message); - self.reset_download(); - return; - } - - if self.should_extract_next_batch() { - match self.unzip_batch(self.options.borrow().batch_size) { - Ok(BatchReturn::Finished) => { - println!("[NAVIGRAPH] Finished extracting"); - // Scope to drop the borrow so we can reset the download - { - let borrowed_task = self.task.borrow(); - if (*borrowed_task).is_none() { - println!("[NAVIGRAPH] Request is none"); - return; - } - let mut borrowed_task = borrowed_task.as_ref().unwrap().borrow_mut(); - borrowed_task.status = TaskStatus::Success(None); - } - self.download_status.replace(DownloadStatus::Downloaded); - // Update the internal state - let res = meta::set_internal_state(InternalState { is_bundled: false }); - if let Err(e) = res { - println!("[NAVIGRAPH] Failed to set internal state: {}", e); - } - }, - Ok(BatchReturn::MoreFilesToDelete) => { - self.download_status.replace(DownloadStatus::CleaningDestination); - - let borrowed_zip_handler = self.zip_handler.borrow(); - if let Some(zip_handler) = borrowed_zip_handler.as_ref() { - self.send_progress_update(Some(zip_handler.deleted), None, None); - } - }, - Ok(BatchReturn::MoreFilesToUnzip) => { - self.download_status.replace(DownloadStatus::Extracting); - - let borrowed_zip_handler = self.zip_handler.borrow(); - if let Some(zip_handler) = borrowed_zip_handler.as_ref() { - self.send_progress_update( - None, - Some(zip_handler.zip_file_count), - Some(zip_handler.current_file_index), - ); - } - }, - Err(e) => { - println!("[NAVIGRAPH] Failed to unzip: {}", e); - self.report_error(e.to_string()); - self.reset_download(); - }, - } - } - } - - pub fn set_download_options(self: &Rc, task: Rc>) -> Result<(), Box> { - { - let params = task.borrow().parse_data_as::()?; - - // Set the options (only batch size for now) - let mut options = self.options.borrow_mut(); - options.batch_size = params.batch_size; - } - task.borrow_mut().status = TaskStatus::Success(None); - - Ok(()) - } - - /// Starts the download process - pub fn download(self: &Rc, task: Rc>) { - // Silently fail if we are already downloading (maybe we should send an error message?) - if *self.download_status.borrow() == DownloadStatus::Downloading { - println!("[NAVIGRAPH] Already downloading"); - return; - } else { - println!("[NAVIGRAPH] Downloading"); - self.download_status.replace(DownloadStatus::Downloading); - self.send_progress_update(None, None, None); - } - self.task.borrow_mut().replace(task.clone()); - - let params = match task.borrow().parse_data_as::() { - Ok(params) => params, - Err(e) => { - self.download_status - .replace(DownloadStatus::Failed(format!("Failed to parse params: {}", e))); - return; - }, - }; - - // Create the request - let captured_self = self.clone(); - println!("[NAVIGRAPH] Creating request"); - match NetworkRequestBuilder::new(¶ms.url) - .unwrap() - .with_callback(move |network_request, status_code| { - captured_self.request_finished_callback(network_request, status_code) - }) - .get() - { - Some(_) => (), - None => { - self.download_status - .replace(DownloadStatus::Failed("Failed to create request".to_string())); - }, - } - } - - /// Sends a status update to the client - fn send_progress_update(&self, deleted: Option, total_to_unzip: Option, unzipped: Option) { - let status = self.download_status.borrow(); - let phase: events::DownloadProgressPhase = match *status { - DownloadStatus::Downloading => events::DownloadProgressPhase::Downloading, - DownloadStatus::CleaningDestination => events::DownloadProgressPhase::Cleaning, - DownloadStatus::Extracting => events::DownloadProgressPhase::Extracting, - _ => return, // Don't send an update if we are not downloading - }; - let data = events::DownloadProgressEvent { - phase, - deleted, - total_to_unzip, - unzipped, - }; - let serialized_data = match serde_json::to_value(data) { - Ok(data) => data, - Err(e) => { - println!("[NAVIGRAPH] Failed to serialize download progress event: {}", e); - return; - }, - }; - Dispatcher::send_event(events::EventType::DownloadProgress, Some(serialized_data)); - } - - fn request_finished_callback(&self, request: NetworkRequest, status_code: i32) { - // Fail if the status code is not 200 - if status_code != 200 { - self.download_status.replace(DownloadStatus::Failed(format!( - "Failed to download file. Status code: {}", - status_code - ))); - return; - } - - let path = PathBuf::from(consts::NAVIGATION_DATA_WORK_LOCATION); - - // Check the data from the request - let data = request.data(); - if data.is_none() { - self.download_status - .replace(DownloadStatus::Failed("No data received".to_string())); - return; - } - // Extract the data from the request (safe to unwrap since we already checked if data was none) - let data = data.unwrap(); - let cursor = Cursor::new(data); - let zip = zip::ZipArchive::new(cursor); - if zip.is_err() { - self.download_status.replace(DownloadStatus::Failed(format!( - "Failed to create zip archive: {}", - zip.err().unwrap() - ))); - return; - } - // Unwrap is safe since we already checked if it was an error - let zip = zip.unwrap(); - - // Create the zip handler - let handler = ZipFileHandler::new(zip, path); - let mut zip_handler = self.zip_handler.borrow_mut(); - *zip_handler = Some(handler); - } - - pub fn unzip_batch(&self, batch_size: usize) -> Result> { - let mut zip_handler = self.zip_handler.borrow_mut(); - - let handler = zip_handler - .as_mut() - .ok_or_else(|| "Zip handler not found".to_string())?; - let res = handler.unzip_batch(batch_size)?; - - Ok(res) - } - - pub fn reset_download(&self) { - // Use the take method to replace the current value with None and drop the old value. - self.zip_handler.borrow_mut().take(); - - // Clear our task - self.task.replace(None); - } - - /// This must be called to clear the download status and reset the download - pub fn acknowledge_download(&self) { - self.download_status.replace(DownloadStatus::NoDownload); - - self.reset_download(); - } - - fn check_failed_and_get_message(&self) -> Option { - let borrowed_status = self.download_status.borrow(); - if let DownloadStatus::Failed(ref message) = *borrowed_status { - Some(message.clone()) - } else { - None - } - } - - fn report_error(&self, message: String) { - let borrowed_task = self.task.borrow(); - if (*borrowed_task).is_none() { - println!("[NAVIGRAPH] Task is none, but an error has been raised: {}", message); - return; - } - let mut borrowed_task = borrowed_task.as_ref().unwrap().borrow_mut(); - borrowed_task.status = TaskStatus::Failure(message.clone()); - } - - fn should_extract_next_batch(&self) -> bool { - let borrowed_zip_handler = self.zip_handler.borrow(); - if let Some(zip_handler) = borrowed_zip_handler.as_ref() { - zip_handler.zip_file_count > zip_handler.current_file_index - } else { - // If there is no zip handler, we are not downloading and we don't need to do anything - false - } - } -} +use std::{cell::RefCell, io::Cursor, path::PathBuf, rc::Rc}; + +use msfs::network::*; + +use crate::{ + consts, + dispatcher::{Dispatcher, Task, TaskStatus}, + download::zip_handler::{BatchReturn, ZipFileHandler}, + json_structs::{events, params}, +}; + +pub struct DownloadOptions { + batch_size: usize, +} + +#[derive(PartialEq, Eq, Clone)] +pub enum DownloadStatus { + NoDownload, + Downloading, + CleaningDestination, + Extracting, + Downloaded(String), + Failed(String), +} + +pub struct NavigationDataDownloader { + zip_handler: RefCell>>>>, + pub download_status: RefCell, + options: RefCell, + task: RefCell>>>, +} + +impl NavigationDataDownloader { + pub fn new() -> Self { + NavigationDataDownloader { + zip_handler: RefCell::new(None), + download_status: RefCell::new(DownloadStatus::NoDownload), + options: RefCell::new(DownloadOptions { batch_size: 10 }), // default batch size + task: RefCell::new(None), + } + } + + pub fn on_update(&self) { + // Check if we failed and need to send an error message + if let Some(message) = self.check_failed_and_get_message() { + self.report_error(message); + self.reset_download(); + return; + } + + if self.should_extract_next_batch() { + match self.unzip_batch(self.options.borrow().batch_size) { + Ok(BatchReturn::Finished(package_uuid)) => { + println!("[NAVIGRAPH] Finished extracting"); + // Scope to drop the borrow so we can reset the download + { + let borrowed_task = self.task.borrow(); + if (*borrowed_task).is_none() { + println!("[NAVIGRAPH] Request is none"); + return; + } + let mut borrowed_task = borrowed_task.as_ref().unwrap().borrow_mut(); + borrowed_task.status = TaskStatus::Success(None); + } + self.download_status + .replace(DownloadStatus::Downloaded(package_uuid)); + } + Ok(BatchReturn::MoreFilesToDelete) => { + self.download_status + .replace(DownloadStatus::CleaningDestination); + + let borrowed_zip_handler = self.zip_handler.borrow(); + if let Some(zip_handler) = borrowed_zip_handler.as_ref() { + self.send_progress_update(Some(zip_handler.deleted), None, None); + } + } + Ok(BatchReturn::MoreFilesToUnzip) => { + self.download_status.replace(DownloadStatus::Extracting); + + let borrowed_zip_handler = self.zip_handler.borrow(); + if let Some(zip_handler) = borrowed_zip_handler.as_ref() { + self.send_progress_update( + None, + Some(zip_handler.zip_file_count), + Some(zip_handler.current_file_index), + ); + } + } + Err(e) => { + println!("[NAVIGRAPH] Failed to unzip: {}", e); + self.report_error(e.to_string()); + self.reset_download(); + } + } + } + } + + pub fn set_download_options( + self: &Rc, + task: Rc>, + ) -> Result<(), Box> { + { + let params = task + .borrow() + .parse_data_as::()?; + + // Set the options (only batch size for now) + let mut options = self.options.borrow_mut(); + options.batch_size = params.batch_size; + } + task.borrow_mut().status = TaskStatus::Success(None); + + Ok(()) + } + + /// Starts the download process + pub fn download(self: &Rc, task: Rc>) { + // Silently fail if we are already downloading (maybe we should send an error message?) + if *self.download_status.borrow() == DownloadStatus::Downloading { + println!("[NAVIGRAPH] Already downloading"); + return; + } else { + println!("[NAVIGRAPH] Downloading"); + self.download_status.replace(DownloadStatus::Downloading); + self.send_progress_update(None, None, None); + } + self.task.borrow_mut().replace(task.clone()); + + let params = match task + .borrow() + .parse_data_as::() + { + Ok(params) => params, + Err(e) => { + self.download_status.replace(DownloadStatus::Failed(format!( + "Failed to parse params: {}", + e + ))); + return; + } + }; + + // Create the request + let captured_self = self.clone(); + println!("[NAVIGRAPH] Creating request"); + match NetworkRequestBuilder::new(¶ms.url) + .unwrap() + .with_callback(move |network_request, status_code| { + captured_self.request_finished_callback(network_request, status_code) + }) + .get() + { + Some(_) => (), + None => { + self.download_status.replace(DownloadStatus::Failed( + "Failed to create request".to_string(), + )); + } + } + } + + /// Sends a status update to the client + fn send_progress_update( + &self, + deleted: Option, + total_to_unzip: Option, + unzipped: Option, + ) { + let status = self.download_status.borrow(); + let phase: events::DownloadProgressPhase = match *status { + DownloadStatus::Downloading => events::DownloadProgressPhase::Downloading, + DownloadStatus::CleaningDestination => events::DownloadProgressPhase::Cleaning, + DownloadStatus::Extracting => events::DownloadProgressPhase::Extracting, + _ => return, // Don't send an update if we are not downloading + }; + let data = events::DownloadProgressEvent { + phase, + deleted, + total_to_unzip, + unzipped, + }; + let serialized_data = match serde_json::to_value(data) { + Ok(data) => data, + Err(e) => { + println!( + "[NAVIGRAPH] Failed to serialize download progress event: {}", + e + ); + return; + } + }; + Dispatcher::send_event(events::EventType::DownloadProgress, Some(serialized_data)); + } + + fn request_finished_callback(&self, request: NetworkRequest, status_code: i32) { + // Fail if the status code is not 200 + if status_code != 200 { + self.download_status.replace(DownloadStatus::Failed(format!( + "Failed to download file. Status code: {}", + status_code + ))); + return; + } + + let path = PathBuf::from(consts::NAVIGATION_DATA_WORK_LOCATION).join("temp"); + + // Check the data from the request + let data = request.data(); + if data.is_none() { + self.download_status + .replace(DownloadStatus::Failed("No data received".to_string())); + return; + } + // Extract the data from the request (safe to unwrap since we already checked if data was none) + let data = data.unwrap(); + let cursor = Cursor::new(data); + let zip = zip::ZipArchive::new(cursor); + if zip.is_err() { + self.download_status.replace(DownloadStatus::Failed(format!( + "Failed to create zip archive: {}", + zip.err().unwrap() + ))); + return; + } + // Unwrap is safe since we already checked if it was an error + let zip = zip.unwrap(); + + // Create the zip handler + let handler = ZipFileHandler::new(zip, path); + let mut zip_handler = self.zip_handler.borrow_mut(); + *zip_handler = Some(handler); + } + + pub fn unzip_batch( + &self, + batch_size: usize, + ) -> Result> { + let mut zip_handler = self.zip_handler.borrow_mut(); + + let handler = zip_handler + .as_mut() + .ok_or_else(|| "Zip handler not found".to_string())?; + let res = handler.unzip_batch(batch_size)?; + + Ok(res) + } + + pub fn reset_download(&self) { + // Use the take method to replace the current value with None and drop the old value. + self.zip_handler.borrow_mut().take(); + + // Clear our task + self.task.replace(None); + } + + /// This must be called to clear the download status and reset the download + pub fn acknowledge_download(&self) { + self.download_status.replace(DownloadStatus::NoDownload); + + self.reset_download(); + } + + fn check_failed_and_get_message(&self) -> Option { + let borrowed_status = self.download_status.borrow(); + if let DownloadStatus::Failed(ref message) = *borrowed_status { + Some(message.clone()) + } else { + None + } + } + + fn report_error(&self, message: String) { + let borrowed_task = self.task.borrow(); + if (*borrowed_task).is_none() { + println!( + "[NAVIGRAPH] Task is none, but an error has been raised: {}", + message + ); + return; + } + let mut borrowed_task = borrowed_task.as_ref().unwrap().borrow_mut(); + borrowed_task.status = TaskStatus::Failure(message.clone()); + } + + fn should_extract_next_batch(&self) -> bool { + let borrowed_zip_handler = self.zip_handler.borrow(); + if let Some(zip_handler) = borrowed_zip_handler.as_ref() { + zip_handler.zip_file_count > zip_handler.current_file_index + } else { + // If there is no zip handler, we are not downloading and we don't need to do anything + false + } + } +} diff --git a/src/wasm/src/download/zip_handler.rs b/src/wasm/src/download/zip_handler.rs index af8a6b9e..7749b3b4 100644 --- a/src/wasm/src/download/zip_handler.rs +++ b/src/wasm/src/download/zip_handler.rs @@ -1,108 +1,154 @@ -use std::{fs, io, path::PathBuf}; - -use crate::util; - -#[derive(PartialEq, Eq)] - -pub enum BatchReturn { - MoreFilesToDelete, - MoreFilesToUnzip, - Finished, -} - -pub struct ZipFileHandler { - // Zip archive to extract - pub zip_archive: Option>, - // Current file index in the zip archive - pub current_file_index: usize, - // Total number of files in the zip archive - pub zip_file_count: usize, - // Number of files deleted so far - pub deleted: usize, - // Path to the directory to extract to - path_buf: PathBuf, - // Whether or not we have cleaned the destination folder yet - cleaned_destination: bool, -} - -impl ZipFileHandler { - pub fn new(zip_archive: zip::ZipArchive, path_buf: PathBuf) -> Self { - // To make accessing zip archive size easier, we just store it to the struct instead of calling it every time - // (avoids ownership issues) - - let zip_file_count = zip_archive.len(); - Self { - zip_archive: Some(zip_archive), - current_file_index: 0, - zip_file_count, - deleted: 0, - path_buf, - cleaned_destination: false, - } - } - - pub fn unzip_batch(&mut self, batch_size: usize) -> Result> { - if self.zip_archive.is_none() { - return Err("No zip archive to extract".to_string().into()); - } - - // If we haven't cleaned the destination folder yet, do so now - if !self.cleaned_destination { - util::delete_folder_recursively(&self.path_buf, Some(batch_size))?; - if !util::path_exists(&self.path_buf) { - fs::create_dir_all(&self.path_buf)?; - self.cleaned_destination = true; - return Ok(BatchReturn::MoreFilesToUnzip); - } - self.deleted += batch_size; - return Ok(BatchReturn::MoreFilesToDelete); - } - - let zip_archive = self - .zip_archive - .as_mut() - .ok_or_else(|| "Failed to access zip archive".to_string())?; - - for _ in 0..batch_size { - if self.current_file_index >= self.zip_file_count { - // Done extracting, drop the zip archive - self.zip_archive = None; - return Ok(BatchReturn::Finished); - } - - let mut file = zip_archive.by_index(self.current_file_index)?; - let outpath = self.path_buf.join( - file.enclosed_name() - .ok_or_else(|| "Failed to get enclosed file name".to_string())?, - ); - - // Check how many times "." appears in the file name - let dot_count = outpath - .to_str() - .ok_or_else(|| "Failed to convert path to string".to_string())? - .matches('.') - .count(); - - // Skip if there are more than 1 "." in the file name (MSFS crashes if we try to extract these files for - // some reason) - if dot_count > 1 { - self.current_file_index += 1; - continue; - } - - if (*file.name()).ends_with('/') { - fs::create_dir_all(outpath).map_err(|_| "Failed to create directory".to_string())?; - } else { - if let Some(p) = outpath.parent() { - if !util::path_exists(p) { - fs::create_dir_all(p).map_err(|_| "Failed to create directory".to_string())?; - } - } - let mut outfile = fs::File::create(outpath).map_err(|_| "Failed to create file".to_string())?; - io::copy(&mut file, &mut outfile).map_err(|_| "Failed to copy file".to_string())?; - } - self.current_file_index += 1; - } - Ok(BatchReturn::MoreFilesToUnzip) - } -} +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +use crate::{ + consts, + util::{self, generate_uuid_from_path, path_exists}, +}; + +#[derive(PartialEq, Eq)] + +pub enum BatchReturn { + MoreFilesToDelete, + MoreFilesToUnzip, + Finished(String), +} + +pub struct ZipFileHandler { + // Zip archive to extract + pub zip_archive: Option>, + // Current file index in the zip archive + pub current_file_index: usize, + // Total number of files in the zip archive + pub zip_file_count: usize, + // Number of files deleted so far + pub deleted: usize, + // Path to the directory to extract to + path_buf: PathBuf, + // Whether or not we have cleaned the destination folder yet + cleaned_destination: bool, +} + +impl ZipFileHandler { + pub fn new(zip_archive: zip::ZipArchive, path_buf: PathBuf) -> Self { + // To make accessing zip archive size easier, we just store it to the struct instead of calling it every time + // (avoids ownership issues) + + let zip_file_count = zip_archive.len(); + Self { + zip_archive: Some(zip_archive), + current_file_index: 0, + zip_file_count, + deleted: 0, + path_buf, + cleaned_destination: false, + } + } + + pub fn unzip_batch( + &mut self, + batch_size: usize, + ) -> Result> { + if self.zip_archive.is_none() { + return Err("No zip archive to extract".to_string().into()); + } + + // If we haven't cleaned the destination folder yet, do so now + if !self.cleaned_destination { + util::delete_folder_recursively(&self.path_buf, Some(batch_size))?; + if !util::path_exists(&self.path_buf) { + fs::create_dir_all(&self.path_buf)?; + self.cleaned_destination = true; + return Ok(BatchReturn::MoreFilesToUnzip); + } + self.deleted += batch_size; + return Ok(BatchReturn::MoreFilesToDelete); + } + + let zip_archive = self + .zip_archive + .as_mut() + .ok_or_else(|| "Failed to access zip archive".to_string())?; + + for _ in 0..batch_size { + if self.current_file_index >= self.zip_file_count { + // Done extracting, drop the zip archive + self.zip_archive = None; + + let work_dir = Path::new(consts::NAVIGATION_DATA_WORK_LOCATION); + + let temp_dir = &work_dir.join("temp"); + + let cycle_path = temp_dir.join("cycle.json"); + + if !util::path_exists(&cycle_path) { + return Err("cycle.json not found".into()); + }; + + let cycle_uuid = generate_uuid_from_path(cycle_path)?; + + let active_cycle = + generate_uuid_from_path(work_dir.join("active").join("cycle.json")).ok(); + + let is_active = active_cycle.is_some_and(|active_uuid| active_uuid == cycle_uuid); + + if path_exists(&work_dir.join(&cycle_uuid)) || is_active { + util::delete_folder_recursively(temp_dir, None)?; + return Err(format!("Package {} already exists", cycle_uuid).into()); + } + + let fix_file = temp_dir.join("filethatfixeseverything"); + + if !util::path_exists(&fix_file) { + fs::File::create(fix_file)?; + } + + fs::rename( + temp_dir, + Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join(&cycle_uuid), + )?; + + return Ok(BatchReturn::Finished(cycle_uuid)); + } + + let mut file = zip_archive.by_index(self.current_file_index)?; + let outpath = self.path_buf.join( + file.enclosed_name() + .ok_or_else(|| "Failed to get enclosed file name".to_string())?, + ); + + // Check how many times "." appears in the file name + let dot_count = outpath + .to_str() + .ok_or_else(|| "Failed to convert path to string".to_string())? + .matches('.') + .count(); + + // Skip if there are more than 1 "." in the file name (MSFS crashes if we try to extract these files for + // some reason) + if dot_count > 1 { + self.current_file_index += 1; + continue; + } + + if (*file.name()).ends_with('/') { + fs::create_dir_all(outpath) + .map_err(|_| "Failed to create directory".to_string())?; + } else { + if let Some(p) = outpath.parent() { + if !util::path_exists(p) { + fs::create_dir_all(p) + .map_err(|_| "Failed to create directory".to_string())?; + } + } + let mut outfile = + fs::File::create(outpath).map_err(|_| "Failed to create file".to_string())?; + io::copy(&mut file, &mut outfile).map_err(|_| "Failed to copy file".to_string())?; + } + self.current_file_index += 1; + } + Ok(BatchReturn::MoreFilesToUnzip) + } +} diff --git a/src/wasm/src/json_structs.rs b/src/wasm/src/json_structs.rs index d77c1907..8f97766e 100644 --- a/src/wasm/src/json_structs.rs +++ b/src/wasm/src/json_structs.rs @@ -8,13 +8,21 @@ pub mod functions { DownloadNavigationData, /// `SetDownloadOptionsParams` SetDownloadOptions, - /// `GetNavigationDataInstallStatus` - GetNavigationDataInstallStatus, /// `ExecuteSQLQueryParams` ExecuteSQLQuery, /// no Params GetDatabaseInfo, + /// `ListAvailablePackages` + ListAvailablePackages, + /// `SetActivePackage` + SetActivePackage, + /// `GetActivePackage` + GetActivePackage, + /// `DeletePackage` + DeletePackage, + /// `CleanPackages` + CleanPackages, /// `GetByIdentParams` GetAirport, @@ -137,9 +145,11 @@ pub mod params { use navigation_database::math::{Coordinates, NauticalMiles}; #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] pub struct DownloadNavigationDataParams { /// URL to download from pub url: String, + pub set_active: Option, } #[derive(serde::Deserialize)] @@ -148,6 +158,28 @@ pub mod params { pub batch_size: usize, } + #[derive(serde::Deserialize)] + pub struct ListAvailablePackages { + pub sort: Option, + pub filter: Option, + } + + #[derive(serde::Deserialize)] + pub struct SetActivePackage { + /// UUID that the package is stored as + pub uuid: String, + } + + #[derive(serde::Deserialize)] + pub struct DeletePackage { + pub uuid: String, + } + + #[derive(serde::Deserialize)] + pub struct CleanPackages { + pub count: Option, + } + #[derive(serde::Deserialize)] pub struct ExecuteSQLQueryParams { /// SQL query to execute diff --git a/src/wasm/src/lib.rs b/src/wasm/src/lib.rs index 3b024184..e135f131 100644 --- a/src/wasm/src/lib.rs +++ b/src/wasm/src/lib.rs @@ -1,25 +1,26 @@ -mod consts; -mod dispatcher; -mod download; -mod json_structs; -mod meta; -mod network_helper; -mod util; - -#[msfs::gauge(name=navigation_data_interface)] -async fn navigation_data_interface(mut gauge: msfs::Gauge) -> Result<(), Box> { - // Log the current version of the module - println!( - "{}", - format!( - "[NAVIGRAPH]: Navigation data interface version {} started", - env!("CARGO_PKG_VERSION") - ) - ); - let mut dispatcher = dispatcher::Dispatcher::new(); - while let Some(event) = gauge.next_event().await { - dispatcher.on_msfs_event(event); - } - - Ok(()) -} +mod consts; +mod dispatcher; +mod download; +mod json_structs; +mod util; + +#[msfs::gauge(name=navigation_data_interface)] +async fn navigation_data_interface( + mut gauge: msfs::Gauge, +) -> Result<(), Box> { + let hash = env!("GIT_HASH").split_at(7).0; + + // Log the current version of the module + println!( + "[NAVIGRAPH]: Navigation data interface version {}-{} started", + env!("CARGO_PKG_VERSION"), + hash + ); + let mut dispatcher: dispatcher::Dispatcher<'_> = + dispatcher::Dispatcher::new(navigation_database::enums::InterfaceFormat::DFDv2); + while let Some(event) = gauge.next_event().await { + dispatcher.on_msfs_event(event); + } + + Ok(()) +} diff --git a/src/wasm/src/meta.rs b/src/wasm/src/meta.rs deleted file mode 100644 index 37e9ba76..00000000 --- a/src/wasm/src/meta.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::{ - cell::RefCell, - error::Error, - path::{Path, PathBuf}, - rc::Rc, -}; - -use msfs::network::NetworkRequestState; - -use crate::{ - consts, - dispatcher::{Task, TaskStatus}, - network_helper::{Method, NetworkHelper}, - util::path_exists, -}; - -#[derive(serde::Serialize, serde::Deserialize, Debug)] -pub struct InternalState { - pub is_bundled: bool, -} - -impl Default for InternalState { - fn default() -> Self { - Self { is_bundled: false } - } -} - -#[derive(serde::Serialize, Clone, Copy, Debug, PartialEq, Eq)] -pub enum InstallStatus { - Bundled, - Manual, - None, -} - -#[derive(serde::Serialize, Debug)] -pub struct NavigationDataStatus { - pub status: InstallStatus, - #[serde(rename = "installedFormat")] - pub installed_format: Option, - #[serde(rename = "installedRevision")] - pub installed_revision: Option, - #[serde(rename = "installedCycle")] - pub installed_cycle: Option, - #[serde(rename = "installedPath")] - pub install_path: Option, - #[serde(rename = "validityPeriod")] - pub validity_period: Option, - #[serde(rename = "latestCycle")] - pub latest_cycle: String, -} - -#[derive(serde::Deserialize)] -pub struct CurrentCycleResponse { - pub name: String, - pub version: String, - pub configuration: String, - pub cycle: String, -} - -#[derive(serde::Deserialize)] -pub struct InstalledNavigationDataCycleInfo { - pub cycle: String, - pub revision: String, - pub name: String, - pub format: String, - #[serde(rename = "validityPeriod")] - pub validity_period: String, -} - -pub fn get_internal_state() -> Result> { - let config_path = Path::new(consts::NAVIGATION_DATA_INTERNAL_CONFIG_LOCATION); - if !path_exists(&config_path) { - return Err("Internal config file does not exist")?; - } - - let config_file = std::fs::File::open(config_path)?; - let internal_state: InternalState = serde_json::from_reader(config_file)?; - - Ok(internal_state) -} - -pub fn set_internal_state(internal_state: InternalState) -> Result<(), Box> { - let config_path = Path::new(consts::NAVIGATION_DATA_INTERNAL_CONFIG_LOCATION); - let config_file = std::fs::File::create(config_path)?; - serde_json::to_writer(config_file, &internal_state)?; - - Ok(()) -} - -pub fn start_network_request(task: Rc>) { - let request = NetworkHelper::make_request("https://navdata.api.navigraph.com/info", Method::Get, None, None); - let request = match request { - Ok(request) => request, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - }; - task.borrow_mut().associated_network_request = Some(request); -} - -pub fn get_installed_cycle_from_json(path: &Path) -> Result> { - let json_file = std::fs::File::open(path)?; - let installed_cycle_info: InstalledNavigationDataCycleInfo = serde_json::from_reader(json_file)?; - - Ok(installed_cycle_info) -} - -pub fn get_navigation_data_install_status(task: Rc>) { - let response_bytes = match task.borrow().associated_network_request.as_ref() { - Some(request) => { - if request.response_state() == NetworkRequestState::DataReady { - let response = request.get_response(); - match response { - Ok(response) => response, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - } - } else { - return; - } - }, - None => { - task.borrow_mut().status = TaskStatus::Failure("No associated network request".to_string()); - return; - }, - }; - - let response_struct: CurrentCycleResponse = match serde_json::from_slice(&response_bytes) { - Ok(response_struct) => response_struct, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - }; - - // figure out install status - let found_downloaded = path_exists(Path::new(consts::NAVIGATION_DATA_WORK_LOCATION)); - - let found_bundled = get_internal_state() - .map(|internal_state| internal_state.is_bundled) - .unwrap_or(false); - - // Check bundled first, as downloaded and bundled are both possible - let status = if found_bundled { - InstallStatus::Bundled - } else if found_downloaded { - InstallStatus::Manual - } else { - InstallStatus::None - }; - - // Open JSON - let json_path = if status != InstallStatus::None { - Some(PathBuf::from(consts::NAVIGATION_DATA_WORK_LOCATION).join("cycle.json")) - } else { - None - }; - - let installed_cycle_info = match json_path { - Some(json_path) => { - let json_file = match std::fs::File::open(json_path) { - Ok(json_file) => json_file, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - }; - - let installed_cycle_info: InstalledNavigationDataCycleInfo = match serde_json::from_reader(json_file) { - Ok(installed_cycle_info) => installed_cycle_info, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - }; - - Some(installed_cycle_info) - }, - None => None, - }; - - let navigation_data_status = NavigationDataStatus { - status, - installed_format: match &installed_cycle_info { - Some(installed_cycle_info) => Some(installed_cycle_info.format.clone()), - None => None, - }, - installed_revision: match &installed_cycle_info { - Some(installed_cycle_info) => Some(installed_cycle_info.revision.clone()), - None => None, - }, - installed_cycle: match &installed_cycle_info { - Some(installed_cycle_info) => Some(installed_cycle_info.cycle.clone()), - None => None, - }, - install_path: if status == InstallStatus::Manual { - Some(consts::NAVIGATION_DATA_WORK_LOCATION.to_string()) - } else { - None - }, - validity_period: match &installed_cycle_info { - Some(installed_cycle_info) => Some(installed_cycle_info.validity_period.clone()), - None => None, - }, - latest_cycle: response_struct.cycle, - }; - - let status_as_value = match serde_json::to_value(&navigation_data_status) { - Ok(status_as_value) => status_as_value, - Err(e) => { - task.borrow_mut().status = TaskStatus::Failure(e.to_string()); - return; - }, - }; - - task.borrow_mut().status = TaskStatus::Success(Some(status_as_value)); -} diff --git a/src/wasm/src/network_helper.rs b/src/wasm/src/network_helper.rs deleted file mode 100644 index a9005491..00000000 --- a/src/wasm/src/network_helper.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::error::Error; - -use msfs::network::{NetworkRequest, NetworkRequestBuilder, NetworkRequestState}; - -pub enum Method { - Get, -} - -pub struct NetworkHelper { - request: NetworkRequest, -} - -impl NetworkHelper { - pub fn make_request( - url: &str, method: Method, headers: Option>, data: Option<&mut [u8]>, - ) -> Result> { - let mut builder = NetworkRequestBuilder::new(url).ok_or("Failed to create NetworkRequestBuilder")?; - - // Add headers - if let Some(headers) = headers { - for header in headers { - let new_builder = builder.with_header(header).ok_or("Failed to add header")?; - builder = new_builder; - } - } - - // Add data - if let Some(data) = data { - let new_builder = builder.with_data(data); - builder = new_builder; - } - - // Send request - let request = match method { - Method::Get => builder.get().ok_or("Failed to send GET request")?, - }; - - Ok(Self { request }) - } - - pub fn response_state(&self) -> NetworkRequestState { - self.request.state() - } - - pub fn get_response(&self) -> Result, Box> { - if self.request.state() != NetworkRequestState::DataReady { - return Err("Request not finished yet".into()); - } - - let data = self.request.data().ok_or("Failed to get data")?; - - Ok(data) - } -} diff --git a/src/wasm/src/util.rs b/src/wasm/src/util.rs index 5bcca068..6b8403b9 100644 --- a/src/wasm/src/util.rs +++ b/src/wasm/src/util.rs @@ -1,82 +1,132 @@ -use std::{fs, io, path::Path}; - -use navigation_database::util::{get_path_type, PathType}; - -pub fn path_exists(path: &Path) -> bool { - get_path_type(path) != PathType::DoesNotExist -} - -pub fn delete_folder_recursively(path: &Path, batch_size: Option) -> io::Result<()> { - // Make sure we are deleting a directory (and in turn that it exists) - if get_path_type(path) != PathType::Directory { - return Ok(()); - } - // Collect the entries that we will delete (taking into account the batch size) - let mut entries = Vec::new(); - for entry in fs::read_dir(path)? { - entries.push(entry?); - if let Some(batch_size) = batch_size { - if entries.len() >= batch_size { - break; - } - } - } - // After we have collected the entries, delete them - for entry in entries { - let path = entry.path(); - let path_type = get_path_type(&path); - - if path_type == PathType::Directory { - delete_folder_recursively(&path, batch_size)?; - } else if path_type == PathType::File { - fs::remove_file(&path)?; - } else if let None = path.extension() { - // There are edge cases where completely empty directories are created and can't be deleted. They get registered as "unknown" path type so we need to check if the path has an extension (which would tell us if it's a file or a directory), and if it doesn't, we delete it as a directory - let _ = fs::remove_dir(&path); // this can fail silently, but we don't care since there also might be cases where a file literally doesn't exist - } - } - // Check if the directory is empty. If it is, delete it - let mut dir_res = fs::read_dir(path)?; - let next = dir_res.next(); - if let Some(result) = next { - if result.is_ok() { - return Ok(()); - } - } else { - // Directory is empty, delete it - fs::remove_dir(path)?; - } - Ok(()) -} - -pub fn copy_files_to_folder(from: &Path, to: &Path) -> io::Result<()> { - // Make sure we are copying a directory (and in turn that it exists) - if get_path_type(from) != PathType::Directory { - return Ok(()); - } - // Let's clear the directory we are copying to - delete_folder_recursively(to, None)?; - // Create the directory we are copying to - fs::create_dir(to)?; - // Collect the entries that we will copy - let entries = fs::read_dir(from)?.collect::, _>>()?; - // Copy the entries - for entry in entries { - let path = entry.path(); - let path_type = get_path_type(&path); - - if path_type == PathType::Directory { - let new_dir = to.join(path.file_name().unwrap()); - fs::create_dir(&new_dir)?; - copy_files_to_folder(&path, &new_dir)?; - } else if path_type == PathType::File { - fs::copy(&path, to.join(path.file_name().unwrap()))?; - } - } - - Ok(()) -} - -pub fn trim_null_terminator(s: &str) -> &str { - s.trim_end_matches(char::from(0)) -} +use std::{ + error::Error, + fs, + io::{self}, + path::Path, +}; + +use navigation_database::{ + traits::InstalledNavigationDataCycleInfo, + util::{get_path_type, PathType}, +}; +use uuid::Uuid; + +pub fn path_exists(path: &Path) -> bool { + get_path_type(path) != PathType::DoesNotExist +} + +pub fn delete_folder_recursively(path: &Path, batch_size: Option) -> io::Result<()> { + // Make sure we are deleting a directory (and in turn that it exists) + if get_path_type(path) != PathType::Directory { + return Ok(()); + } + // Collect the entries that we will delete (taking into account the batch size) + let mut entries = Vec::new(); + for entry in fs::read_dir(path)? { + entries.push(entry?); + if let Some(batch_size) = batch_size { + if entries.len() >= batch_size { + break; + } + } + } + // After we have collected the entries, delete them + for entry in entries { + let path = entry.path(); + let path_type = get_path_type(&path); + + if path.file_name().unwrap() == "" { + eprintln!("[NAVIGRAPH]: Bugged entry"); + continue; + } + + if path_type == PathType::Directory { + delete_folder_recursively(&path, batch_size)?; + } else if path_type == PathType::File { + fs::remove_file(&path)?; + } else if path.extension().is_none() { + // There are edge cases where completely empty directories are created and can't be deleted. They get + // registered as "unknown" path type so we need to check if the path has an extension (which would tell us + // if it's a file or a directory), and if it doesn't, we delete it as a directory + let _ = fs::remove_dir(&path); // this can fail silently, but we don't care since there also might be cases + // where a file literally doesn't exist + } + } + // Check if the directory is empty. If it is, delete it + let mut dir_res = fs::read_dir(path)?; + let next = dir_res.next(); + if let Some(result) = next { + if result.is_ok() { + return Ok(()); + } + } else { + // Directory is empty, delete it + fs::remove_dir(path)?; + } + Ok(()) +} + +pub fn copy_files_to_folder(from: &Path, to: &Path) -> io::Result<()> { + // Make sure we are copying a directory (and in turn that it exists) + if get_path_type(from) != PathType::Directory { + return Ok(()); + } + // Let's clear the directory we are copying to + delete_folder_recursively(to, None)?; + // Create the directory we are copying to + fs::create_dir(to)?; + // Collect the entries that we will copy + let entries = fs::read_dir(from)?; + + // Copy the entries + for entry in entries { + let Ok(entry) = entry else { + eprintln!("[NAVIGRAPH]: Bugged entry"); + continue; + }; + + let path = entry.path(); + let path_type = get_path_type(&path); + + if path.file_name().unwrap() == "" { + eprintln!("[NAVIGRAPH]: Bugged entry"); + continue; + } + + if path_type == PathType::Directory { + let new_dir = to.join(path.file_name().unwrap()); + fs::create_dir(&new_dir)?; + copy_files_to_folder(&path, &new_dir)?; + } else if path_type == PathType::File { + fs::copy(&path, to.join(path.file_name().unwrap()))?; + } + } + + Ok(()) +} + +pub fn trim_null_terminator(s: &str) -> &str { + s.trim_end_matches(char::from(0)) +} + +pub fn generate_uuid_from_path

(cycle_path: P) -> Result> +where + P: AsRef, +{ + let cycle = &serde_json::from_reader(fs::File::open(cycle_path)?)?; + + Ok(generate_uuid_from_cycle(cycle)) +} + +pub fn generate_uuid_from_cycle(cycle: &InstalledNavigationDataCycleInfo) -> String { + let uuid_hash = format!( + "{}{}{}{}", + cycle.cycle, cycle.revision, cycle.format, cycle.name + ); + + let uuid_uuid = Uuid::new_v3(&Uuid::NAMESPACE_URL, uuid_hash.as_bytes()); + + let uuid_hypenated = uuid_uuid.as_hyphenated(); + + uuid_hypenated.to_string() +}