diff --git a/.gitignore b/.gitignore index a10a0bf..8b89fb9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /.vscode *.code-workspace /tmp +.DS_Store +@eaDir/ diff --git a/LICENSE.txt b/LICENSE.txt index 3e5e7da..176d123 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,9 +1,9 @@ The MIT License (MIT) -Copyright © 2022 Frank Boës +Copyright © 2022-2024 Frank Boës Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 53b4e17..ca1547d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # ![](docs/favicon-64x64.png) Aerofly Missionsgerät -The Aerofly Missionsgerät converts [Aerofly FS 4's](https://www.aerofly.com/) `main.mcf` and other flight plan file formats into a shareable mission file, using your current flight plan and other settings. +The Aerofly Missionsgerät converts simulator flight plan files for [Aerofly FS 4](https://www.aerofly.com/) (`main.mcf` and `tmc`), Microsoft Flight Simulator (`pln`) and X-Plane (`fms`). It also imports Garmin flight plan files (`fpl`). -There are two versions of this tool: +Head over to [https://fboes.github.io/aerofly-missions/dist/](https://fboes.github.io/aerofly-missions/dist/) to get started. -1. **Web version**: Use the Missionsgerät right out of your browser by visiting [https://fboes.github.io/aerofly-missions/dist/](https://fboes.github.io/aerofly-missions/dist/). -2. **Local Node.js CLI tool**: If you want some automation for your workflow. +There is also a local Node.js CLI tool, if you want some automation for your workflow. ![Flight plan as text briefing](docs/flightplan.png) diff --git a/dist/Aerofly/Mission.js b/dist/Aerofly/Mission.js index e7bf8cb..d48600c 100644 --- a/dist/Aerofly/Mission.js +++ b/dist/Aerofly/Mission.js @@ -361,6 +361,8 @@ export class Mission { if (gpl.cruisingAlt) { this.cruise_altitude_ft = gpl.cruisingAlt; } + // Assuming non AFS4 flight plans to start on the ground ;) + this.flight_setting = Mission.FLIGHT_SETTING_TAXI; this.checkpoints = gpl.waypoints.map((w, i) => { var _a; let cp = new MissionCheckpoint(); diff --git a/dist/Import/MsfsPln.js b/dist/Import/MsfsPln.js index 15e2fc1..7062a6f 100644 --- a/dist/Import/MsfsPln.js +++ b/dist/Import/MsfsPln.js @@ -17,7 +17,7 @@ export class MsfsPln extends GarminFpl { const coords = this.convertCoordinate(worldPosition); if (index === 0 || index === waypointsXml.length - 1) { const runwayNumberFP = this.getXmlNode(xml, "RunwayNumberFP"); - let runwayDesignatorFP = this.getXmlNode(xml, "RunwayNumberFP"); + let runwayDesignatorFP = this.getXmlNode(xml, "RunwayDesignatorFP"); const rw = runwayNumberFP + (runwayDesignatorFP === "NONE" ? "" : runwayDesignatorFP.substring(0, 1)); if (runwayNumberFP) { if (index === 0) { diff --git a/dist/Tests/MsfsPlnTest.js b/dist/Tests/MsfsPlnTest.js index 1480421..8661a97 100644 --- a/dist/Tests/MsfsPlnTest.js +++ b/dist/Tests/MsfsPlnTest.js @@ -11,6 +11,7 @@ export class MsfsPlnTest extends Test { this.testEGOV(); this.testLittleNavMap(); this.testGarminParse(); + this.testRunway(); } testEGOV() { // Parse PLN @@ -33,6 +34,7 @@ export class MsfsPlnTest extends Test { this.group(MsfsPln.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 16); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); } // Export Mission to XML const exportPln = new MsfsPlnExport(mission); @@ -82,10 +84,21 @@ export class MsfsPlnTest extends Test { this.group(MsfsPln.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 11); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); this.assertEquals(mission.checkpoints[0].type, MissionCheckpoint.TYPE_ORIGIN); this.assertEquals(mission.checkpoints[1].type, MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); this.assertEquals(mission.checkpoints[9].type, MissionCheckpoint.TYPE_DESTINATION_RUNWAY); this.assertEquals(mission.checkpoints[10].type, MissionCheckpoint.TYPE_DESTINATION); } } + testRunway() { + const pln = new MsfsPln(fs.readFileSync("./src/Tests/cases/ENHD_local_flight.pln", "utf8")); + this.group(MsfsPln.name + ": Runway check"); + { + this.assertEquals(pln.departureRunway, "13"); + this.assertEquals(pln.destinationRunway, "31"); + const mission = new Mission("", "").fromGarminFpl(pln); + this.assertEquals(mission.checkpoints.length, 9); + } + } } diff --git a/dist/Tests/XplaneFmsTest.js b/dist/Tests/XplaneFmsTest.js index 1979abb..7694a47 100644 --- a/dist/Tests/XplaneFmsTest.js +++ b/dist/Tests/XplaneFmsTest.js @@ -11,6 +11,7 @@ export class XplaneFmsTest extends Test { this.testEGOV(); this.testLittleNavMap(); this.testGarminParse(); + this.testRunway(); } testEGOV() { const fms = new XplaneFms(fs.readFileSync("./src/Tests/cases/EGCC-EDDF.fms", "utf8")); @@ -33,6 +34,7 @@ export class XplaneFmsTest extends Test { this.group(XplaneFms.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 17); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); } // Export Mission to XML const exportFms = new XplaneFmsExport(mission); @@ -85,10 +87,21 @@ export class XplaneFmsTest extends Test { this.group(XplaneFms.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 11); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); this.assertEquals(mission.checkpoints[0].type, MissionCheckpoint.TYPE_ORIGIN); this.assertEquals(mission.checkpoints[1].type, MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); this.assertEquals(mission.checkpoints[9].type, MissionCheckpoint.TYPE_DESTINATION_RUNWAY); this.assertEquals(mission.checkpoints[10].type, MissionCheckpoint.TYPE_DESTINATION); } } + testRunway() { + const pln = new XplaneFms(fs.readFileSync("./src/Tests/cases/ENHD_local_flight.fms", "utf8")); + this.group(XplaneFms.name + ": Runway check"); + { + this.assertEquals(pln.departureRunway, "13"); + this.assertEquals(pln.destinationRunway, "31"); + const mission = new Mission("", "").fromGarminFpl(pln); + this.assertEquals(mission.checkpoints.length, 9); + } + } } diff --git a/dist/index.html b/dist/index.html index d7b3155..1fde633 100644 --- a/dist/index.html +++ b/dist/index.html @@ -8,8 +8,8 @@ - - + + @@ -34,7 +34,7 @@

-

The Aerofly Missionsgerät converts Aerofly FS 4's main.mcf (and other flight plan file formats) into a custom_missions_user.tmc file (and other flight plan file formats), using your current flight plan and other settings to generate a shareable mission.

+

The Aerofly Missionsgerät converts simulator flight plan files for Aerofly FS 4 (main.mcf and tmc), Microsoft Flight Simulator (pln) and X-Plane (fms). It also imports Garmin flight plan files (fpl).
For more information see the Missionsgerät's instructions.

Step 1: Load flight plan file

@@ -305,6 +305,7 @@

Step 3: Check flight plan

By activating the Mapbox map you consent to the integration probably sending personal data across the internet. Please reload the page after activating Mapbox.

+

Use Shift + Click on the map to move waypoints.

@@ -312,6 +313,7 @@

Step 3: Check flight plan

Step 4: Save converted files

The Missionsgerät exports Aerofly FS tmc, Microsoft FS pln or X-Plane fms flight plan files.

+

See the Missionsgerät's instructions on how to install the mission files into Aerofly FS4.

It also generates a Markdown file containing a textual description of the flight including weather data, as well as a GeoJSON file.

diff --git a/dist/manifest.json b/dist/manifest.json index d656310..04cad55 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -1,7 +1,7 @@ { "name": "Aerofly Missionsgerät", "short_name": "Missionsgerät", - "description": "The Aerofly Missionsgerät converts Aerofly FS 4's `main.mcf` and other flight plan file formats into a shareable mission file, using your current flight plan and other settings.", + "description": "The Aerofly Missionsgerät converts simulator flight plan files for Aerofly FS 4, Microsoft Flight Simulator and X-Plane. It also imports Garmin flight plan files.", "icons": [ { "src": "./favicon-180x180.png", diff --git a/docs/README.md b/docs/README.md index 82647e1..f89db65 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,15 @@ -# ![](favicon-64x64.png) Additional documents +# ![](favicon-64x64.png) Documentation + +The basic steps to work with the Missionsgerät are straightforward: + +1. The Aerofly Missionsgerät converts simulator flight plan files for [Aerofly FS 4](https://www.aerofly.com/) (`main.mcf` and `tmc`), Microsoft Flight Simulator (`pln`) and X-Plane (`fms`). It also imports Garmin flight plan files (`fpl`). +2. Upload the flight plan file to the Missionsgerät. +3. You may want to add settings like the aircraft used, time and weather. Only Aerofly FS 4 flight plan files contain these information, so other flight plan files need to add these manually. You may also want to modify your starting position and heading by using the Missionsgerät's map. +4. Download the converted flight plan files file from the Missionsgerät. + +If you want to use the flight plan file in Aerofly FS 4, check the [generic `custom_missions_user.tmc` installation instructions](./generic-installation.md). + +## Additional documents 1. [Building a custom missions file](./custom-missions.md) 1. [Flight plan output](./flightplan.md) @@ -9,7 +20,9 @@ On a more theoretical level: 1. [How to build a flight plan](./build-flightplan.md) -These mission files are included: +## Included mission files + +These missions are ready to download: 1. [Carl's Homecoming](./carls_homecoming/README.md) Take a seat in Carl's Pitts S-2, and starting in Hamburg travel up to Denmark, Sweden and finally Finnland. diff --git a/docs/generic-installation.md b/docs/generic-installation.md index 1021f8d..db0d113 100644 --- a/docs/generic-installation.md +++ b/docs/generic-installation.md @@ -2,12 +2,15 @@ To install a custom missions file in Aerofly FS 4: -1. Open a file explorer in your Aerofly FS 4 user folder. Usually this is located at `C:\Users\%USERNAME%\Documents\Aerofly FS 4`. -2. Open up the `missions` folder. -3. Put the new `.tmc` file into the `missions` folder, +1. Open a file explorer in your Aerofly FS 4 user folder. Usually this is located at + `C:\Users\%USERNAME%\Documents\Aerofly FS 4`. +2. Open up the `missions` folder. If this folder does not exists, create it. +3. Put the new `.tmc` file into the `missions` folder, and rename it to `custom_missions_user.tmc`. -4. On starting Aerofly FS 4, your custom missions will be loaded from the - new `custom_missions_user.tmc`. +4. On starting Aerofly FS 4, your custom missions will be loaded from the new + `custom_missions_user.tmc`. + The custom missions can be found in Aerofly FS 4 in + ["Main Menu" > "Missions" > "Challenges"](https://www.aerofly.com/tutorials/missions/). Be aware that there is only one `custom_missions_user.tmc` at any given time. If you have multiple mission files, rename them accordingly and keep backups of the original files. diff --git a/docs/reno_air_races/README.md b/docs/reno_air_races/README.md index d8a6e52..90c0d16 100644 --- a/docs/reno_air_races/README.md +++ b/docs/reno_air_races/README.md @@ -6,7 +6,7 @@ Welcome to the [Reno Air Races](https://airrace.org/). Make yourself familiar wi Please note: You will have the exact positions of the poles marked in your map for the jet course (with a MB-339) and bi-plane course (with a Pitts S-2). -The flight plan itself will not show up properly in Aerofly FS4, as the waypoints are way too close - and sadly the poles are not modelled. But glancing on the moving map gives you an idea where the poles are supposed to be, and you may have a glimpse as some funny spots on the aerial images, which are the poles. +The flight plan itself will not show up properly in Aerofly FS 4, as the waypoints are way too close - and sadly the poles are not modeled. But glancing on the moving map gives you an idea where the poles are supposed to be, and you may have a glimpse as some funny spots on the aerial images, which are the poles. You may also want to take a look at [Krzysk's Reno Course](https://github.com/krzysk1/reno_course) and [ApfelFliegers's installation instructions](https://www.aerofly.com/community/forum/index.php?thread/19105-user-created-custom-missions/&postID=121141#post121141). diff --git a/package.json b/package.json index d6f14a7..5e9807f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "name": "aerofly-missions", "author": "Frank Boës (https://3960.org/)", "license": "MIT", - "description": "The Aerofly Missionsgerät converts Aerofly FS 4's `main.mcf` and other flight plan file formats into a shareable mission file, using your current flight plan and other settings.", + "description": "The Aerofly Missionsgerät converts simulator flight plan files for Aerofly FS 4, Microsoft Flight Simulator and X-Plane. It also imports Garmin flight plan files.", "version": "2.19.0", "exports": "./dist/aerofly-missions.js", "bin": { diff --git a/src/Aerofly/Mission.ts b/src/Aerofly/Mission.ts index 5fd9274..ede4ca6 100644 --- a/src/Aerofly/Mission.ts +++ b/src/Aerofly/Mission.ts @@ -424,6 +424,9 @@ export class Mission { this.cruise_altitude_ft = gpl.cruisingAlt; } + // Assuming non AFS4 flight plans to start on the ground ;) + this.flight_setting = Mission.FLIGHT_SETTING_TAXI; + this.checkpoints = gpl.waypoints.map((w, i) => { let cp = new MissionCheckpoint(); cp.lon_lat.lat = w.lat; @@ -439,8 +442,18 @@ export class Mission { ) { cp.type = i === 1 ? MissionCheckpoint.TYPE_DEPARTURE_RUNWAY : MissionCheckpoint.TYPE_DESTINATION_RUNWAY; cp.name = cp.name.replace(/^(RW)/, ""); + + /*if (cp.type === MissionCheckpoint.TYPE_DEPARTURE_RUNWAY) { + gpl.departureRunway = undefined; + } else { + gpl.destinationRunway = undefined; + }*/ } + //this.addCheckpointAfter(0, 0.5); + // if (gpl.departureRunway) { + // if (gpl.destinationRunway) { + return cp; }); diff --git a/src/Import/MsfsPln.ts b/src/Import/MsfsPln.ts index 97760e6..70048a9 100644 --- a/src/Import/MsfsPln.ts +++ b/src/Import/MsfsPln.ts @@ -24,7 +24,7 @@ export class MsfsPln extends GarminFpl { if (index === 0 || index === waypointsXml.length - 1) { const runwayNumberFP = this.getXmlNode(xml, "RunwayNumberFP"); - let runwayDesignatorFP = this.getXmlNode(xml, "RunwayNumberFP") as MsfsPlnRunwayDesignator | ""; + let runwayDesignatorFP = this.getXmlNode(xml, "RunwayDesignatorFP") as MsfsPlnRunwayDesignator | ""; const rw = runwayNumberFP + (runwayDesignatorFP === "NONE" ? "" : runwayDesignatorFP.substring(0, 1)); if (runwayNumberFP) { if (index === 0) { diff --git a/src/Tests/MsfsPlnTest.ts b/src/Tests/MsfsPlnTest.ts index 006fe8b..6ecd739 100644 --- a/src/Tests/MsfsPlnTest.ts +++ b/src/Tests/MsfsPlnTest.ts @@ -10,6 +10,7 @@ export class MsfsPlnTest extends Test { this.testEGOV(); this.testLittleNavMap(); this.testGarminParse(); + this.testRunway(); } testEGOV() { @@ -34,6 +35,7 @@ export class MsfsPlnTest extends Test { this.group(MsfsPln.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 16); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); } // Export Mission to XML @@ -88,10 +90,23 @@ export class MsfsPlnTest extends Test { this.group(MsfsPln.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 11); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); this.assertEquals(mission.checkpoints[0].type, MissionCheckpoint.TYPE_ORIGIN); this.assertEquals(mission.checkpoints[1].type, MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); this.assertEquals(mission.checkpoints[9].type, MissionCheckpoint.TYPE_DESTINATION_RUNWAY); this.assertEquals(mission.checkpoints[10].type, MissionCheckpoint.TYPE_DESTINATION); } } + + testRunway() { + const pln = new MsfsPln(fs.readFileSync("./src/Tests/cases/ENHD_local_flight.pln", "utf8")); + this.group(MsfsPln.name + ": Runway check"); + { + this.assertEquals(pln.departureRunway, "13"); + this.assertEquals(pln.destinationRunway, "31"); + + const mission = new Mission("", "").fromGarminFpl(pln); + this.assertEquals(mission.checkpoints.length, 9); + } + } } diff --git a/src/Tests/XplaneFmsTest.ts b/src/Tests/XplaneFmsTest.ts index c1d3406..bf7b991 100644 --- a/src/Tests/XplaneFmsTest.ts +++ b/src/Tests/XplaneFmsTest.ts @@ -10,6 +10,7 @@ export class XplaneFmsTest extends Test { this.testEGOV(); this.testLittleNavMap(); this.testGarminParse(); + this.testRunway(); } testEGOV() { @@ -34,6 +35,7 @@ export class XplaneFmsTest extends Test { this.group(XplaneFms.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 17); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); } // Export Mission to XML @@ -92,10 +94,23 @@ export class XplaneFmsTest extends Test { this.group(XplaneFms.name + ": Mission conversion"); { this.assertEquals(mission.checkpoints.length, 11); + this.assertEquals(mission.flight_setting, Mission.FLIGHT_SETTING_TAXI); this.assertEquals(mission.checkpoints[0].type, MissionCheckpoint.TYPE_ORIGIN); this.assertEquals(mission.checkpoints[1].type, MissionCheckpoint.TYPE_DEPARTURE_RUNWAY); this.assertEquals(mission.checkpoints[9].type, MissionCheckpoint.TYPE_DESTINATION_RUNWAY); this.assertEquals(mission.checkpoints[10].type, MissionCheckpoint.TYPE_DESTINATION); } } + + testRunway() { + const pln = new XplaneFms(fs.readFileSync("./src/Tests/cases/ENHD_local_flight.fms", "utf8")); + this.group(XplaneFms.name + ": Runway check"); + { + this.assertEquals(pln.departureRunway, "13"); + this.assertEquals(pln.destinationRunway, "31"); + + const mission = new Mission("", "").fromGarminFpl(pln); + this.assertEquals(mission.checkpoints.length, 9); + } + } }