├── .envrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── build.zig ├── doc └── libflightplan.3.scd ├── examples └── basic.c ├── flake.lock ├── flake.nix ├── include └── flightplan.h ├── nix ├── devshell.nix ├── overlay.nix └── package.nix ├── shell.nix ├── src-build └── ScdocStep.zig ├── src ├── Departure.zig ├── Destination.zig ├── Error.zig ├── FlightPlan.zig ├── Route.zig ├── Runway.zig ├── Waypoint.zig ├── binding.zig ├── format.zig ├── format │ ├── garmin.zig │ └── xplane_fms_11.zig ├── include │ └── bridge.h ├── main.zig ├── test.zig ├── time.zig └── xml.zig └── test ├── basic.fpl ├── error_no_flightplan.fpl ├── error_syntax.fpl └── xplane11.fms /.envrc: -------------------------------------------------------------------------------- 1 | # If we are a computer with nix-shell available, then use that to setup 2 | # the build environment with exactly what we need. 3 | if has nix-shell; then 4 | use nix 5 | fi 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest] 8 | 9 | target: [ 10 | aarch64-linux-gnu, 11 | aarch64-linux-musl, 12 | aarch64-macos, 13 | i386-linux-gnu, 14 | i386-linux-musl, 15 | i386-windows, 16 | x86_64-linux-gnu, 17 | x86_64-linux-musl, 18 | x86_64-macos, 19 | x86_64-windows-gnu, 20 | ] 21 | runs-on: ${{ matrix.os }} 22 | needs: test 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v2 26 | with: 27 | submodules: recursive 28 | fetch-depth: 0 29 | 30 | # Install Nix and use that to run our tests so our environment matches exactly. 31 | - uses: cachix/install-nix-action@v16 32 | with: 33 | nix_path: nixpkgs=channel:nixos-unstable 34 | 35 | # Run our checks to catch quick issues 36 | - run: nix flake check 37 | 38 | # Run our go tests within the context of the dev shell from the flake. This 39 | # will ensure we have all our dependencies. 40 | - name: test 41 | run: nix develop -c zig build -Dtarget=${{ matrix.target }} 42 | 43 | test: 44 | strategy: 45 | matrix: 46 | os: [ubuntu-latest] 47 | runs-on: ${{ matrix.os }} 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v2 51 | with: 52 | submodules: recursive 53 | fetch-depth: 0 54 | 55 | # Install Nix and use that to run our tests so our environment matches exactly. 56 | - uses: cachix/install-nix-action@v16 57 | with: 58 | nix_path: nixpkgs=channel:nixos-unstable 59 | 60 | # Run our checks to catch quick issues 61 | - run: nix flake check 62 | 63 | # Run our go tests within the context of the dev shell from the flake. This 64 | # will ensure we have all our dependencies. 65 | - name: test 66 | run: nix develop -c zig build test-unit 67 | 68 | # Run a full build to ensure that works 69 | - run: nix build 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | /result* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/zig-libxml2"] 2 | path = vendor/zig-libxml2 3 | url = https://github.com/mitchellh/zig-libxml2.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libflightplan (Zig and C) 2 | 3 | libflightplan is a library for reading and writing flight plans in 4 | various formats. Flight plans are used in aviation to save properties of 5 | one or more flights such as route (waypoints), altitude, source and departure 6 | airport, etc. This library is written primarily in Zig but exports a C ABI 7 | compatible shared and static library so that any programming language that 8 | can interface with C can interface with this library. 9 | 10 | **Warning!** If you use this library with the intention of using the 11 | flight plan for actual flight, be very careful to verify the plan in 12 | your avionics or EFB. Never trust the output of this library for actual 13 | flight. 14 | 15 | **Library status: Unstable.** This library is _brand new_ and was built for 16 | hobby purposes. It only supports a handful of formats, with limitations. 17 | My primary interest at the time of writing this is ForeFlight flight plans 18 | and being able to use them to build supporting tools, but I'm interested 19 | in supporting more formats over time. 20 | 21 | ## Formats 22 | 23 | | Name | Ext | Read | Write | 24 | | :--- | :---: | :---: | :---: | 25 | | ForeFlight | FPL | ✅ | ✅* | 26 | | Garmin | FPL | ✅ | ✅* | 27 | | X-Plane FMS 11 | FMS | ❌ | ✅* | 28 | 29 | \*: The C API doesn't support creating flight plans from scratch or 30 | modifying existing flight plans. But you can read in one format and 31 | encode in another. The Zig API supports full creation and modification. 32 | 33 | ## Usage 34 | 35 | libflightplan can be used from C and [Zig](https://ziglang.org/). Examples 36 | for each are shown below. 37 | 38 | ### C 39 | 40 | The C API is documented as 41 | [man pages](https://github.com/mitchellh/libflightplan/tree/main/doc) as well as the 42 | [flightplan.h header file](https://github.com/mitchellh/libflightplan/blob/main/include/flightplan.h). 43 | An example program is available in [`examples/basic.c`](https://github.com/mitchellh/libflightplan/blob/main/examples/basic.c), 44 | and a simplified version is reproduced below. This example shows how to 45 | read and extract information from a ForeFlight flight plan. 46 | 47 | The C API is available as both a static and shared library. To build them, 48 | install [Zig](https://ziglang.org/) and run `zig build install`. This also 49 | installs `pkg-config` files so the header and libraries can be easily found 50 | and integrated with other build systems. 51 | 52 | ```c 53 | #include 54 | #include 55 | #include 56 | 57 | int main() { 58 | // Parse our flight plan from an FPL file out of ForeFlight. 59 | flightplan *fpl = fpl_garmin_parse_file("./test/basic.fpl"); 60 | if (fpl == NULL) { 61 | // We can get a more detailed error. 62 | flightplan_error *err = fpl_last_error(); 63 | printf("error: %s\n", fpl_error_message(err)); 64 | fpl_cleanup(); 65 | return 1; 66 | } 67 | 68 | // Iterate and output the full ordered route. 69 | int max = fpl_route_points_count(fpl); 70 | printf("\nroute: \"%s\" (points: %d)\n", fpl_route_name(fpl), max); 71 | for (int i = 0; i < max; i++) { 72 | flightplan_route_point *point = fpl_route_points_get(fpl, i); 73 | printf(" %s\n", fpl_route_point_identifier(point)); 74 | } 75 | 76 | // Convert this to an X-Plane 11 flight plan. 77 | fpl_xplane11_write_to_file(fpl, "./copy.fms"); 78 | 79 | fpl_free(fpl); 80 | fpl_cleanup(); 81 | return 0; 82 | } 83 | ``` 84 | 85 | ### Zig 86 | 87 | ```zig 88 | const std = @import("std"); 89 | const flightplan = @import("flightplan"); 90 | 91 | fn main() !void { 92 | defer flightplan.deinit(); 93 | 94 | var alloc = std.heap.ArenaAllocator.init(std.heap.page_allocator); 95 | defer alloc.deinit(); 96 | 97 | var fpl = try flightplan.Format.Garmin.initFromFile(alloc, "./test/basic.fpl"); 98 | defer fpl.deinit(); 99 | 100 | std.debug.print("route: \"{s}\" (points: {d})\n", .{ 101 | fpl.route.name.?, 102 | fpl.route.points.items.len, 103 | }); 104 | for (fpl.route.points.items) |point| { 105 | std.debug.print(" {s}\n", .{point}); 106 | } 107 | 108 | // Convert to an X-Plane 11 flight plan format 109 | flightplan.Format.XPlaneFMS11.Format.writeToFile("./copy.fms", fpl); 110 | } 111 | ``` 112 | 113 | ## Build 114 | 115 | To build libflightplan, you need to have the following installed: 116 | 117 | * [Zig](https://ziglang.org/) 118 | * [Libxml2](http://www.xmlsoft.org/) 119 | 120 | With the dependencies installed, you can run `zig build` to make a local 121 | build of the libraries. You can run `zig build install` to build and install 122 | the libraries and headers to your standard prefix. And you can run `zig build test` 123 | to run all the tests. 124 | 125 | A [Nix](https://nixos.org/) flake is also provided. If you are a Nix user, you 126 | can easily build this library, depend on it, etc. You know who you are and you 127 | know what to do. 128 | 129 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Builder = std.build.Builder; 3 | const libxml2 = @import("vendor/zig-libxml2/libxml2.zig"); 4 | 5 | const ScdocStep = @import("src-build/ScdocStep.zig"); 6 | 7 | // Zig packages in use 8 | const pkgs = struct { 9 | const flightplan = pkg("src/main.zig"); 10 | }; 11 | 12 | /// pkg can be called to get the Pkg for this library. Downstream users 13 | /// can use this to add the package to the import paths. 14 | pub fn pkg(path: []const u8) std.build.Pkg { 15 | return std.build.Pkg{ 16 | .name = "flightplan", 17 | .path = .{ .path = path }, 18 | }; 19 | } 20 | 21 | pub fn build(b: *Builder) !void { 22 | const mode = b.standardReleaseOptions(); 23 | const target = b.standardTargetOptions(.{}); 24 | 25 | // Options 26 | const man_pages = b.option( 27 | bool, 28 | "man-pages", 29 | "Set to true to build man pages. Requires scdoc. Defaults to true if scdoc is found.", 30 | ) orelse scdoc_found: { 31 | _ = b.findProgram(&[_][]const u8{"scdoc"}, &[_][]const u8{}) catch |err| switch (err) { 32 | error.FileNotFound => break :scdoc_found false, 33 | else => return err, 34 | }; 35 | break :scdoc_found true; 36 | }; 37 | 38 | // Steps 39 | const test_step = b.step("test", "Run all tests"); 40 | const test_unit_step = b.step("test-unit", "Run unit tests only"); 41 | 42 | // Build libxml2 for static builds only 43 | const xml2 = try libxml2.create(b, target, mode, .{ 44 | .iconv = false, 45 | .lzma = false, 46 | .zlib = false, 47 | }); 48 | 49 | // Native Zig tests 50 | const lib_tests = b.addTestSource(pkgs.flightplan.path); 51 | addSharedSettings(lib_tests, mode, target); 52 | xml2.link(lib_tests); 53 | test_unit_step.dependOn(&lib_tests.step); 54 | test_step.dependOn(&lib_tests.step); 55 | 56 | // Static C lib 57 | { 58 | const static_lib = b.addStaticLibrary("flightplan", "src/binding.zig"); 59 | addSharedSettings(static_lib, mode, target); 60 | xml2.addIncludeDirs(static_lib); 61 | static_lib.install(); 62 | b.default_step.dependOn(&static_lib.step); 63 | 64 | const static_binding_test = b.addExecutable("static-binding", null); 65 | static_binding_test.setBuildMode(mode); 66 | static_binding_test.setTarget(target); 67 | static_binding_test.linkLibC(); 68 | static_binding_test.addIncludeDir("include"); 69 | static_binding_test.addCSourceFile("examples/basic.c", &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99" }); 70 | static_binding_test.linkLibrary(static_lib); 71 | xml2.link(static_binding_test); 72 | 73 | const static_binding_test_run = static_binding_test.run(); 74 | 75 | test_step.dependOn(&static_binding_test_run.step); 76 | } 77 | 78 | // Dynamic C lib. We only build this if this is the native target so we 79 | // can link to libxml2 on our native system. 80 | if (target.isNative()) { 81 | const dynamic_lib_name = if (target.isWindows()) 82 | "flightplan.dll" 83 | else 84 | "flightplan"; 85 | 86 | const dynamic_lib = b.addSharedLibrary(dynamic_lib_name, "src/binding.zig", .unversioned); 87 | addSharedSettings(dynamic_lib, mode, target); 88 | dynamic_lib.linkSystemLibrary("libxml-2.0"); 89 | dynamic_lib.install(); 90 | b.default_step.dependOn(&dynamic_lib.step); 91 | 92 | const dynamic_binding_test = b.addExecutable("dynamic-binding", null); 93 | dynamic_binding_test.setBuildMode(mode); 94 | dynamic_binding_test.setTarget(target); 95 | dynamic_binding_test.linkLibC(); 96 | dynamic_binding_test.addIncludeDir("include"); 97 | dynamic_binding_test.addCSourceFile("examples/basic.c", &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99" }); 98 | dynamic_binding_test.linkLibrary(dynamic_lib); 99 | 100 | const dynamic_binding_test_run = dynamic_binding_test.run(); 101 | test_step.dependOn(&dynamic_binding_test_run.step); 102 | } 103 | 104 | // Headers 105 | const install_header = b.addInstallFileWithDir( 106 | .{ .path = "include/flightplan.h" }, 107 | .header, 108 | "flightplan.h", 109 | ); 110 | b.getInstallStep().dependOn(&install_header.step); 111 | 112 | // pkg-config 113 | { 114 | const file = try std.fs.path.join( 115 | b.allocator, 116 | &[_][]const u8{ b.cache_root, "libflightplan.pc" }, 117 | ); 118 | const pkgconfig_file = try std.fs.cwd().createFile(file, .{}); 119 | 120 | const writer = pkgconfig_file.writer(); 121 | try writer.print( 122 | \\prefix={s} 123 | \\includedir=${{prefix}}/include 124 | \\libdir=${{prefix}}/lib 125 | \\ 126 | \\Name: libflightplan 127 | \\URL: https://github.com/mitchellh/libflightplan 128 | \\Description: Library for reading and writing aviation flight plans. 129 | \\Version: 0.1.0 130 | \\Cflags: -I${{includedir}} 131 | \\Libs: -L${{libdir}} -lflightplan 132 | , .{b.install_prefix}); 133 | defer pkgconfig_file.close(); 134 | 135 | b.installFile(file, "share/pkgconfig/libflightplan.pc"); 136 | } 137 | 138 | if (man_pages) { 139 | const scdoc_step = ScdocStep.create(b); 140 | try scdoc_step.install(); 141 | } 142 | } 143 | 144 | /// The shared settings that we need to apply when building a library or 145 | /// executable using libflightplan. 146 | fn addSharedSettings( 147 | lib: *std.build.LibExeObjStep, 148 | mode: std.builtin.Mode, 149 | target: std.zig.CrossTarget, 150 | ) void { 151 | lib.setBuildMode(mode); 152 | lib.setTarget(target); 153 | lib.addPackage(pkgs.flightplan); 154 | lib.addIncludeDir("src/include"); 155 | lib.addIncludeDir("include"); 156 | lib.linkLibC(); 157 | } 158 | -------------------------------------------------------------------------------- /doc/libflightplan.3.scd: -------------------------------------------------------------------------------- 1 | libflightplan(3) "github.com/mitchellh/libflightplan" "Library Functions Manual" 2 | 3 | # NAME 4 | 5 | libflightplan - library used to read and write aviation flight plans 6 | 7 | # DESCRIPTION 8 | 9 | *libflightplan* is a library for reading and writing flight plans in 10 | various formats. Flight plans are used in aviation to save properties of 11 | one or more flights such as route (waypoints), altitude, source and departure 12 | airport, etc. 13 | 14 | This library is available as a native C library as well as Zig. The man pages 15 | focus on the C API currently. 16 | 17 | # API NOTES 18 | 19 | - fpl_cleanup(3) should be called when all users of the library are done. 20 | This cleans up any global state associatd with the library. 21 | 22 | - The library may allocate global state on the heap to store error 23 | information (accessible via fpl_last_error(3)). 24 | 25 | - The library is not threadsafe. Global error state is stored in thread local 26 | variables. 27 | 28 | # EXAMPLE 29 | 30 | The example below shows how the C API can be used to parse a ForeFlight 31 | flight plan and read route information about it. 32 | 33 | ``` 34 | #include 35 | #include 36 | #include 37 | 38 | int main() { 39 | // Parse our flight plan from an FPL file out of ForeFlight. 40 | flightplan *fpl = fpl_parse_garmin("./test/basic.fpl"); 41 | if (fpl == NULL) { 42 | // We can get a more detailed error. 43 | flightplan_error *err = fpl_last_error(); 44 | printf("error: %s\n", fpl_error_message(err)); 45 | fpl_cleanup(); 46 | return 1; 47 | } 48 | 49 | // Iterate and output the full ordered route. 50 | int max = fpl_route_points_count(fpl); 51 | printf("\nroute: \"%s\" (points: %d)\n", fpl_route_name(fpl), max); 52 | for (int i = 0; i < max; i++) { 53 | flightplan_route_point *point = fpl_route_points_get(fpl, i); 54 | printf(" %s\n", fpl_route_point_identifier(point)); 55 | } 56 | 57 | fpl_free(fpl); 58 | fpl_cleanup(); 59 | return 0; 60 | } 61 | ``` 62 | 63 | # AUTHORS 64 | 65 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 66 | See . 67 | -------------------------------------------------------------------------------- /examples/basic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | int main() { 6 | // Parse our flight plan from an FPL file out of ForeFlight. 7 | flightplan *fpl = fpl_garmin_parse_file("./test/basic.fpl"); 8 | if (fpl == NULL) { 9 | return 1; 10 | } 11 | 12 | // Extract information from our flight plan easily 13 | printf("created at: %s\n\n", fpl_created(fpl)); 14 | 15 | // Iterate through the available waypoints in the flightplan 16 | printf("waypoints: %d\n", fpl_waypoints_count(fpl)); 17 | flightplan_waypoint_iter *iter = fpl_waypoints_iter(fpl); 18 | while (1) { 19 | flightplan_waypoint *wp = fpl_waypoints_next(iter); 20 | if (wp == NULL) { 21 | break; 22 | } 23 | 24 | printf(" %s\t(type: %s,\tlat/lon: %f/%f)\n", 25 | fpl_waypoint_identifier(wp), 26 | fpl_waypoint_type_str(fpl_waypoint_type(wp)), 27 | fpl_waypoint_lat(wp), 28 | fpl_waypoint_lon(wp) 29 | ); 30 | } 31 | fpl_waypoint_iter_free(iter); 32 | 33 | // Iterate through the ordered route 34 | int max = fpl_route_points_count(fpl); 35 | printf("\nroute: \"%s\" (points: %d)\n", fpl_route_name(fpl), max); 36 | for (int i = 0; i < max; i++) { 37 | flightplan_route_point *point = fpl_route_points_get(fpl, i); 38 | printf(" %s\n", fpl_route_point_identifier(point)); 39 | } 40 | 41 | fpl_free(fpl); 42 | fpl_cleanup(); 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1641205782, 7 | "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "locked": { 21 | "lastModified": 1638122382, 22 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 23 | "owner": "numtide", 24 | "repo": "flake-utils", 25 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "type": "github" 32 | } 33 | }, 34 | "flake-utils_2": { 35 | "locked": { 36 | "lastModified": 1629481132, 37 | "narHash": "sha256-JHgasjPR0/J1J3DRm4KxM4zTyAj4IOJY8vIl75v/kPI=", 38 | "owner": "numtide", 39 | "repo": "flake-utils", 40 | "rev": "997f7efcb746a9c140ce1f13c72263189225f482", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "type": "github" 47 | } 48 | }, 49 | "nixpkgs": { 50 | "locked": { 51 | "lastModified": 1642069818, 52 | "narHash": "sha256-666w6j8wl/bojfgpp0k58/UJ5rbrdYFbI2RFT2BXbSQ=", 53 | "owner": "nixos", 54 | "repo": "nixpkgs", 55 | "rev": "46821ea01c8f54d2a20f5a503809abfc605269d7", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "nixos", 60 | "ref": "nixpkgs-unstable", 61 | "repo": "nixpkgs", 62 | "type": "github" 63 | } 64 | }, 65 | "nixpkgs_2": { 66 | "locked": { 67 | "lastModified": 1631288242, 68 | "narHash": "sha256-sXm4KiKs7qSIf5oTAmrlsEvBW193sFj+tKYVirBaXz0=", 69 | "owner": "NixOS", 70 | "repo": "nixpkgs", 71 | "rev": "0e24c87754430cb6ad2f8c8c8021b29834a8845e", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "owner": "NixOS", 76 | "ref": "nixpkgs-unstable", 77 | "repo": "nixpkgs", 78 | "type": "github" 79 | } 80 | }, 81 | "root": { 82 | "inputs": { 83 | "flake-compat": "flake-compat", 84 | "flake-utils": "flake-utils", 85 | "nixpkgs": "nixpkgs", 86 | "zig": "zig", 87 | "zig-libxml2-src": "zig-libxml2-src" 88 | } 89 | }, 90 | "zig": { 91 | "inputs": { 92 | "flake-utils": "flake-utils_2", 93 | "nixpkgs": "nixpkgs_2" 94 | }, 95 | "locked": { 96 | "lastModified": 1642206480, 97 | "narHash": "sha256-aS5zbhz+KXmDYBINbEabnVEhp7OQ0yR/O7pfFZqKRPw=", 98 | "owner": "roarkanize", 99 | "repo": "zig-overlay", 100 | "rev": "78e7670a7e1d57a60819dc1bfa026670bf09c48c", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "roarkanize", 105 | "repo": "zig-overlay", 106 | "type": "github" 107 | } 108 | }, 109 | "zig-libxml2-src": { 110 | "flake": false, 111 | "locked": { 112 | "lastModified": 1642210805, 113 | "narHash": "sha256-zQh4yqCOetocb8fV/0Rgbq3JcMhaJQKGgvmsSrdl/h4=", 114 | "ref": "main", 115 | "rev": "c2cf5ec294d08adfa0fc7aea7245a83871ed19f2", 116 | "revCount": 10, 117 | "submodules": true, 118 | "type": "git", 119 | "url": "https://github.com/mitchellh/zig-libxml2.git" 120 | }, 121 | "original": { 122 | "ref": "main", 123 | "submodules": true, 124 | "type": "git", 125 | "url": "https://github.com/mitchellh/zig-libxml2.git" 126 | } 127 | } 128 | }, 129 | "root": "root", 130 | "version": 7 131 | } 132 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "libflightplan"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | zig.url = "github:roarkanize/zig-overlay"; 8 | 9 | # Used for shell.nix 10 | flake-compat = { url = github:edolstra/flake-compat; flake = false; }; 11 | 12 | # Dependencies we track using flake.lock 13 | zig-libxml2-src = { 14 | url = "https://github.com/mitchellh/zig-libxml2.git"; 15 | flake = false; 16 | submodules = true; 17 | type = "git"; 18 | ref = "main"; 19 | }; 20 | }; 21 | 22 | outputs = { self, nixpkgs, flake-utils, ... }@inputs: 23 | let 24 | overlays = [ 25 | # Our repo overlay 26 | (import ./nix/overlay.nix) 27 | 28 | # Other overlays 29 | (final: prev: { 30 | zigpkgs = inputs.zig.packages.${prev.system}; 31 | zig-libxml2-src = inputs.zig-libxml2-src; 32 | }) 33 | ]; 34 | 35 | # Our supported systems are the same supported systems as the Zig binaries 36 | systems = builtins.attrNames inputs.zig.packages; 37 | in flake-utils.lib.eachSystem systems (system: 38 | let pkgs = import nixpkgs { inherit overlays system; }; 39 | in rec { 40 | devShell = pkgs.devShell; 41 | packages.libflightplan = pkgs.libflightplan; 42 | defaultPackage = packages.libflightplan; 43 | } 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /include/flightplan.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBFLIGHTPLAN_H_GUARD 2 | #define LIBFLIGHTPLAN_H_GUARD 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | /* 9 | * NAME fpl_cleanup() 10 | * 11 | * DESCRIPTION 12 | * 13 | * This should be called when the process is done using this library 14 | * to perform any global level memory cleanup (really just any errors). 15 | * This is safe to call multiple times. 16 | * */ 17 | void fpl_cleanup(); 18 | 19 | // A flightplan represents the primary flightplan data structure. 20 | typedef void flightplan; 21 | 22 | /* 23 | * NAME fpl_new() 24 | * 25 | * DESCRIPTION 26 | * 27 | * Create a new empty flight plan. 28 | */ 29 | flightplan *fpl_new(); 30 | 31 | /* 32 | * NAME fpl_free() 33 | * 34 | * DESCRIPTION 35 | * 36 | * Free resources associated with a flight plan. The flight plan can no longer 37 | * be used after this is called. This must be called for any flight plan that 38 | * is returned. 39 | */ 40 | void fpl_free(flightplan *); 41 | 42 | /* 43 | * NAME fpl_created() 44 | * 45 | * DESCRIPTION 46 | * 47 | * Returns the timestamp when the flight plan was created. 48 | * 49 | * NOTE(mitchellh): This raw string is not what I want long term. I want to 50 | * convert this to a UTC unix timestamp, so this function will probably change 51 | * to a time_t result at some point. 52 | */ 53 | char *fpl_created(flightplan *); 54 | 55 | /************************************************************************** 56 | * Errors 57 | *************************************************************************/ 58 | 59 | typedef void flightplan_error; 60 | 61 | /* 62 | * NAME fpl_last_error() 63 | * 64 | * DESCRIPTION 65 | * 66 | * Returns the last error (if any). An error can be set in any situation 67 | * where a function returns NULL or otherwise noted by the documentation. 68 | * The error doesn't need to be freed; any memory associated with error storage 69 | * is freed when fpl_cleanup is called. 70 | * 71 | * This error is only valid until another error occurs. 72 | * */ 73 | flightplan_error *fpl_last_error(); 74 | 75 | /* 76 | * NAME fpl_error_message() 77 | * 78 | * DESCRIPTION 79 | * 80 | * Returns a human-friendly error message for this error. 81 | * */ 82 | char *fpl_error_message(flightplan_error *); 83 | 84 | /************************************************************************** 85 | * Import/Export 86 | *************************************************************************/ 87 | 88 | /* 89 | * NAME fpl_garmin_parse_file() 90 | * 91 | * DESCRIPTION 92 | * 93 | * Parse a Garmin FPL file. This is also compatible with ForeFlight. 94 | */ 95 | flightplan *fpl_garmin_parse_file(char *); 96 | 97 | /* 98 | * NAME fpl_garmin_write_to_file() 99 | * 100 | * DESCRIPTION 101 | * 102 | * Write a flight plan in Garmin FPL format to the given file. 103 | */ 104 | int fpl_garmin_write_to_file(flightplan *, char *); 105 | 106 | /* 107 | * NAME fpl_xplane11_write_to_file() 108 | * 109 | * DESCRIPTION 110 | * 111 | * Write a flight plan in X-Plane 11 FMS format to the given file. 112 | */ 113 | int fpl_xplane11_write_to_file(flightplan *, char *); 114 | 115 | /************************************************************************** 116 | * Waypoints 117 | *************************************************************************/ 118 | 119 | // A waypoint that the flight plan may or may not use but knows about. 120 | typedef void flightplan_waypoint; 121 | typedef void flightplan_waypoint_iter; 122 | 123 | // Types of waypoints. 124 | typedef enum { 125 | FLIGHTPLAN_INVALID, 126 | FLIGHTPLAN_USER_WAYPOINT, 127 | FLIGHTPLAN_AIRPORT, 128 | FLIGHTPLAN_NDB, 129 | FLIGHTPLAN_VOR, 130 | FLIGHTPLAN_INT, 131 | FLIGHTPLAN_INT_VRP, 132 | } flightplan_waypoint_type; 133 | 134 | /* 135 | * NAME fpl_waypoints_count() 136 | * 137 | * DESCRIPTION 138 | * 139 | * Returns the total number of waypoints that are in this flight plan. 140 | */ 141 | int fpl_waypoints_count(flightplan *); 142 | 143 | /* 144 | * NAME fpl_waypoint_iter() 145 | * 146 | * DESCRIPTION 147 | * 148 | * Returns an iterator that can be used to read each of the waypoints. 149 | * The iterator is only valid so long as zero modifications are made 150 | * to the waypoint list. 151 | * 152 | * The iterator must be freed with fpl_waypoint_iter_free. 153 | */ 154 | flightplan_waypoint_iter *fpl_waypoints_iter(flightplan *); 155 | 156 | /* 157 | * NAME fpl_waypoint_iter_free() 158 | * 159 | * DESCRIPTION 160 | * 161 | * Free resources associated with an iterator. 162 | */ 163 | void fpl_waypoint_iter_free(flightplan_waypoint_iter *); 164 | 165 | /* 166 | * NAME fpl_waypoints_next() 167 | * 168 | * DESCRIPTION 169 | * 170 | * Get the next waypoint for the iterator. This returns NULL when there are 171 | * no more waypoints available. The values returned should NOT be manually 172 | * freed, they are owned by the flight plan. 173 | */ 174 | flightplan_waypoint *fpl_waypoints_next(flightplan_waypoint_iter *); 175 | 176 | // TODO 177 | flightplan_waypoint *fpl_waypoint_new(); 178 | void fpl_waypoint_free(flightplan_waypoint *); 179 | 180 | /* 181 | * NAME fpl_waypoint_identifier() 182 | * 183 | * DESCRIPTION 184 | * 185 | * Return the unique identifier for this waypoint. 186 | */ 187 | char *fpl_waypoint_identifier(flightplan_waypoint *); 188 | 189 | /* 190 | * NAME fpl_waypoint_lat() 191 | * 192 | * DESCRIPTION 193 | * 194 | * Return the latitude for this waypoint as a decimal value. 195 | */ 196 | float fpl_waypoint_lat(flightplan_waypoint *); 197 | 198 | /* 199 | * NAME fpl_waypoint_lon() 200 | * 201 | * DESCRIPTION 202 | * 203 | * Return the longitude for this waypoint as a decimal value. 204 | */ 205 | float fpl_waypoint_lon(flightplan_waypoint *); 206 | 207 | /* 208 | * NAME fpl_waypoint_type() 209 | * 210 | * DESCRIPTION 211 | * 212 | * Returns the type of this waypoint. 213 | */ 214 | flightplan_waypoint_type fpl_waypoint_type(flightplan_waypoint *); 215 | 216 | /* 217 | * NAME fpl_waypoint_type_str() 218 | * 219 | * DESCRIPTION 220 | * 221 | * Convert a waypoint type to a string value. 222 | */ 223 | char *fpl_waypoint_type_str(flightplan_waypoint_type); 224 | 225 | /************************************************************************** 226 | * Route 227 | *************************************************************************/ 228 | 229 | typedef void flightplan_route_point; 230 | typedef void flightplan_route_point_iter; 231 | 232 | /* 233 | * NAME fpl_route_name() 234 | * 235 | * DESCRIPTION 236 | * 237 | * The name of the route. 238 | */ 239 | char *fpl_route_name(flightplan *); 240 | 241 | /* 242 | * NAME fpl_route_points_count() 243 | * 244 | * DESCRIPTION 245 | * 246 | * Returns the total number of route points that are in this flight plan. 247 | */ 248 | int fpl_route_points_count(flightplan *); 249 | 250 | /* 251 | * NAME fpl_route_points_get() 252 | * 253 | * DESCRIPTION 254 | * 255 | * Returns the route point at the given index in the route. index must be 256 | * greater than 0 and less than fpl_route_points_count(). 257 | */ 258 | flightplan_route_point *fpl_route_points_get(flightplan *, int); 259 | 260 | /* 261 | * NAME fpl_route_point_identifier() 262 | * 263 | * DESCRIPTION 264 | * 265 | * Returns the identifier of this route point. This should match a waypoint 266 | * in the flight plan if it is validly formed. 267 | */ 268 | char *fpl_route_point_identifier(flightplan_route_point *); 269 | 270 | #ifdef __cplusplus 271 | } 272 | #endif 273 | 274 | #endif 275 | -------------------------------------------------------------------------------- /nix/devshell.nix: -------------------------------------------------------------------------------- 1 | { mkShell 2 | 3 | , pkg-config 4 | , libxml2 5 | , scdoc 6 | , zig 7 | }: mkShell rec { 8 | name = "libflightplan"; 9 | 10 | nativeBuildInputs = [ 11 | pkg-config 12 | scdoc 13 | zig 14 | ]; 15 | 16 | buildInputs = [ 17 | libxml2 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /nix/overlay.nix: -------------------------------------------------------------------------------- 1 | final: prev: rec { 2 | # Notes: 3 | # 4 | # When determining a SHA256, use this to set a fake one until we know 5 | # the real value: 6 | # 7 | # vendorSha256 = nixpkgs.lib.fakeSha256; 8 | # 9 | 10 | devShell = prev.callPackage ./devshell.nix { }; 11 | libflightplan = prev.callPackage ./package.nix { }; 12 | 13 | # zig we want to be the latest nightly since 0.9.0 is not released yet. 14 | zig = final.zigpkgs.master.latest; 15 | } 16 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , zig 4 | , pkg-config 5 | , scdoc 6 | , libxml2 7 | , zig-libxml2-src 8 | }: 9 | 10 | stdenv.mkDerivation rec { 11 | pname = "libflightplan"; 12 | version = "0.1.0"; 13 | 14 | src = ./..; 15 | 16 | nativeBuildInputs = [ zig scdoc pkg-config ]; 17 | 18 | buildInputs = [ 19 | libxml2 20 | ]; 21 | 22 | dontConfigure = true; 23 | 24 | preBuild = '' 25 | export HOME=$TMPDIR 26 | mkdir -p ./vendor/zig-libxml2 27 | cp -r ${zig-libxml2-src}/* ./vendor/zig-libxml2 28 | ''; 29 | 30 | installPhase = '' 31 | runHook preInstall 32 | zig build -Drelease-safe -Dman-pages --prefix $out install 33 | runHook postInstall 34 | ''; 35 | 36 | outputs = [ "out" "dev" "man" ]; 37 | 38 | meta = with lib; { 39 | description = "A library for reading and writing flight plans in various formats"; 40 | homepage = "https://github.com/mitchellh/libflightplan"; 41 | license = licenses.mit; 42 | platforms = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let flake-compat = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flake-compat; in 4 | fetchTarball { 5 | url = "https://github.com/edolstra/flake-compat/archive/${flake-compat.locked.rev}.tar.gz"; 6 | sha256 = flake-compat.locked.narHash; 7 | } 8 | ) 9 | { src = ./.; }).shellNix 10 | -------------------------------------------------------------------------------- /src-build/ScdocStep.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const fs = std.fs; 4 | const Step = std.build.Step; 5 | const Builder = std.build.Builder; 6 | 7 | /// ScdocStep generates man pages using scdoc(1). 8 | /// 9 | /// It reads all the raw pages from src_path and writes them to out_path. 10 | /// src_path is typically "doc/" relative to the build root and out_path is 11 | /// the build cache. 12 | /// 13 | /// The man pages can be installed by calling install() on the step. 14 | const ScdocStep = @This(); 15 | 16 | step: Step, 17 | builder: *Builder, 18 | 19 | /// path to read man page sources from, defaults to the "doc/" subdirectory 20 | /// from the build.zig file. This must be an absolute path. 21 | src_path: []const u8, 22 | 23 | /// path where the generated man pages will be written (NOT installed). This 24 | /// defaults to build cache root. 25 | out_path: []const u8, 26 | 27 | pub fn create(builder: *Builder) *ScdocStep { 28 | const self = builder.allocator.create(ScdocStep) catch unreachable; 29 | self.* = init(builder); 30 | return self; 31 | } 32 | 33 | pub fn init(builder: *Builder) ScdocStep { 34 | return ScdocStep{ 35 | .builder = builder, 36 | .step = Step.init(.custom, "generate man pages", builder.allocator, make), 37 | .src_path = builder.pathFromRoot("doc/"), 38 | .out_path = fs.path.join(builder.allocator, &[_][]const u8{ 39 | builder.cache_root, 40 | "man", 41 | }) catch unreachable, 42 | }; 43 | } 44 | 45 | fn make(step: *std.build.Step) !void { 46 | const self = @fieldParentPtr(ScdocStep, "step", step); 47 | 48 | // Create our cache path 49 | // TODO(mitchellh): ideally this would be pure zig 50 | { 51 | const command = try std.fmt.allocPrint( 52 | self.builder.allocator, 53 | "rm -f {[path]s}/* && mkdir -p {[path]s}", 54 | .{ .path = self.out_path }, 55 | ); 56 | _ = try self.builder.exec(&[_][]const u8{ "sh", "-c", command }); 57 | } 58 | 59 | // Find all our man pages which are in our src path ending with ".scd". 60 | var dir = try fs.openDirAbsolute(self.src_path, .{ 61 | .iterate = true, 62 | }); 63 | defer dir.close(); 64 | 65 | var iter = dir.iterate(); 66 | while (try iter.next()) |*entry| { 67 | // We only want "scd" files to generate. 68 | if (!mem.eql(u8, fs.path.extension(entry.name), ".scd")) { 69 | continue; 70 | } 71 | 72 | const src = try fs.path.join( 73 | self.builder.allocator, 74 | &[_][]const u8{ self.src_path, entry.name }, 75 | ); 76 | 77 | const dst = try fs.path.join( 78 | self.builder.allocator, 79 | &[_][]const u8{ self.out_path, entry.name[0..(entry.name.len - 4)] }, 80 | ); 81 | 82 | const command = try std.fmt.allocPrint( 83 | self.builder.allocator, 84 | "scdoc < {s} > {s}", 85 | .{ src, dst }, 86 | ); 87 | _ = try self.builder.exec(&[_][]const u8{ "sh", "-c", command }); 88 | } 89 | } 90 | 91 | pub fn install(self: *ScdocStep) !void { 92 | // Ensure that `zig build install` depends on our generation step first. 93 | self.builder.getInstallStep().dependOn(&self.step); 94 | 95 | // Then run our install step which looks at what we made out of our 96 | // generation and moves it to the install prefix. 97 | const install_step = InstallStep.create(self.builder, self); 98 | self.builder.getInstallStep().dependOn(&install_step.step); 99 | } 100 | 101 | /// Install man pages, create using install() on ScdocStep. 102 | const InstallStep = struct { 103 | step: Step, 104 | builder: *Builder, 105 | scdoc: *ScdocStep, 106 | 107 | pub fn create(builder: *Builder, scdoc: *ScdocStep) *InstallStep { 108 | const self = builder.allocator.create(InstallStep) catch unreachable; 109 | self.* = InstallStep.init(builder, scdoc); 110 | return self; 111 | } 112 | 113 | pub fn init(builder: *Builder, scdoc: *ScdocStep) InstallStep { 114 | return InstallStep{ 115 | .builder = builder, 116 | .step = Step.init(.custom, "generate man pages", builder.allocator, InstallStep.make), 117 | .scdoc = scdoc, 118 | }; 119 | } 120 | 121 | fn make(step: *Step) !void { 122 | const self = @fieldParentPtr(InstallStep, "step", step); 123 | 124 | // Get our absolute output path 125 | var path = self.scdoc.out_path; 126 | if (!fs.path.isAbsolute(path)) { 127 | path = self.builder.pathFromRoot(path); 128 | } 129 | 130 | // Find all our man pages which are in our src path ending with ".scd". 131 | var dir = try fs.openDirAbsolute(path, .{ .iterate = true }); 132 | defer dir.close(); 133 | var iter = dir.iterate(); 134 | while (try iter.next()) |*entry| { 135 | // We expect filenames to be "foo.3" and this gets us "3" 136 | const section = entry.name[(entry.name.len - 1)..]; 137 | 138 | const src = try fs.path.join( 139 | self.builder.allocator, 140 | &[_][]const u8{ path, entry.name }, 141 | ); 142 | const output = try std.fmt.allocPrint( 143 | self.builder.allocator, 144 | "share/man/man{s}/{s}", 145 | .{ section, entry.name }, 146 | ); 147 | 148 | const fileStep = self.builder.addInstallFile( 149 | .{ .path = src }, 150 | output, 151 | ); 152 | try fileStep.step.make(); 153 | } 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /src/Departure.zig: -------------------------------------------------------------------------------- 1 | /// Departure represents information about the departure portion of a flight 2 | /// plan, such as the departing airport, runway, procedure, transition, etc. 3 | /// 4 | /// This is just the departure procedure metadata. The route of the DP is 5 | /// expected to still be added manually to the FlightPlan's route field. 6 | const Self = @This(); 7 | 8 | const std = @import("std"); 9 | const Allocator = std.mem.Allocator; 10 | const Runway = @import("Runway.zig"); 11 | 12 | /// Departure waypoint ID. This waypoint must be present in the waypoints map 13 | /// on a flight plan for more information such as lat/lon. This doesn't have to 14 | /// be an airport, this can be a VOR or another NAVAID. 15 | identifier: [:0]const u8, 16 | 17 | /// Departure runway. While this can be set for any identifier, note that 18 | /// a runway is non-sensical for a non-airport identifier. 19 | runway: ?Runway = null, 20 | 21 | // Name of the SID used for departure (if any) 22 | sid: ?[:0]const u8 = null, 23 | 24 | // Name of the departure transition (if any). This may be set when sid 25 | // is null but that makes no sense. 26 | transition: ?[:0]const u8 = null, 27 | 28 | pub fn deinit(self: *Self, alloc: Allocator) void { 29 | alloc.free(self.identifier); 30 | if (self.sid) |v| alloc.free(v); 31 | if (self.transition) |v| alloc.free(v); 32 | self.* = undefined; 33 | } 34 | -------------------------------------------------------------------------------- /src/Destination.zig: -------------------------------------------------------------------------------- 1 | /// Destination represents information about the destination portion of a flight 2 | /// plan, such as the destination airport, arrival, approach, etc. 3 | const Self = @This(); 4 | 5 | const std = @import("std"); 6 | const Allocator = std.mem.Allocator; 7 | const Runway = @import("Runway.zig"); 8 | 9 | /// Destination waypoint ID. This waypoint must be present in the waypoints map 10 | /// on a flight plan for more information such as lat/lon. This doesn't have to 11 | /// be an airport, this can be a VOR or another NAVAID. 12 | identifier: [:0]const u8, 13 | 14 | /// Destination runway. While this can be set for any identifier, note that 15 | /// a runway is non-sensical for a non-airport identifier. 16 | runway: ?Runway = null, 17 | 18 | // Name of the STAR used for arrival (if any). 19 | star: ?[:0]const u8 = null, 20 | 21 | // Name of the arrival transition (if any). 22 | star_transition: ?[:0]const u8 = null, 23 | 24 | // Name of the approach used for arrival (if any). The recommended format 25 | // is the ARINC 424-18 format, such as LOCD, I26L, etc. 26 | approach: ?[:0]const u8 = null, 27 | 28 | // Name of the arrival transition (if any). 29 | approach_transition: ?[:0]const u8 = null, 30 | 31 | pub fn deinit(self: *Self, alloc: Allocator) void { 32 | alloc.free(self.identifier); 33 | if (self.star) |v| alloc.free(v); 34 | if (self.star_transition) |v| alloc.free(v); 35 | if (self.approach) |v| alloc.free(v); 36 | if (self.approach_transition) |v| alloc.free(v); 37 | self.* = undefined; 38 | } 39 | -------------------------------------------------------------------------------- /src/Error.zig: -------------------------------------------------------------------------------- 1 | const Self = @This(); 2 | 3 | const std = @import("std"); 4 | const Allocator = std.mem.Allocator; 5 | 6 | const c = @import("xml.zig").c; 7 | 8 | /// Possible errors that can be returned by many of the functions 9 | /// in this library. See the function doc comments for details on 10 | /// exactly which of these can be returnd. 11 | pub const Set = error{ 12 | OutOfMemory, 13 | Unimplemented, 14 | ReadFailed, 15 | WriteFailed, 16 | NodeExpected, 17 | InvalidElement, 18 | RequiredValueMissing, 19 | RouteMissingWaypoint, 20 | }; 21 | 22 | /// last error that occurred, this MIGHT be set if an error code is returned. 23 | /// This is thread local so using this library in a threaded environment will 24 | /// store errors separately. 25 | threadlocal var _lastError: ?Self = null; 26 | 27 | /// Error code for this error 28 | code: Set, 29 | 30 | /// Additional details for this error. Whether this is set depends on what 31 | /// triggered the error. The type of this is dependent on the context in which 32 | /// the error was triggered. 33 | detail: ?Detail = null, 34 | 35 | /// Extra details for an error. What is set is dependent on what raised the error. 36 | pub const Detail = union(enum) { 37 | /// message is a basic string message. 38 | string: String, 39 | 40 | /// xml-specific error message (typically a parse error) 41 | xml: XMLDetail, 42 | 43 | /// Gets a human-friendly message regardless of type. 44 | pub fn message(self: *Detail) [:0]const u8 { 45 | switch (self.*) { 46 | .string => |*v| return v.message, 47 | .xml => |*v| return v.message(), 48 | } 49 | } 50 | 51 | pub fn deinit(self: Detail) void { 52 | switch (self) { 53 | .string => |v| v.deinit(), 54 | .xml => |v| v.deinit(), 55 | } 56 | } 57 | 58 | /// XMLDetail when an XML-related error occurs for formats that use XML. 59 | pub const XMLDetail = struct { 60 | pub const Context = union(enum) { 61 | global: void, 62 | parser: c.xmlParserCtxtPtr, 63 | writer: c.xmlTextWriterPtr, 64 | }; 65 | 66 | ctx: Context, 67 | 68 | /// Return the raw xmlError structure. 69 | pub fn err(self: *XMLDetail) ?*c.xmlError { 70 | return switch (self.ctx) { 71 | .global => c.xmlGetLastError(), 72 | .parser => |ptr| c.xmlCtxtGetLastError(ptr), 73 | .writer => |ptr| c.xmlCtxtGetLastError(ptr), 74 | }; 75 | } 76 | 77 | pub fn message(self: *XMLDetail) [:0]const u8 { 78 | const v = self.err() orelse return "no error"; 79 | return std.mem.span(v.message); 80 | } 81 | 82 | pub fn deinit(self: XMLDetail) void { 83 | switch (self.ctx) { 84 | .global => {}, 85 | .parser => |ptr| c.xmlFreeParserCtxt(ptr), 86 | .writer => |ptr| c.xmlFreeTextWriter(ptr), 87 | } 88 | } 89 | }; 90 | 91 | pub const String = struct { 92 | alloc: Allocator, 93 | message: [:0]const u8, 94 | 95 | pub fn init(alloc: Allocator, comptime fmt: []const u8, args: anytype) !String { 96 | const msg = try std.fmt.allocPrintZ(alloc, fmt, args); 97 | return String{ .alloc = alloc, .message = msg }; 98 | } 99 | 100 | pub fn deinit(self: String) void { 101 | self.alloc.free(self.message); 102 | } 103 | }; 104 | }; 105 | 106 | /// Helper to easily initialize an error with a message. 107 | pub fn initMessage(alloc: Allocator, code: Set, comptime fmt: []const u8, args: anytype) !Self { 108 | const detail = Detail{ 109 | .string = try Detail.String.init(alloc, fmt, args), 110 | }; 111 | return Self{ 112 | .code = code, 113 | .detail = detail, 114 | }; 115 | } 116 | 117 | /// Returns a human-friendly message about the error. 118 | pub fn message(self: *Self) [:0]const u8 { 119 | if (self.detail) |*detail| { 120 | return detail.message(); 121 | } 122 | 123 | return "no error message"; 124 | } 125 | 126 | /// Release resources associated with an error. 127 | pub fn deinit(self: Self) void { 128 | if (self.detail) |detail| { 129 | detail.deinit(); 130 | } 131 | } 132 | 133 | /// Return the last error (if any). 134 | pub inline fn lastError() ?*Self { 135 | if (_lastError) |*err| { 136 | return err; 137 | } 138 | 139 | return null; 140 | } 141 | 142 | // Set a new last error. 143 | pub fn setLastError(err: ?Self) void { 144 | // Unset previous error if there is one. 145 | if (_lastError) |last| { 146 | last.deinit(); 147 | } 148 | 149 | _lastError = err; 150 | } 151 | 152 | /// Set a new last error that was an XML error. 153 | pub fn setLastErrorXML(code: Set, ctx: Detail.XMLDetail.Context) Set { 154 | // Can't nest it all due to: https://github.com/ziglang/zig/issues/6043 155 | const detail = Detail{ .xml = .{ .ctx = ctx } }; 156 | setLastError(Self{ 157 | .code = code, 158 | .detail = detail, 159 | }); 160 | 161 | return code; 162 | } 163 | 164 | test "set last error" { 165 | // Setting it while null does nothing 166 | setLastError(null); 167 | setLastError(null); 168 | 169 | // Can set and retrieve 170 | setLastError(Self{ .code = Set.ReadFailed }); 171 | const err = lastError().?; 172 | try std.testing.expectEqual(err.code, Set.ReadFailed); 173 | 174 | // Can set to null 175 | setLastError(null); 176 | try std.testing.expect(lastError() == null); 177 | } 178 | -------------------------------------------------------------------------------- /src/FlightPlan.zig: -------------------------------------------------------------------------------- 1 | /// The primary abstract flight plan structure. This is the structure that 2 | /// various formats decode to an encode from. 3 | /// 4 | /// Note all features of this structure are not supported by all formats. 5 | /// For example, the flight rules field (IFR or VFR) is not used at all by 6 | /// the Garmin or ForeFlight FPL formats, but is used by MSFS 2020 PLN. 7 | /// Formats just ignore information they don't use. 8 | const Self = @This(); 9 | 10 | const std = @import("std"); 11 | const hash_map = std.hash_map; 12 | const Allocator = std.mem.Allocator; 13 | 14 | const Waypoint = @import("Waypoint.zig"); 15 | const Departure = @import("Departure.zig"); 16 | const Destination = @import("Destination.zig"); 17 | const Route = @import("Route.zig"); 18 | 19 | /// Allocator associated with this FlightPlan. This allocator must be 20 | /// used for all the memory owned by this structure for deinit to work. 21 | alloc: Allocator, 22 | 23 | // The type of flight rules, assumes IFR. 24 | rules: Rules = .ifr, 25 | 26 | /// The AIRAC cycle used to create this flight plan, i.e. 2201. 27 | /// See: https://en.wikipedia.org/wiki/Aeronautical_Information_Publication 28 | /// This is expected to be heap-allocated and will be freed on deinit. 29 | airac: ?[:0]const u8 = null, 30 | 31 | /// The timestamp when this flight plan was created. This is expected to 32 | /// be heap-allocated and will be freed on deinit. 33 | /// TODO: some well known format 34 | created: ?[:0]const u8 = null, 35 | 36 | /// Departure information 37 | departure: ?Departure = null, 38 | 39 | /// Destination information 40 | destination: ?Destination = null, 41 | 42 | /// Waypoints that are part of the route. These are unordered, they are 43 | /// just the full list of possible waypoints that the route may contain. 44 | waypoints: hash_map.StringHashMapUnmanaged(Waypoint) = .{}, 45 | 46 | /// The flight plan route. This route may only contain waypoints in the 47 | /// waypoints map. 48 | route: Route = .{}, 49 | 50 | /// Flight rules types 51 | pub const Rules = enum { 52 | vfr, 53 | ifr, 54 | }; 55 | 56 | /// Clean up resources associated with the flight plan. This should 57 | /// always be called for any created flight plan when it is no longer in use. 58 | pub fn deinit(self: *Self) void { 59 | if (self.airac) |v| self.alloc.free(v); 60 | if (self.created) |v| self.alloc.free(v); 61 | if (self.departure) |*dep| dep.deinit(self.alloc); 62 | if (self.destination) |*des| des.deinit(self.alloc); 63 | 64 | self.route.deinit(self.alloc); 65 | 66 | var it = self.waypoints.iterator(); 67 | while (it.next()) |kv| { 68 | kv.value_ptr.deinit(self.alloc); 69 | } 70 | self.waypoints.deinit(self.alloc); 71 | 72 | self.* = undefined; 73 | } 74 | 75 | test { 76 | _ = Waypoint; 77 | _ = @import("binding.zig"); 78 | } 79 | -------------------------------------------------------------------------------- /src/Route.zig: -------------------------------------------------------------------------------- 1 | /// Route structure represents an ordered list of waypoints (and other 2 | /// potential metadata) for a route in a flight plan. 3 | const Self = @This(); 4 | 5 | const std = @import("std"); 6 | const Allocator = std.mem.Allocator; 7 | 8 | const PointsList = std.ArrayListUnmanaged(Point); 9 | 10 | /// Name of the route, human-friendly. 11 | name: ?[:0]const u8 = null, 12 | 13 | /// Ordered list of points in the route. Currently, each value is a string 14 | /// matching the name of a Waypoint. In the future, this will be changed 15 | /// to a rich struct that has more information. 16 | points: PointsList = .{}, 17 | 18 | /// Point is a point in a route. 19 | pub const Point = struct { 20 | /// Identifier of this route point, MUST correspond to a matching 21 | /// waypoint in the flight plan or most encoding will fail. 22 | identifier: [:0]const u8, 23 | 24 | /// The route that this point is via, such as an airway. This is used 25 | /// by certain formats and ignored by most. 26 | via: ?Via = null, 27 | 28 | /// Altitude in feet (MSL, AGL, whatever you'd like for your flight 29 | /// plan and format). This is used by some formats to note the desired 30 | /// altitude at a given point. This can be zero to note cruising altitude 31 | /// or field elevation. 32 | altitude: u16 = 0, 33 | 34 | pub const Via = union(enum) { 35 | airport_departure: void, 36 | airport_destination: void, 37 | direct: void, 38 | airway: [:0]const u8, 39 | }; 40 | 41 | pub fn deinit(self: *Point, alloc: Allocator) void { 42 | alloc.free(self.identifier); 43 | self.* = undefined; 44 | } 45 | }; 46 | 47 | pub fn deinit(self: *Self, alloc: Allocator) void { 48 | if (self.name) |v| alloc.free(v); 49 | while (self.points.popOrNull()) |*v| v.deinit(alloc); 50 | self.points.deinit(alloc); 51 | self.* = undefined; 52 | } 53 | -------------------------------------------------------------------------------- /src/Runway.zig: -------------------------------------------------------------------------------- 1 | const Self = @This(); 2 | 3 | const std = @import("std"); 4 | const testing = std.testing; 5 | 6 | // Number is the runway number such as "25" 7 | number: u16, 8 | 9 | // Position is the relative position of the runway, if any, such as 10 | // L, R, C. This is a byte so that it can be any ASCII character, but 11 | position: ?Position = null, 12 | 13 | /// Position is the potential position of runways with matching numbers. 14 | pub const Position = enum { 15 | L, 16 | R, 17 | C, 18 | }; 19 | 20 | /// Runway string such as "15L", "15", etc. The buffer must be at least 21 | /// 3 characters large. If the buffer isn't large enough you'll get an error. 22 | pub fn toString(self: Self, buf: []u8) ![:0]u8 { 23 | var posString: [:0]const u8 = ""; 24 | if (self.position) |pos| { 25 | posString = @tagName(pos); 26 | } 27 | 28 | return try std.fmt.bufPrintZ(buf, "{d:0>2}{s}", .{ self.number, posString }); 29 | } 30 | 31 | test "string" { 32 | var buf: [6]u8 = undefined; 33 | 34 | { 35 | const rwy = Self{ .number = 25 }; 36 | try testing.expectEqualStrings(try rwy.toString(&buf), "25"); 37 | } 38 | 39 | { 40 | const rwy = Self{ .number = 25, .position = .L }; 41 | try testing.expectEqualStrings(try rwy.toString(&buf), "25L"); 42 | } 43 | 44 | { 45 | const rwy = Self{ .number = 1, .position = .C }; 46 | try testing.expectEqualStrings(try rwy.toString(&buf), "01C"); 47 | } 48 | 49 | // Stupid but should work 50 | { 51 | const rwy = Self{ .number = 679 }; 52 | try testing.expectEqualStrings(try rwy.toString(&buf), "679"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Waypoint.zig: -------------------------------------------------------------------------------- 1 | /// Waypoint structure is a single potential waypoint in a route. This 2 | /// contains all the metadata about the waypoint. 3 | const Self = @This(); 4 | 5 | const std = @import("std"); 6 | const Allocator = std.mem.Allocator; 7 | const mem = std.mem; 8 | 9 | /// Name of the waypoint. This is a key that is used by the route to lookup 10 | /// the waypoint. 11 | identifier: [:0]const u8, 12 | 13 | /// Type of the waypoint, such as VOR, NDB, etc. 14 | type: Type, 15 | 16 | /// Latitude and longitude of this waypoint. 17 | lat: f32 = 0, 18 | lon: f32 = 0, 19 | 20 | pub const Type = enum { 21 | user_waypoint, 22 | airport, 23 | ndb, 24 | vor, 25 | int, 26 | int_vrp, 27 | 28 | pub fn fromString(v: []const u8) Type { 29 | if (mem.eql(u8, v, "AIRPORT")) { 30 | return .airport; 31 | } else if (mem.eql(u8, v, "NDB")) { 32 | return .ndb; 33 | } else if (mem.eql(u8, v, "USER WAYPOINT")) { 34 | return .user_waypoint; 35 | } else if (mem.eql(u8, v, "VOR")) { 36 | return .vor; 37 | } else if (mem.eql(u8, v, "INT")) { 38 | return .int; 39 | } else if (mem.eql(u8, v, "INT-VRP")) { 40 | return .int_vrp; 41 | } 42 | @panic("invalid waypoint type"); 43 | } 44 | 45 | pub fn toString(self: Type) [:0]const u8 { 46 | return switch (self) { 47 | .user_waypoint => "USER WAYPOINT", 48 | .airport => "AIRPORT", 49 | .ndb => "NDB", 50 | .vor => "VOR", 51 | .int => "INT", 52 | .int_vrp => "INT-VRP", 53 | }; 54 | } 55 | }; 56 | 57 | pub fn deinit(self: *Self, alloc: Allocator) void { 58 | alloc.free(self.identifier); 59 | self.* = undefined; 60 | } 61 | -------------------------------------------------------------------------------- /src/binding.zig: -------------------------------------------------------------------------------- 1 | // This file contains the C bindings that are exported when building 2 | // the system libraries. 3 | // 4 | // WHERE IS THE DOCUMENTATION? Note that all the documentation for the C 5 | // interface is in the header file flightplan.h. The implementation for 6 | // these various functions may have some comments but are meant towards 7 | // maintainers. 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const c_allocator = std.heap.c_allocator; 12 | 13 | const lib = @import("main.zig"); 14 | const Error = lib.Error; 15 | const FlightPlan = lib.FlightPlan; 16 | const Waypoint = lib.Waypoint; 17 | const Route = lib.Route; 18 | const testutil = @import("test.zig"); 19 | 20 | const c = @cImport({ 21 | @cInclude("flightplan.h"); 22 | }); 23 | 24 | //------------------------------------------------------------------- 25 | // Formats 26 | 27 | pub usingnamespace @import("format/garmin.zig").Binding; 28 | pub usingnamespace @import("format/xplane_fms_11.zig").Binding; 29 | 30 | //------------------------------------------------------------------- 31 | // General functions 32 | 33 | export fn fpl_cleanup() void { 34 | lib.deinit(); 35 | } 36 | 37 | export fn fpl_new() ?*FlightPlan { 38 | return cflightplan(.{ .alloc = c_allocator }); 39 | } 40 | 41 | export fn fpl_set_created(raw: ?*FlightPlan, str: [*:0]const u8) u8 { 42 | const fpl = raw orelse return 1; 43 | const copy = std.mem.span(str); 44 | fpl.created = Allocator.dupeZ(c_allocator, u8, copy) catch return 1; 45 | return 0; 46 | } 47 | 48 | export fn fpl_created(raw: ?*FlightPlan) ?[*:0]const u8 { 49 | if (raw) |fpl| { 50 | if (fpl.created) |v| { 51 | return v.ptr; 52 | } 53 | } 54 | 55 | return null; 56 | } 57 | 58 | export fn fpl_free(raw: ?*FlightPlan) void { 59 | if (raw) |v| { 60 | v.deinit(); 61 | c_allocator.destroy(v); 62 | } 63 | } 64 | 65 | pub fn cflightplan(fpl: FlightPlan) ?*FlightPlan { 66 | const result = c_allocator.create(FlightPlan) catch return null; 67 | result.* = fpl; 68 | return result; 69 | } 70 | 71 | //------------------------------------------------------------------- 72 | // Errors 73 | 74 | export fn fpl_last_error() ?*Error { 75 | return Error.lastError(); 76 | } 77 | 78 | export fn fpl_error_message(raw: ?*Error) ?[*:0]const u8 { 79 | const err = raw orelse return null; 80 | return err.message().ptr; 81 | } 82 | 83 | //------------------------------------------------------------------- 84 | // Waypoints 85 | 86 | const WPIterator = std.meta.fieldInfo(FlightPlan, .waypoints).field_type.ValueIterator; 87 | 88 | export fn fpl_waypoints_count(raw: ?*FlightPlan) c_int { 89 | if (raw) |fpl| { 90 | return @intCast(c_int, fpl.waypoints.count()); 91 | } 92 | 93 | return 0; 94 | } 95 | 96 | export fn fpl_waypoints_iter(raw: ?*FlightPlan) ?*WPIterator { 97 | const fpl = raw orelse return null; 98 | const iter = fpl.waypoints.valueIterator(); 99 | 100 | const result = c_allocator.create(@TypeOf(iter)) catch return null; 101 | result.* = iter; 102 | return result; 103 | } 104 | 105 | export fn fpl_waypoint_iter_free(raw: ?*WPIterator) void { 106 | if (raw) |iter| { 107 | c_allocator.destroy(iter); 108 | } 109 | } 110 | 111 | export fn fpl_waypoints_next(raw: ?*WPIterator) ?*Waypoint { 112 | const iter = raw orelse return null; 113 | return iter.next(); 114 | } 115 | 116 | export fn fpl_waypoint_identifier(raw: ?*Waypoint) ?[*:0]const u8 { 117 | const wp = raw orelse return null; 118 | return wp.identifier.ptr; 119 | } 120 | 121 | export fn fpl_waypoint_lat(raw: ?*Waypoint) f32 { 122 | const wp = raw orelse return -1; 123 | return wp.lat; 124 | } 125 | 126 | export fn fpl_waypoint_lon(raw: ?*Waypoint) f32 { 127 | const wp = raw orelse return -1; 128 | return wp.lon; 129 | } 130 | 131 | export fn fpl_waypoint_type(raw: ?*Waypoint) c.flightplan_waypoint_type { 132 | const wp = raw orelse return c.FLIGHTPLAN_INVALID; 133 | return @enumToInt(wp.type) + 1; // must add 1 due to _INVALID 134 | } 135 | 136 | export fn fpl_waypoint_type_str(raw: c.flightplan_waypoint_type) [*:0]const u8 { 137 | // subtraction here due to _INVALID 138 | return @intToEnum(Waypoint.Type, raw - 1).toString().ptr; 139 | } 140 | 141 | //------------------------------------------------------------------- 142 | // Route 143 | 144 | export fn fpl_route_name(raw: ?*FlightPlan) ?[*:0]const u8 { 145 | const fpl = raw orelse return null; 146 | if (fpl.route.name) |v| { 147 | return v.ptr; 148 | } 149 | 150 | return null; 151 | } 152 | 153 | export fn fpl_route_points_count(raw: ?*FlightPlan) c_int { 154 | const fpl = raw orelse return 0; 155 | return @intCast(c_int, fpl.route.points.items.len); 156 | } 157 | 158 | export fn fpl_route_points_get(raw: ?*FlightPlan, idx: c_int) ?*Route.Point { 159 | const fpl = raw orelse return null; 160 | return &fpl.route.points.items[@intCast(usize, idx)]; 161 | } 162 | 163 | export fn fpl_route_point_identifier(raw: ?*Route.Point) ?[*:0]const u8 { 164 | const ptr = raw orelse return null; 165 | return ptr.identifier; 166 | } 167 | -------------------------------------------------------------------------------- /src/format.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fs = std.fs; 3 | const mem = std.mem; 4 | const testing = std.testing; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const FlightPlan = @import("FlightPlan.zig"); 8 | 9 | /// Format returns a typed format for the given underlying implementation. 10 | /// 11 | /// Users do NOT need to use this type; most formats have direct reader/writer 12 | /// functions you can use directly. This generic type is here just to be useful 13 | /// as a way to guide format implementors to a common format and to add higher 14 | /// level operations in the future. 15 | /// 16 | /// Implementations must support the following fields: 17 | /// 18 | /// * Binding: type - C bindings to expose 19 | /// * Reader: type - Reader implementation for reading flight plans. 20 | /// * Writer: type - Writer implementatino for encoding flight plans. 21 | /// 22 | /// TODO: more docs 23 | pub fn Format( 24 | comptime Impl: type, 25 | ) type { 26 | return struct { 27 | /// Initialize a flight plan from a file path. 28 | pub fn initFromFile(alloc: Allocator, path: [:0]const u8) !FlightPlan { 29 | return Impl.Reader.initFromFile(alloc, path); 30 | } 31 | 32 | /// Write the flightplan to the given writer. writer is expected 33 | /// to implement std.io.writer. 34 | pub fn writeTo(writer: anytype, fpl: *const FlightPlan) !void { 35 | return Impl.Writer.writeTo(writer, fpl); 36 | } 37 | 38 | /// Write the flightplan to the given filepath. 39 | pub fn writeToFile(path: [:0]const u8, fpl: *const FlightPlan) !void { 40 | // Create our file 41 | const flags = fs.File.CreateFlags{ .truncate = true }; 42 | const file = if (fs.path.isAbsolute(path)) 43 | try fs.createFileAbsolute(path, flags) 44 | else 45 | try fs.cwd().createFile(path, flags); 46 | defer file.close(); 47 | 48 | // Write as a writer 49 | try writeTo(file.writer(), fpl); 50 | } 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/format/garmin.zig: -------------------------------------------------------------------------------- 1 | /// This file contains the reading/writing logic for the Garmin FPL 2 | /// format. This format is also used by ForeFlight with slight modifications. 3 | /// The reader/writer handle both formats. 4 | /// 5 | /// The FPL format does not support departure/arrival procedures. The 6 | /// data it uses is: 7 | /// 8 | /// * Waypoints 9 | /// * Route: only the identifier for each point 10 | /// 11 | /// Reference: https://www8.garmin.com/xmlschemas/FlightPlanv1.xsd 12 | const Garmin = @This(); 13 | 14 | const std = @import("std"); 15 | const mem = std.mem; 16 | const testing = std.testing; 17 | const Allocator = std.mem.Allocator; 18 | 19 | const FlightPlan = @import("../FlightPlan.zig"); 20 | const Waypoint = @import("../Waypoint.zig"); 21 | const format = @import("../format.zig"); 22 | const testutil = @import("../test.zig"); 23 | const xml = @import("../xml.zig"); 24 | const c = xml.c; 25 | const Error = @import("../Error.zig"); 26 | const ErrorSet = Error.Set; 27 | const Route = @import("../Route.zig"); 28 | 29 | test { 30 | _ = Binding; 31 | _ = Reader; 32 | _ = Writer; 33 | } 34 | 35 | /// The Format type that can be used with the generic functions on FlightPlan. 36 | /// You can also call the direct functions in this file. 37 | pub const Format = format.Format(Garmin); 38 | 39 | /// Initialize a flightplan from a file. 40 | pub fn initFromFile(alloc: Allocator, path: [:0]const u8) !FlightPlan { 41 | return Reader.initFromFile(alloc, path); 42 | } 43 | 44 | /// Encode a flightplan to this format to the given writer. writer should 45 | /// be a std.io.Writer-like implementation. 46 | pub fn writeTo(writer: anytype, fpl: *const FlightPlan) !void { 47 | return Writer.writeTo(writer, fpl); 48 | } 49 | 50 | /// Binding are the C bindings for this format. 51 | pub const Binding = struct { 52 | const binding = @import("../binding.zig"); 53 | const c_allocator = std.heap.c_allocator; 54 | 55 | export fn fpl_garmin_parse_file(path: [*:0]const u8) ?*FlightPlan { 56 | var fpl = Reader.initFromFile(c_allocator, mem.sliceTo(path, 0)) catch return null; 57 | return binding.cflightplan(fpl); 58 | } 59 | 60 | export fn fpl_garmin_write_to_file(raw: ?*FlightPlan, path: [*:0]const u8) c_int { 61 | const fpl = raw orelse return -1; 62 | Format.writeToFile(mem.sliceTo(path, 0), fpl) catch return -1; 63 | return 0; 64 | } 65 | }; 66 | 67 | /// Reader implementation (see format.zig) 68 | pub const Reader = struct { 69 | pub fn initFromFile(alloc: Allocator, path: [:0]const u8) !FlightPlan { 70 | // Create a parser context. We use the context form rather than the global 71 | // xmlReadFile form so that we can be a little more thread safe. 72 | const ctx = c.xmlNewParserCtxt(); 73 | if (ctx == null) { 74 | Error.setLastError(null); 75 | return ErrorSet.ReadFailed; 76 | } 77 | // NOTE: we do not defer freeing the context cause we want to preserve 78 | // the context if there are any errors. 79 | 80 | // Read the file 81 | const doc = c.xmlCtxtReadFile(ctx, path.ptr, null, 0); 82 | if (doc == null) { 83 | return Error.setLastErrorXML(ErrorSet.ReadFailed, .{ .parser = ctx }); 84 | } 85 | defer c.xmlFreeParserCtxt(ctx); 86 | defer c.xmlFreeDoc(doc); 87 | 88 | // Get the root elem 89 | const root = c.xmlDocGetRootElement(doc); 90 | return initFromXMLNode(alloc, root); 91 | } 92 | 93 | pub fn initFromReader(alloc: Allocator, reader: anytype) !FlightPlan { 94 | // Read the full contents. 95 | var buf = try reader.readAllAlloc( 96 | alloc, 97 | 1024 * 1024 * 50, // 50 MB for now 98 | ); 99 | defer alloc.free(buf); 100 | 101 | const ctx = c.xmlNewParserCtxt(); 102 | if (ctx == null) { 103 | Error.setLastError(null); 104 | return ErrorSet.ReadFailed; 105 | } 106 | // NOTE: we do not defer freeing the context cause we want to preserve 107 | // the context if there are any errors. 108 | 109 | // Read the 110 | const doc = c.xmlCtxtReadMemory( 111 | ctx, 112 | buf.ptr, 113 | @intCast(c_int, buf.len), 114 | null, 115 | null, 116 | 0, 117 | ); 118 | if (doc == null) { 119 | return Error.setLastErrorXML(ErrorSet.ReadFailed, .{ .parser = ctx }); 120 | } 121 | defer c.xmlFreeParserCtxt(ctx); 122 | defer c.xmlFreeDoc(doc); 123 | 124 | // Get the root elem 125 | const root = c.xmlDocGetRootElement(doc); 126 | return initFromXMLNode(alloc, root); 127 | } 128 | 129 | fn initFromXMLNode(alloc: Allocator, node: *c.xmlNode) !FlightPlan { 130 | // Should be an opening node 131 | if (node.type != c.XML_ELEMENT_NODE) { 132 | return ErrorSet.NodeExpected; 133 | } 134 | 135 | // Should be a "flight-plan" node. 136 | if (c.xmlStrcmp(node.name, "flight-plan") != 0) { 137 | Error.setLastError(try Error.initMessage( 138 | alloc, 139 | ErrorSet.InvalidElement, 140 | "flight-plan element not found", 141 | .{}, 142 | )); 143 | 144 | return ErrorSet.InvalidElement; 145 | } 146 | 147 | const WPType = comptime std.meta.fieldInfo(FlightPlan, .waypoints).field_type; 148 | var self = FlightPlan{ 149 | .alloc = alloc, 150 | .created = undefined, 151 | .waypoints = WPType{}, 152 | .route = undefined, 153 | }; 154 | 155 | try parseFlightPlan(&self, node); 156 | return self; 157 | } 158 | 159 | fn parseFlightPlan(self: *FlightPlan, node: *c.xmlNode) !void { 160 | var cur: ?*c.xmlNode = node.children; 161 | while (cur) |n| : (cur = n.next) { 162 | if (n.type != c.XML_ELEMENT_NODE) { 163 | continue; 164 | } 165 | 166 | if (c.xmlStrcmp(n.name, "created") == 0) { 167 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 168 | defer xml.free(copy); 169 | self.created = try Allocator.dupeZ(self.alloc, u8, mem.sliceTo(copy, 0)); 170 | } else if (c.xmlStrcmp(n.name, "waypoint-table") == 0) { 171 | try parseWaypointTable(self, n); 172 | } else if (c.xmlStrcmp(n.name, "route") == 0) { 173 | self.route = try parseRoute(self.alloc, n); 174 | } 175 | } 176 | } 177 | 178 | fn parseWaypointTable(self: *FlightPlan, node: *c.xmlNode) !void { 179 | var cur: ?*c.xmlNode = node.children; 180 | while (cur) |n| : (cur = n.next) { 181 | if (n.type != c.XML_ELEMENT_NODE) { 182 | continue; 183 | } 184 | 185 | if (c.xmlStrcmp(n.name, "waypoint") == 0) { 186 | const wp = try parseWaypoint(self.alloc, n); 187 | try self.waypoints.put(self.alloc, wp.identifier, wp); 188 | } 189 | } 190 | } 191 | 192 | fn parseRoute(alloc: Allocator, node: *c.xmlNode) !Route { 193 | var self = Route{ 194 | .name = undefined, 195 | .points = .{}, 196 | }; 197 | 198 | var cur: ?*c.xmlNode = node.children; 199 | while (cur) |n| : (cur = n.next) { 200 | if (n.type != c.XML_ELEMENT_NODE) { 201 | continue; 202 | } 203 | 204 | if (c.xmlStrcmp(n.name, "route-name") == 0) { 205 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 206 | defer xml.free(copy); 207 | self.name = try Allocator.dupeZ(alloc, u8, mem.sliceTo(copy, 0)); 208 | } else if (c.xmlStrcmp(n.name, "route-point") == 0) { 209 | try parseRoutePoint(&self, alloc, n); 210 | } 211 | } 212 | 213 | return self; 214 | } 215 | 216 | fn parseRoutePoint(self: *Route, alloc: Allocator, node: *c.xmlNode) !void { 217 | var cur: ?*c.xmlNode = node.children; 218 | while (cur) |n| : (cur = n.next) { 219 | if (n.type != c.XML_ELEMENT_NODE) { 220 | continue; 221 | } 222 | 223 | if (c.xmlStrcmp(n.name, "waypoint-identifier") == 0) { 224 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 225 | defer xml.free(copy); 226 | const zcopy = try Allocator.dupeZ(alloc, u8, mem.sliceTo(copy, 0)); 227 | try self.points.append(alloc, Route.Point{ 228 | .identifier = zcopy, 229 | }); 230 | } 231 | } 232 | } 233 | 234 | fn parseWaypoint(alloc: Allocator, node: *c.xmlNode) !Waypoint { 235 | var self = Waypoint{ 236 | .identifier = undefined, 237 | .type = undefined, 238 | }; 239 | 240 | var cur: ?*c.xmlNode = node.children; 241 | while (cur) |n| : (cur = n.next) { 242 | if (n.type != c.XML_ELEMENT_NODE) { 243 | continue; 244 | } 245 | 246 | if (c.xmlStrcmp(n.name, "identifier") == 0) { 247 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 248 | defer xml.free(copy); 249 | self.identifier = try Allocator.dupeZ(alloc, u8, mem.sliceTo(copy, 0)); 250 | } else if (c.xmlStrcmp(n.name, "lat") == 0) { 251 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 252 | defer xml.free(copy); 253 | self.lat = try std.fmt.parseFloat(f32, mem.sliceTo(copy, 0)); 254 | } else if (c.xmlStrcmp(n.name, "lon") == 0) { 255 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 256 | defer xml.free(copy); 257 | self.lon = try std.fmt.parseFloat(f32, mem.sliceTo(copy, 0)); 258 | } else if (c.xmlStrcmp(n.name, "type") == 0) { 259 | const copy = c.xmlNodeListGetString(node.doc, n.children, 1); 260 | defer xml.free(copy); 261 | self.type = Waypoint.Type.fromString(mem.sliceTo(copy, 0)); 262 | } 263 | } 264 | 265 | return self; 266 | } 267 | 268 | test "basic reading" { 269 | const testPath = try testutil.testFile("basic.fpl"); 270 | var plan = try Format.initFromFile(testing.allocator, testPath); 271 | defer plan.deinit(); 272 | 273 | try testing.expectEqualStrings(plan.created.?, "20211230T22:07:20Z"); 274 | try testing.expectEqual(plan.waypoints.count(), 20); 275 | 276 | // Test route 277 | try testing.expectEqualStrings(plan.route.name.?, "KHHR TO KHTH"); 278 | try testing.expectEqual(plan.route.points.items.len, 20); 279 | 280 | // Test a waypoint 281 | { 282 | const wp = plan.waypoints.get("KHHR").?; 283 | try testing.expectEqualStrings(wp.identifier, "KHHR"); 284 | try testing.expect(wp.lat > 33.91 and wp.lat < 33.93); 285 | try testing.expect(wp.lon > -118.336 and wp.lon < -118.334); 286 | try testing.expectEqual(wp.type, .airport); 287 | try testing.expectEqualStrings(wp.type.toString(), "AIRPORT"); 288 | } 289 | } 290 | 291 | test "parse error" { 292 | const testPath = try testutil.testFile("error_syntax.fpl"); 293 | try testing.expectError(ErrorSet.ReadFailed, Format.initFromFile(testing.allocator, testPath)); 294 | 295 | var lastErr = Error.lastError().?; 296 | defer Error.setLastError(null); 297 | try testing.expectEqual(lastErr.code, ErrorSet.ReadFailed); 298 | 299 | const xmlErr = lastErr.detail.?.xml.err(); 300 | const message = mem.span(xmlErr.?.message); 301 | try testing.expect(message.len > 0); 302 | } 303 | 304 | test "error: no flight-plan" { 305 | const testPath = try testutil.testFile("error_no_flightplan.fpl"); 306 | try testing.expectError(ErrorSet.InvalidElement, Format.initFromFile(testing.allocator, testPath)); 307 | 308 | var lastErr = Error.lastError().?; 309 | defer Error.setLastError(null); 310 | try testing.expectEqual(lastErr.code, ErrorSet.InvalidElement); 311 | } 312 | }; 313 | 314 | /// Writer implementation (see format.zig) 315 | pub const Writer = struct { 316 | pub fn writeTo(writer: anytype, fpl: *const FlightPlan) !void { 317 | // Initialize an in-memory buffer. We have to do all writes to a buffer 318 | // first. We know that our flight plans can't be _that_ big (for a 319 | // reasonable user) so this is fine. 320 | var buf = c.xmlBufferCreate(); 321 | if (buf == null) { 322 | return Error.setLastErrorXML(ErrorSet.OutOfMemory, .{ .global = {} }); 323 | } 324 | defer c.xmlBufferFree(buf); 325 | 326 | var xmlwriter = c.xmlNewTextWriterMemory(buf, 0); 327 | if (xmlwriter == null) { 328 | return Error.setLastErrorXML(ErrorSet.OutOfMemory, .{ .global = {} }); 329 | } 330 | 331 | // Make the output human-friendly 332 | var rc = c.xmlTextWriterSetIndent(xmlwriter, 1); 333 | if (rc < 0) { 334 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 335 | } 336 | rc = c.xmlTextWriterSetIndentString(xmlwriter, "\t"); 337 | if (rc < 0) { 338 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 339 | } 340 | 341 | rc = c.xmlTextWriterStartDocument(xmlwriter, "1.0", "utf-8", null); 342 | if (rc < 0) { 343 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 344 | } 345 | 346 | // Start 347 | const ns = "http://www8.garmin.com/xmlschemas/FlightPlan/v1"; 348 | rc = c.xmlTextWriterStartElementNS(xmlwriter, null, "flight-plan", ns); 349 | if (rc < 0) { 350 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 351 | } 352 | 353 | // 354 | if (fpl.created) |created| { 355 | rc = c.xmlTextWriterWriteElement(xmlwriter, "created", created); 356 | if (rc < 0) { 357 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 358 | } 359 | } 360 | 361 | // Encode our waypoints 362 | try writeWaypoints(xmlwriter, fpl); 363 | 364 | // Encode our route 365 | try writeRoute(xmlwriter, fpl); 366 | 367 | // End 368 | rc = c.xmlTextWriterEndElement(xmlwriter); 369 | if (rc < 0) { 370 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 371 | } 372 | 373 | // End doc 374 | rc = c.xmlTextWriterEndDocument(xmlwriter); 375 | if (rc < 0) { 376 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 377 | } 378 | 379 | // Free our text writer. We defer this now because errors below no longer 380 | // need this reference. 381 | defer c.xmlFreeTextWriter(xmlwriter); 382 | 383 | // Success, lets copy our buffer to the writer. 384 | try writer.writeAll(mem.span(buf.*.content)); 385 | } 386 | 387 | fn writeWaypoints(xmlwriter: c.xmlTextWriterPtr, fpl: *const FlightPlan) !void { 388 | // Do nothing if we have no waypoints 389 | if (fpl.waypoints.count() == 0) { 390 | return; 391 | } 392 | 393 | // Buffer for writing 394 | var buf: [128]u8 = undefined; 395 | 396 | // Start 397 | var rc = c.xmlTextWriterStartElement(xmlwriter, "waypoint-table"); 398 | if (rc < 0) { 399 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 400 | } 401 | 402 | // Iterate over each waypoint and write it 403 | var iter = fpl.waypoints.valueIterator(); 404 | while (iter.next()) |wp| { 405 | // Start 406 | rc = c.xmlTextWriterStartElement(xmlwriter, "waypoint"); 407 | if (rc < 0) { 408 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 409 | } 410 | 411 | rc = c.xmlTextWriterWriteElement(xmlwriter, "identifier", wp.identifier); 412 | if (rc < 0) { 413 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 414 | } 415 | 416 | rc = c.xmlTextWriterWriteElement(xmlwriter, "type", wp.type.toString()); 417 | if (rc < 0) { 418 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 419 | } 420 | 421 | rc = c.xmlTextWriterWriteElement( 422 | xmlwriter, 423 | "lat", 424 | try std.fmt.bufPrintZ(&buf, "{d}", .{wp.lat}), 425 | ); 426 | if (rc < 0) { 427 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 428 | } 429 | 430 | rc = c.xmlTextWriterWriteElement( 431 | xmlwriter, 432 | "lon", 433 | try std.fmt.bufPrintZ(&buf, "{d}", .{wp.lon}), 434 | ); 435 | if (rc < 0) { 436 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 437 | } 438 | 439 | // End 440 | rc = c.xmlTextWriterEndElement(xmlwriter); 441 | if (rc < 0) { 442 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 443 | } 444 | } 445 | 446 | // End 447 | rc = c.xmlTextWriterEndElement(xmlwriter); 448 | if (rc < 0) { 449 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 450 | } 451 | } 452 | 453 | fn writeRoute(xmlwriter: c.xmlTextWriterPtr, fpl: *const FlightPlan) !void { 454 | // Start 455 | var rc = c.xmlTextWriterStartElement(xmlwriter, "route"); 456 | if (rc < 0) { 457 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 458 | } 459 | 460 | if (fpl.route.name) |name| { 461 | rc = c.xmlTextWriterWriteElement(xmlwriter, "route-name", name); 462 | if (rc < 0) { 463 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 464 | } 465 | } 466 | 467 | for (fpl.route.points.items) |point| { 468 | // Find the waypoint for this point 469 | const wp = fpl.waypoints.get(point.identifier) orelse return ErrorSet.RouteMissingWaypoint; 470 | 471 | // Start 472 | rc = c.xmlTextWriterStartElement(xmlwriter, "route-point"); 473 | if (rc < 0) { 474 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 475 | } 476 | 477 | rc = c.xmlTextWriterWriteElement(xmlwriter, "waypoint-identifier", point.identifier); 478 | if (rc < 0) { 479 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 480 | } 481 | 482 | rc = c.xmlTextWriterWriteElement(xmlwriter, "waypoint-type", wp.type.toString()); 483 | if (rc < 0) { 484 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 485 | } 486 | 487 | // End 488 | rc = c.xmlTextWriterEndElement(xmlwriter); 489 | if (rc < 0) { 490 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 491 | } 492 | } 493 | 494 | // End 495 | rc = c.xmlTextWriterEndElement(xmlwriter); 496 | if (rc < 0) { 497 | return Error.setLastErrorXML(ErrorSet.WriteFailed, .{ .writer = xmlwriter }); 498 | } 499 | } 500 | 501 | test "basic writing" { 502 | const testPath = try testutil.testFile("basic.fpl"); 503 | var plan = try Format.initFromFile(testing.allocator, testPath); 504 | defer plan.deinit(); 505 | 506 | // Write the plan and compare 507 | var output = std.ArrayList(u8).init(testing.allocator); 508 | defer output.deinit(); 509 | 510 | // Write 511 | try Writer.writeTo(output.writer(), &plan); 512 | 513 | // Debug, write output to compare 514 | //std.debug.print("write:\n\n{s}\n", .{output.items}); 515 | 516 | // re-read to verify it parses 517 | const reader = std.io.fixedBufferStream(output.items).reader(); 518 | var plan2 = try Reader.initFromReader(testing.allocator, reader); 519 | defer plan2.deinit(); 520 | } 521 | }; 522 | -------------------------------------------------------------------------------- /src/format/xplane_fms_11.zig: -------------------------------------------------------------------------------- 1 | /// This file contains the format implementation for the X-Plane FMS v11 format 2 | /// used by X-Plane 11.10 and later. 3 | /// 4 | /// Reference: https://developer.x-plane.com/article/flightplan-files-v11-fms-file-format/ 5 | const FMS = @This(); 6 | 7 | const std = @import("std"); 8 | const mem = std.mem; 9 | const testing = std.testing; 10 | const Allocator = std.mem.Allocator; 11 | 12 | const format = @import("../format.zig"); 13 | const testutil = @import("../test.zig"); 14 | const time = @import("../time.zig"); 15 | const FlightPlan = @import("../FlightPlan.zig"); 16 | const Route = @import("../Route.zig"); 17 | const Waypoint = @import("../Waypoint.zig"); 18 | const Error = @import("../Error.zig"); 19 | const ErrorSet = Error.Set; 20 | 21 | test { 22 | _ = Binding; 23 | _ = Reader; 24 | _ = Writer; 25 | } 26 | 27 | /// The Format type that can be used with the generic functions on FlightPlan. 28 | /// You can also call the direct functions in this file. 29 | pub const Format = format.Format(FMS); 30 | 31 | /// Binding are the C bindings for this format. 32 | pub const Binding = struct { 33 | const binding = @import("../binding.zig"); 34 | const c_allocator = std.heap.c_allocator; 35 | 36 | export fn fpl_xplane11_write_to_file(raw: ?*FlightPlan, path: [*:0]const u8) c_int { 37 | const fpl = raw orelse return -1; 38 | Format.writeToFile(mem.sliceTo(path, 0), fpl) catch return -1; 39 | return 0; 40 | } 41 | }; 42 | 43 | /// Reader implementation (see format.zig) 44 | /// TODO 45 | pub const Reader = struct { 46 | pub fn initFromFile(alloc: Allocator, path: [:0]const u8) !FlightPlan { 47 | _ = alloc; 48 | _ = path; 49 | return ErrorSet.Unimplemented; 50 | } 51 | }; 52 | 53 | /// Writer implementation (see format.zig) 54 | pub const Writer = struct { 55 | pub fn writeTo(writer: anytype, fpl: *const FlightPlan) !void { 56 | // Buffer that might be used for string operations. 57 | // Ensure this is always big enough. 58 | var buf: [8]u8 = undefined; 59 | 60 | // Header 61 | try writer.writeAll("I\n"); 62 | try writer.writeAll("1100 Version\n"); 63 | 64 | // Determine our AIRAC cycle. We try to use the airac cycle 65 | // on the flight plan. If that's not set, we just make 66 | // one up based on the current year. Waypoints don't change 67 | // often and flightplan validation will find this error so 68 | // if the user got here they are okay with defaults. 69 | if (fpl.airac) |v| { 70 | try writer.print("CYCLE {s}\n", .{v}); 71 | } else { 72 | const t = time.c.time(null); 73 | const tm = time.c.localtime(&t).*; 74 | const v = try std.fmt.bufPrintZ(&buf, "{d}01", .{ 75 | // we want years since 2000 76 | tm.tm_year - 100, 77 | }); 78 | 79 | try writer.print("CYCLE {s}\n", .{v}); 80 | } 81 | 82 | // Departure 83 | if (fpl.departure) |dep| { 84 | // Departure airport. If we have departure info set then we use that. 85 | try writeDeparture(writer, fpl, dep.identifier); 86 | 87 | // Write additional departure info 88 | try writeDepartureProc(writer, fpl); 89 | } else if (fpl.route.points.items.len > 0) { 90 | // No departure info set, we just try to use the first route. 91 | const point = &fpl.route.points.items[0]; 92 | try writeDeparture(writer, fpl, point.identifier); 93 | } else { 94 | // No route 95 | return ErrorSet.RequiredValueMissing; 96 | } 97 | 98 | // Destination 99 | if (fpl.destination) |des| { 100 | // Departure airport. If we have departure info set then we use that. 101 | try writeDestination(writer, fpl, des.identifier); 102 | 103 | // Write additional departure info 104 | try writeDestinationProc(writer, fpl); 105 | } else if (fpl.route.points.items.len > 0) { 106 | // No departure info set, we just try to use the first route. 107 | const point = &fpl.route.points.items[fpl.route.points.items.len - 1]; 108 | try writeDestination(writer, fpl, point.identifier); 109 | } else { 110 | // No route 111 | return ErrorSet.RequiredValueMissing; 112 | } 113 | 114 | // Route 115 | try writeRoute(writer, fpl); 116 | } 117 | 118 | fn writeDeparture(writer: anytype, fpl: *const FlightPlan, id: []const u8) !void { 119 | // Get the waypoint associated with the departure ID so we can 120 | // determine the type. 121 | const wp = fpl.waypoints.get(id) orelse 122 | return ErrorSet.RouteMissingWaypoint; 123 | 124 | // Prefix we use depends if departure is an airport or not. 125 | const prefix = switch (wp.type) { 126 | .airport => "ADEP", 127 | else => "DEP", 128 | }; 129 | 130 | try writer.print("{s} {s}\n", .{ prefix, wp.identifier }); 131 | } 132 | 133 | fn writeDepartureProc(writer: anytype, fpl: *const FlightPlan) !void { 134 | var buf: [8]u8 = undefined; 135 | const dep = fpl.departure.?; 136 | 137 | if (dep.runway) |rwy| try writer.print("DEPRWY RW{s}\n", .{ 138 | try rwy.toString(&buf), 139 | }); 140 | 141 | if (dep.sid) |v| { 142 | try writer.print("SID {s}\n", .{v}); 143 | if (dep.transition) |transition| 144 | try writer.print("SIDTRANS {s}\n", .{transition}); 145 | } 146 | } 147 | 148 | fn writeDestination(writer: anytype, fpl: *const FlightPlan, id: []const u8) !void { 149 | // Get the waypoint associated with the ID so we can determine the type. 150 | const wp = fpl.waypoints.get(id) orelse 151 | return ErrorSet.RouteMissingWaypoint; 152 | 153 | // Prefix we use depends if departure is an airport or not. 154 | const prefix = switch (wp.type) { 155 | .airport => "ADES", 156 | else => "DES", 157 | }; 158 | 159 | try writer.print("{s} {s}\n", .{ prefix, wp.identifier }); 160 | } 161 | 162 | fn writeDestinationProc(writer: anytype, fpl: *const FlightPlan) !void { 163 | var buf: [8]u8 = undefined; 164 | const des = fpl.destination.?; 165 | 166 | if (des.runway) |rwy| try writer.print("DESRWY RW{s}\n", .{ 167 | try rwy.toString(&buf), 168 | }); 169 | 170 | if (des.star) |v| { 171 | try writer.print("STAR {s}\n", .{v}); 172 | if (des.star_transition) |transition| 173 | try writer.print("STARTRANS {s}\n", .{transition}); 174 | } 175 | 176 | if (des.approach) |v| { 177 | try writer.print("APP {s}\n", .{v}); 178 | if (des.approach_transition) |transition| 179 | try writer.print("APPTRANS {s}\n", .{transition}); 180 | } 181 | } 182 | 183 | fn writeRoute(writer: anytype, fpl: *const FlightPlan) !void { 184 | try writer.print("NUMENR {d}\n", .{fpl.route.points.items.len}); 185 | 186 | for (fpl.route.points.items) |point, i| { 187 | const wp = fpl.waypoints.get(point.identifier) orelse return ErrorSet.RouteMissingWaypoint; 188 | 189 | const typeCode: u8 = switch (wp.type) { 190 | .airport => 1, 191 | .ndb => 2, 192 | .vor => 3, 193 | .int => 11, 194 | .int_vrp => 11, 195 | .user_waypoint => 28, 196 | }; 197 | 198 | // Get our "via" value for XPlane. If this isn't set, we try to 199 | // determine it based on what kind of route point this is. 200 | const via = point.via orelse blk: { 201 | if (i == 0 and wp.type == .airport) { 202 | // First route, airport => departure airport 203 | break :blk Route.Point.Via{ .airport_departure = {} }; 204 | } else if (i == fpl.route.points.items.len - 1 and wp.type == .airport) { 205 | // Last route, airport => destination airport 206 | break :blk Route.Point.Via{ .airport_destination = {} }; 207 | } else { 208 | // Anything else, we go direct 209 | break :blk Route.Point.Via{ .direct = {} }; 210 | } 211 | }; 212 | 213 | // Convert the Via tagged union to the string value xplane expects 214 | const viaString = switch (via) { 215 | .airport_departure => "ADEP", 216 | .airport_destination => "ADES", 217 | .direct => "DRCT", 218 | .airway => |v| v, 219 | }; 220 | 221 | try writer.print("{d} {s} {s} {d} {d} {d}\n", .{ 222 | typeCode, 223 | wp.identifier, 224 | viaString, 225 | point.altitude, 226 | wp.lat, 227 | wp.lon, 228 | }); 229 | } 230 | } 231 | 232 | test "read Garmin FPL, write X-Plane" { 233 | const Garmin = @import("garmin.zig"); 234 | 235 | const testPath = try testutil.testFile("basic.fpl"); 236 | var plan = try Garmin.Format.initFromFile(testing.allocator, testPath); 237 | defer plan.deinit(); 238 | 239 | // Write the plan and compare 240 | var output = std.ArrayList(u8).init(testing.allocator); 241 | defer output.deinit(); 242 | 243 | // Write 244 | try Writer.writeTo(output.writer(), &plan); 245 | 246 | // Debug, write output to compare 247 | // std.debug.print("write:\n\n{s}\n", .{output.items}); 248 | 249 | // TODO: re-read to verify it parses 250 | } 251 | }; 252 | -------------------------------------------------------------------------------- /src/include/bridge.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Zig can't "call" the macro properly so this is a brige function we can 4 | // call in order to initialize libxml. 5 | void _zig_LIBXML_TEST_VERSION() { 6 | LIBXML_TEST_VERSION 7 | } 8 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const FlightPlan = @import("FlightPlan.zig"); 4 | pub const Waypoint = @import("Waypoint.zig"); 5 | pub const Route = @import("Route.zig"); 6 | pub const Departure = @import("Departure.zig"); 7 | pub const Destination = @import("Destination.zig"); 8 | pub const Runway = @import("Runway.zig"); 9 | pub const Error = @import("Error.zig"); 10 | pub const Format = struct { 11 | pub const Garmin = @import("format/garmin.zig"); 12 | pub const XPlaneFMS11 = @import("format/xplane_fms_11.zig"); 13 | }; 14 | 15 | /// deinit should be called when the process is done with this library 16 | /// to perform process-level cleanup. This frees memory associated with 17 | /// some global error values. 18 | pub fn deinit() void { 19 | Error.setLastError(null); 20 | } 21 | 22 | test { 23 | _ = Error; 24 | _ = Departure; 25 | _ = Destination; 26 | _ = FlightPlan; 27 | _ = Route; 28 | _ = Runway; 29 | _ = Format.Garmin; 30 | _ = Format.XPlaneFMS11; 31 | } 32 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | /// Get the path to a test fixture file. This should only be used in tests 4 | /// since it depends on a predictable source path. This returns a null-terminated 5 | /// string slice so that it can be used directly with C APIs (libxml), but 6 | /// the cost is then it must be freed. 7 | pub fn testFile(comptime path: []const u8) ![:0]const u8 { 8 | comptime { 9 | const sepSlice = &[_]u8{std.fs.path.sep}; 10 | 11 | // Build our path which has our relative test directory. 12 | var path2 = "/../test/" ++ path; 13 | 14 | // The path is expected to always use / so we replace it if its 15 | // a different value. If sep is / we technically don't have to do this 16 | // but we always do just so we can ensure this code path works 17 | var buf: [path2.len]u8 = undefined; 18 | _ = std.mem.replace( 19 | u8, 20 | path2, 21 | &[_]u8{'/'}, 22 | sepSlice, 23 | buf[0..], 24 | ); 25 | const finalPath = buf[0..]; 26 | 27 | // Get the directory of this source file. 28 | const srcDir = std.fs.path.dirname(@src().file) orelse unreachable; 29 | 30 | // Add our path 31 | const final: []const u8 = srcDir ++ finalPath ++ &[_]u8{0}; 32 | return final[0 .. final.len - 1 :0]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/time.zig: -------------------------------------------------------------------------------- 1 | // This file exports a singular source for time.h from libc. 2 | 3 | pub const c = @cImport({ 4 | @cInclude("time.h"); 5 | }); 6 | -------------------------------------------------------------------------------- /src/xml.zig: -------------------------------------------------------------------------------- 1 | pub const c = @cImport({ 2 | @cDefine("LIBXML_WRITER_ENABLED", {}); 3 | @cInclude("libxml/xmlreader.h"); 4 | @cInclude("libxml/xmlwriter.h"); 5 | }); 6 | 7 | // free calls xmlFree 8 | pub fn free(ptr: ?*anyopaque) void { 9 | if (ptr) |v| { 10 | c.xmlFree.?(v); 11 | } 12 | } 13 | 14 | /// Find a node that has the given element type and return it. This looks 15 | /// in sibling nodes. 16 | pub fn findNode(node: ?*c.xmlNode, name: []const u8) ?*c.xmlNode { 17 | var cur = node; 18 | while (cur) |n| : (cur = n.next) { 19 | if (n.type != c.XML_ELEMENT_NODE) { 20 | continue; 21 | } 22 | 23 | if (c.xmlStrcmp(n.name, name.ptr) == 0) { 24 | return n; 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | -------------------------------------------------------------------------------- /test/basic.fpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20211230T22:07:20Z 4 | 5 | N123CD 6 | 7 | 8 | 20220103T17:00:00Z 9 | 11000 10 | 11 | 12 | 13 | 14 | KHHR 15 | AIRPORT 16 | 33.92286102713828 17 | -118.3350830946681 18 | 19 | 20 | 21 | 22 | AHEIM 23 | INT 24 | 33.82031388888889 25 | -117.9119472222222 26 | 27 | 28 | 29 | 30 | POWUP 31 | INT 32 | 33.94479166666667 33 | -117.851825 34 | 35 | 36 | 37 | 38 | DOWDD 39 | INT 40 | 33.98541388888889 41 | -117.8321666666667 42 | 43 | 44 | 45 | 46 | ADAMM 47 | INT 48 | 34.01704722222222 49 | -117.8168166666667 50 | 51 | 52 | 53 | 54 | POM 55 | VOR 56 | 34.0783888888889 57 | -117.787075 58 | 59 | 60 | 61 | 62 | CALBE 63 | INT 64 | 34.12199722222222 65 | -117.7284111111111 66 | 67 | 68 | 69 | 70 | MEANT 71 | INT 72 | 34.19028888888889 73 | -117.6363472222222 74 | 75 | 76 | 77 | 78 | GARDY 79 | INT 80 | 34.25594166666666 81 | -117.5476222222222 82 | 83 | 84 | 85 | 86 | HESPE 87 | INT 88 | 34.34116666666667 89 | -117.4320305555556 90 | 91 | 92 | 93 | 94 | APLES 95 | INT 96 | 34.54846666666667 97 | -117.1494833333333 98 | 99 | 100 | 101 | 102 | BASAL 103 | INT 104 | 34.757775 105 | -116.8618305555556 106 | 107 | 108 | 109 | 110 | DAG 111 | VOR 112 | 34.9624583333333 113 | -116.578166666667 114 | 115 | 116 | 117 | 118 | DISBE 119 | INT 120 | 35.12005555555555 121 | -116.3817277777778 122 | 123 | 124 | 125 | 126 | CHRLT 127 | INT 128 | 35.14893055555556 129 | -116.3456111111111 130 | 131 | 132 | 133 | 134 | CLARR 135 | INT 136 | 35.67568055555556 137 | -115.6797527777778 138 | 139 | 140 | 141 | 142 | HIDEN 143 | INT 144 | 36.02836944444444 145 | -116.0101666666667 146 | 147 | 148 | 149 | 150 | BTY 151 | VOR 152 | 36.8005972222222 153 | -116.747641666667 154 | 155 | 156 | 157 | 158 | LIDAT 159 | INT 160 | 37.43018611111111 161 | -117.2780555555556 162 | 163 | 164 | 165 | 166 | KHTH 167 | AIRPORT 168 | 38.54507703083606 169 | -118.6324030949059 170 | 171 | 172 | 173 | 174 | 175 | KHHR TO KHTH 176 | 1 177 | 178 | 179 | KHHR 180 | AIRPORT 181 | 182 | 183 | 184 | AHEIM 185 | INT 186 | 187 | 188 | 189 | POWUP 190 | INT 191 | 192 | 193 | 194 | DOWDD 195 | INT 196 | 197 | 198 | 199 | ADAMM 200 | INT 201 | 202 | 203 | 204 | POM 205 | VOR 206 | 207 | 208 | 209 | CALBE 210 | INT 211 | 212 | 213 | 214 | MEANT 215 | INT 216 | 217 | 218 | 219 | GARDY 220 | INT 221 | 222 | 223 | 224 | HESPE 225 | INT 226 | 227 | 228 | 229 | APLES 230 | INT 231 | 232 | 233 | 234 | BASAL 235 | INT 236 | 237 | 238 | 239 | DAG 240 | VOR 241 | 242 | 243 | 244 | DISBE 245 | INT 246 | 247 | 248 | 249 | CHRLT 250 | INT 251 | 252 | 253 | 254 | CLARR 255 | INT 256 | 257 | 258 | 259 | HIDEN 260 | INT 261 | 262 | 263 | 264 | BTY 265 | VOR 266 | 267 | 268 | 269 | LIDAT 270 | INT 271 | 272 | 273 | 274 | KHTH 275 | AIRPORT 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /test/error_no_flightplan.fpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/error_syntax.fpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/xplane11.fms: -------------------------------------------------------------------------------- 1 | I 2 | 1100 Version 3 | CYCLE 2113 4 | ADEP KHHR 5 | DEPRWY RW25 6 | SID SPACX2 7 | SIDTRANS SPACX 8 | ADES KSBP 9 | DESRWY RW29 10 | APP R29 11 | APPTRANS FABEG 12 | NUMENR 6 13 | 1 KHHR ADEP 66.000000 33.922861 -118.335083 14 | 11 SPACX DRCT 0.000000 33.822339 -118.443939 15 | 3 LAX DRCT 0.000000 33.93315 -118.432014 16 | 3 VTU DRCT 0.000000 34.115064 -119.0495 17 | 3 RZS T257 0.000000 34.509533 -119.770992 18 | 1 KSBP ADES 212.000000 35.237278 -120.642611 19 | --------------------------------------------------------------------------------