diff --git a/README b/README new file mode 100644 index 0000000..915c83b --- /dev/null +++ b/README @@ -0,0 +1,67 @@ +FG SimBrief Importer Add-On +=========================== + +What Is This? +------------- + +This is an add-on for FlightGear that adds a SimBrief Import dialog. + +(See https://www.simbrief.com/). + +What It Does +------------ + +The SimBrief importer can import various aspects of a SimBrief flight plan into +FlightGear. The import functionality attempts to support as many aircraft types +as possible, but due to the nature of the beast, it does not work equally well +with all of them. + +Ideally, the following functionality will be supported: + +- Flight Plan Route: This one imports the departure and destination airports, + and all enroute waypoints, into the default flightplan. If "Activate + immediately" is checked, it will also activate the flightplan. +- Departure RWY, SID: This will attempt to select the planned departure runway, + and the planned SID, from the flightplan. Note that this will only work if + your FlightGear navdata matches the selections from SimBrief, which may not + be the case, especially if you're using the default FG navdata and/or an + outdated AIRAC cycle in SimBrief. +- Arrival RWY, STAR: This will attempt to select the planned arrival runway and + STAR. The same caveats apply as with the departure runway and SID. +- Performance Init: This sets a handful of key performance parameters; + currently: cruise altitude and callsign. +- Payload: Imports passenger and cargo weights, and attempts to distribute them + sensibly over available payload weight slots. +- Fuel: Imports block fuel as per the flightplan, and attempts to distribute it + sensibly over available fuel tanks. Because the importer has no idea where + those tanks are located, or what their priorities are, it starts at the top, + and uses a very crude logic to detect left/right pairs. (A more sophisticated + fuel distribution system may be provided in the future). +- Winds Aloft: Runs a background process that sets winds aloft according to the + forecast winds in the flightplan. This will only work with Basic Weather, + since the Advanced Weather engine runs its own wind simulation that will + overwrite winds aloft regardless of what we set. + +Supported Aircraft Types (as per 09/2021) +----------------------------------------- + + | FPL | Dep/SID | Arr/STAR | Perf Init | Payload | Fuel | ++------------------------+-----+---------+----------+-----------+---------+------+ +| E-Jet-family-YV | E-Jet uses its own version of the simbrief importer | ++------------------------+-----+---------+----------+-----------+---------+------+ +| A320-family | A320 uses its own simbrief importer | ++------------------------+-----+---------+----------+-----------+---------+------+ +| 747-8i | yes | yes | yes | yes | yes | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| 747-400 | yes | yes | yes | yes | no | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| 737-800YV | yes | yes | yes | yes | no | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| 777 | yes | yes | yes | yes | no | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| Citation-II | yes | yes | yes | yes | yes | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| CRJ700-family | yes | yes | yes | yes | yes | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ +| QSeries | yes | yes | yes | yes | no | yes | ++------------------------+-----+---------+----------+-----------+---------+------+ diff --git a/addon-config.xml b/addon-config.xml new file mode 100644 index 0000000..9fccc47 --- /dev/null +++ b/addon-config.xml @@ -0,0 +1,28 @@ + + + + + + + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + + 0 + + + + + diff --git a/addon-main.nas b/addon-main.nas new file mode 100644 index 0000000..fcdc022 --- /dev/null +++ b/addon-main.nas @@ -0,0 +1,584 @@ +var urlencode = func(str) { + var out = ''; + var c = ''; + var n = 0; + for (var i = 0; i < size(str); i += 1) { + n = str[i]; + if (string.isalnum(n)) { + out = out ~ chr(n); + } + elsif (n == 32) { + out = out ~ '+'; + } + else { + out = out ~ sprintf('%%%02x', n); + } + } + return out; +}; + +var download = func (username, onSuccess, onFailure=nil) { + if (getprop('/sim/simbrief/downloading')) { + print("SimBrief download already active"); + } + setprop('/sim/simbrief/downloading', 1); + setprop('/sim/simbrief/text-status', 'downloading...'); + var filename = getprop('/sim/fg-home') ~ "/Export/simbrief.xml"; + var url = "https://www.simbrief.com/api/xml.fetcher.php?username=" ~ urlencode(username); + if (onFailure == nil) { + onFailure = func (r) { + setprop('/sim/simbrief/text-status', sprintf('HTTP error (%s/%s)', r.status, r.reason)); + printf("SimBrief download from %s failed with HTTP status %s", + url, r.status); + } + } + + http.save(url, filename) + .done(func (r) { + setprop('/sim/simbrief/text-status', 'parsing...'); + printf("SimBrief download from %s complete.", url); + var errs = []; + call(onSuccess, [filename], nil, {}, errs); + if (size(errs) > 0) { + setprop('/sim/simbrief/text-status', 'parser errors, see log for details'); + debug.printerror(errs); + } + else { + setprop('/sim/simbrief/text-status', 'all done!'); + } + }) + .fail(onFailure) + .always(func { + setprop('/sim/simbrief/downloading', 0); + }); +}; + +var read = func (filename=nil) { + if (filename == nil) { + filename = getprop('/sim/fg-home') ~ "/Export/simbrief.xml"; + } + var xml = io.readxml(filename); + var ofpNode = xml.getChild('OFP'); + if (ofpNode == nil) { + print("Error loading SimBrief OFP"); + return nil; + } + else { + return ofpNode; + } +}; + +var toFlightplan = func (ofp, fp=nil) { + # get departure and destination + var departureID = ofp.getNode('origin/icao_code').getValue(); + var departures = findAirportsByICAO(departureID); + if (departures == nil or size(departures) == 0) { + printf("Airport not found: %s", departureID); + return nil; + } + + var destinationID = ofp.getNode('destination/icao_code').getValue(); + var destinations = findAirportsByICAO(destinationID); + if (destinations == nil or size(destinations) == 0) { + printf("Airport not found: %s", destinationID); + return nil; + } + + # cruise parameters + var initialAltitude = ofp.getNode('general/initial_altitude').getValue(); + var cruiseAltitude = initialAltitude; + var seenTOC = 0; + + # collect enroute waypoints + var wps = []; + var ofpNavlog = ofp.getNode('navlog'); + var ofpFixes = ofpNavlog.getChildren('fix'); + var sidID = nil; + var starID = nil; + foreach (var ofpFix; ofpFixes) { + if (ofpFix.getNode('is_sid_star').getBoolValue()) { + if ((ofpFix.getValue('stage') == 'CLB') and + (getprop('/sim/simbrief/options/import-departure') or 0) and + (sidID == nil)) { + sidID = ofpFix.getValue('via_airway'); + } + elsif ((ofpFix.getValue('stage') == 'DSC') and + (getprop('/sim/simbrief/options/import-arrival') or 0) and + (starID == nil)) { + starID = ofpFix.getValue('via_airway'); + } + # skip: we only want enroute waypoints + continue; + } + var ident = ofpFix.getNode('ident').getValue(); + if (ident == 'TOC' or ident == 'TOD') { + # skip TOC and TOD: the FMS should deal with those dynamically + if (ident == 'TOC') { + seenTOC = 1; + } + continue; + } + var altNode = ofpFix.getNode('altitude_feet'); + var alt = (altNode == nil) ? nil : altNode.getValue(); + var coords = geo.Coord.new(); + coords.set_latlon( + ofpFix.getNode('pos_lat').getValue(), + ofpFix.getNode('pos_long').getValue()); + printf("%s %f %f", ident, coords.lat(), coords.lon()); + var wp = createWP(coords, ident); + if (seenTOC and alt == initialAltitude) { + # this is the waypoint where we expect to reach initial cruise + # altitude + + # reset 'seen TOC' flag to avoid setting alt restrictions on + # subsequent waypoints + seenTOC = 0; + + # we'll use an "at" restriction here: we don't want to climb any + # higher, hence "above" would be wrong, and we want the VNAV to do + # its best to reach the altitude before this point, so "below" + # would also be wrong. + + # This doesn't work, and I don't know why. + # wp.setAltitude(alt, 'at'); + } + else if (alt > cruiseAltitude) { + # this is a step climb target waypoint + cruiseAltitude = alt; + + # This doesn't work, and I don't know why. + # wp.setAltitude(alt, 'at'); + } + append(wps, wp); + } + + # we have everything we need; it's now safe-ish to overwrite or + # create the actual flightplan + + if (fp == nil) { + fp = createFlightplan(); + } + fp.cleanPlan(); + fp.sid = nil; + fp.sid_trans = nil; + fp.star = nil; + fp.star_trans = nil; + fp.approach = nil; + fp.approach_trans = nil; + fp.departure = departures[0]; + fp.destination = destinations[0]; + fp.insertWaypoints(wps, 1); + if (getprop('/sim/simbrief/options/import-departure') or 0) { + departureRunwayID = ofp.getNode('origin').getValue('plan_rwy'); + printf("Trying to select departure: %s / %s", sidID, departureRunwayID); + fp.departure_runway = departures[0].runways[departureRunwayID]; + if (sidID != nil) { + fp.sid = departures[0].getSid(sidID); + if (fp.sid == nil) + fp.sid = departures[0].getSid(sidID ~ '.' ~ departureRunwayID); + if (fp.sid == nil) { + printf("SID not found: %s", sidID); + } + } + } + if (getprop('/sim/simbrief/options/import-arrival') or 0) { + destinationRunwayID = ofp.getNode('destination').getValue('plan_rwy'); + printf("Trying to select arrival: %s / %s", starID, destinationRunwayID); + fp.destination_runway = destinations[0].runways[destinationRunwayID]; + if (starID != nil) { + fp.star = destinations[0].getStar(starID); + if (fp.star == nil) + fp.star = destinations[0].getStar(starID ~ '.' ~ destinationRunwayID); + if (fp.star == nil) { + printf("STAR not found: %s", starID); + } + } + } + return fp; +}; + +var importFOB = func (ofp) { + var unit = ofp.getNode('params/units').getValue(); + var fuelFactor = ((unit == 'lbs') ? LB2KG : 1); + + # From here on, we'll do everything in kilograms (kg) + var fob = ofp.getNode('fuel/plan_ramp').getValue() * fuelFactor; + var unallocated = fob; + var tankNodes = props.globals.getNode('/consumables/fuel').getChildren('tank'); + var numTanks = size(tankNodes); + + printf("Fuel to allocate: %1.1f kg", fob); + + var allocate = func(tankNumber, maxAmount = nil) { + var tankNode = tankNodes[tankNumber]; + if (tankNode == nil) { + printf("Tank #%i not installed", tankNumber); + return; + } + var tankNameNode = tankNode.getNode('name'); + if (tankNameNode == nil) { + printf("Tank #%i not installed", tankNumber); + return; + } + var tankName = tankNameNode.getValue(); + var amount = unallocated; + if (maxAmount != nil) { + amount = math.min(amount, maxAmount); + } + var tankCapacity = + tankNode.getNode('capacity-m3').getValue() * + tankNode.getNode('density-kgpm3').getValue(); + amount = math.min(amount, tankCapacity); + printf("Allocating %1.1f/%1.1f kg to %s", amount, unallocated, tankName); + tankNode.getNode('level-kg').setValue(amount); + unallocated -= amount; + } + + var allocatePair = func (tank1, tank2) { + var cap1 = tankNodes[tank1].getValue('capacity-m3') or 1; + var cap2 = tankNodes[tank2].getValue('capacity-m3') or 1; + var cap = cap1 + cap2; + var allocate1 = unallocated * cap1 / cap; + var allocate2 = unallocated * cap2 / cap; + allocate(tank1, allocate1); + allocate(tank2, allocate2); + } + + var i = 0; + while (i < numTanks) { + var tankNode = tankNodes[i]; + var tankName = tankNode.getValue('name') or 'unnamed'; + if (i >= numTanks - 1 or string.imatch(tankName, "*center*")) { + allocate(i); + i = i + 1; + } + else { + allocatePair(i, i + 1); + i = i + 2; + } + } + printf("Fuel not allocated: %1.1f kg", unallocated); +}; + +var importPayload = func (ofp) { + var unit = ofp.getNode('params/units').getValue(); + var factor = ((unit == 'lbs') ? 1 : KG2LB); + var weightNodes = props.globals.getNode('/sim').getChildren('weight'); + var cargoWeightNodes = []; + var paxWeightNodes = []; + + foreach (var node; weightNodes) { + var nodeName = node.getValue('name'); + + printf("Checking weight node %s", nodeName); + + if (string.imatch(nodeName, "*passenger*") or + string.imatch(nodeName, "*pax*") or + string.imatch(nodeName, "*cabin*") or + string.imatch(nodeName, "*class*") or + string.imatch(nodeName, "*baggage*") or + string.imatch(nodeName, "*seat*")) { + append(paxWeightNodes, node); + } + elsif (string.imatch(nodeName, "*cargo*") or + string.imatch(nodeName, "*payload*")) { + append(cargoWeightNodes, node); + } + } + + if (size(paxWeightNodes) == 0 and size(cargoWeightNodes) == 0) { + printf("Alas, this aircraft does not seem to use the standard weights system. Please configure payload manually."); + return; + } + + # Everything in lbs + var cargoUnallocated = ofp.getNode('weights/cargo').getValue() * factor; + var paxUnallocated = ofp.getNode('weights/payload').getValue() * factor - cargoUnallocated; + + var distribute = func (what, nodes, unallocated) { + printf("Allocating %s: %1.1f lbs", what, unallocated); + var totalF = 0; + foreach (var node; nodes) { + var f = node.getValue('max-lb') - node.getValue('min-lb'); + node.setValue('weight-lb', node.getValue('min-lb')); + unallocated = unallocated - node.getValue('min-lb'); + totalF = totalF + f; + printf("Allocating %1.1f/%1.1f lbs to %s", node.getValue('min-lb'), unallocated, node.getValue('name')); + } + printf("Remaining %s after minimum weights: %1.1f lbs", what, unallocated); + var remaining = unallocated; + if (remaining > 0) { + foreach (var node; nodes) { + var maxAdd = node.getValue('max-lb') - node.getValue('min-lb'); + var f = maxAdd / totalF; + var toAdd = math.min(f * remaining, maxAdd, unallocated); + node.setValue('weight-lb', + node.getValue('weight-lb') + + toAdd); + unallocated = unallocated - toAdd; + printf("Allocating %1.1f/%1.1f lbs to %s", toAdd, unallocated, node.getValue('name')); + } + } + printf("Remaining unallocated %s: %1.1f lbs", what, unallocated); + return unallocated; + } + + if (size(paxWeightNodes) == 0) { + printf("No passenger space found, forcing passengers into cargo hold"); + cargoUnallocated = cargoUnallocated + paxUnallocated; + paxUnallocated = 0; + } + cargoUnallocated = distribute("cargo", cargoWeightNodes, cargoUnallocated); + paxUnallocated = distribute("passengers", paxWeightNodes, paxUnallocated + cargoUnallocated); +}; + +var importPerfInit = func (ofp) { + # climb profile: kts-below-FL100/kts-above-FL100/mach + var climbProfile = split('/', ofp.getNode('general/climb_profile').getValue()); + # descent profile: mach/kts-above-FL100/kts-below-FL100 + var descentProfile = split('/', ofp.getNode('general/descent_profile').getValue()); + var cruiseMach = ofp.getNode('general/cruise_mach').getValue(); + var airline = ofp.getNode('general/icao_airline').getValue(); + var flightNumber = ofp.getNode('general/flight_number').getValue(); + var callsign = (airline == nil) ? flightNumber : (airline ~ flightNumber); + var cruiseAlt = ofp.getNode('general/initial_altitude').getValue(); + + + setprop("/sim/multiplay/callsign", callsign); + setprop("/autopilot/route-manager/cruise/altitude-ft", cruiseAlt); + + if (props.globals.getNode("/controls/flight/speed-schedule") != nil) { + setprop("/controls/flight/speed-schedule/climb-below-10k", climbProfile[0]); + setprop("/controls/flight/speed-schedule/climb-kts", climbProfile[1]); + setprop("/controls/flight/speed-schedule/climb-mach", climbProfile[2] / 100); + setprop("/controls/flight/speed-schedule/cruise-mach", cruiseMach); + setprop("/controls/flight/speed-schedule/descent-mach", descentProfile[0] / 100); + setprop("/controls/flight/speed-schedule/descent-kts", descentProfile[1]); + setprop("/controls/flight/speed-schedule/descent-below-10k", descentProfile[2]); + } +}; + +var aloftTimer = nil; +var aloftPoints = []; + +var setAloftWinds = func (aloftPoint) { + # printf("setAloftWinds()"); + # debug.dump(aloftPoint); + forindex (var i; aloftPoint.layers) { + var node = props.globals.getNode("/environment/config/aloft/entry[" ~ i ~ "]"); + node.getChild('elevation-ft').setValue(aloftPoint.layers[i].alt); + node.getChild('wind-from-heading-deg').setValue(aloftPoint.layers[i].dir); + node.getChild('wind-speed-kt').setValue(aloftPoint.layers[i].spd); + node.getChild('temperature-degc').setValue(aloftPoint.layers[i].temp); + } +}; + +var interpolate = func (f, a, b) { + return a + f * (b - a); +}; + +var interpolateDegrees = func (f, a, b) { + return geo.normdeg(a + geo.normdeg180(b - a) * f); +}; + +var interpolateComponentWise = func (f, ipf, a, b) { + var s = math.min(size(a), size(b)); + var result = []; + for (var i = 0; i < s; i = i+1) { + append(result, ipf(f, a[i], b[i])); + } + return result; +}; + +var interpolateLayers = func (f, a, b) { + if (b == nil) return a; + if (a == nil) return b; + return { + alt: interpolate(f, a.alt, b.alt), + spd: interpolate(f, a.spd, b.spd), + temp: interpolate(f, a.temp, b.temp), + dir: interpolateDegrees(f, a.dir, b.dir), + }; +}; + +var interpolateAloftPoints = func (f, a, b) { + if (b == nil) return a; + if (a == nil) return b; + return { + layers: interpolateComponentWise(f, interpolateLayers, a.layers, b.layers), + }; +}; + +var updateAloft = func () { + # printf("updateAloft()"); + var pos = geo.aircraft_position(); + foreach (var p; aloftPoints) { + p.dist = pos.distance_to(p.coord); + } + var sorted = sort(aloftPoints, func (a, b) { return (a.dist - b.dist); }); + var pointA = sorted[0]; + var pointB = sorted[1]; + var f = (pointB.dist < 0.1) ? 0 : (pointB.dist / (pointA.dist + pointB.dist)); + # foreach (var s; sorted) { + # printf(s.dist); + # } + # debug.dump(f, pointA, pointB); + var aloftPoint = interpolateAloftPoints(f, pointA, pointB); + # printf("Aloft wind interpolation: %f between %s and %s", + # f, pointA.name, pointB.name); + # debug.dump(aloftPoint.layers); + setAloftWinds(aloftPoint); +}; + +var startAloftUpdater = func () { + if (aloftTimer == nil) { + aloftTimer = maketimer(10, updateAloft); + aloftTimer.simulatedTime = 1; + } + if (aloftTimer.isRunning) return; + aloftTimer.start(); +}; + +var importWindsAloft = func (ofp) { + # # disable default winds and set winds-aloft mode + # setprop("/local-weather/config/wind-model", "aloft waypoints"); + # setprop("/environment/params/metar-updates-winds-aloft", 0); + + # if (defined('local_weather')) { + # # clear out the advanced weather winds-aloft interpolation points + # setsize(local_weather.windIpointArray, 0); + # } + + # now go through the flightplan waypoints and create a wind interpolation point for each of them. + var ofpNavlog = ofp.getNode('navlog'); + var ofpFixes = ofpNavlog.getChildren('fix'); + foreach (var ofpFix; ofpFixes) { + var lat = ofpFix.getNode('pos_lat').getValue(); + var lon = ofpFix.getNode('pos_long').getValue(); + var args = [lat, lon]; + var layers = []; + var uneven = 0; + foreach (var ofpWindLayer; ofpFix.getNode('wind_data').getChildren('level')) { + var dir = ofpWindLayer.getNode('wind_dir').getValue(); + var spd = ofpWindLayer.getNode('wind_spd').getValue(); + var alt = ofpWindLayer.getNode('altitude').getValue(); + var temp = ofpWindLayer.getNode('oat').getValue(); + if (alt != 14000) { + # advanced weather ignores this one for some reason + append(args, dir, spd); + } + # pick up every other layer: simbrief reports 10 layers starting + # at sea level, but we can only use 5, and we don't need sea level + # (as that comes from METAR) + if (uneven) { + append(layers, { alt: alt, dir: dir, spd: spd, temp: temp }); + } + uneven = !uneven; + } + # if (defined('local_weather')) { + # call(local_weather.set_wind_ipoint, args); + # } + var aloftPos = geo.Coord.new(); + aloftPos.set_latlon(lat, lon); + + var aloftPoint = { coord: aloftPos, dist: 0.0, layers: layers, name: ofpFix.getNode('ident').getValue() }; + append(aloftPoints, aloftPoint); + } + startAloftUpdater(); +}; + +var loadFP = func () { + var username = getprop('/sim/simbrief/username'); + if (username == nil or username == '') { + print("Username not set"); + return; + } + + download(username, func (filename) { + var ofpNode = read(filename); + if (ofpNode == nil) { + print("Error loading simbrief XML file"); + return; + } + + if (getprop('/sim/simbrief/options/import-fp') or 0) { + var modifyableFlightplan = flightplan(); + var haveFMS = globals['fms'] != nil + and typeof(fms) == 'hash' + and typeof(fms['getModifyableFlightplan']) == 'func' + and typeof(fms['commitFlightplan']) == 'func' + and typeof(fms['kickRouteManager']) == 'func'; + + if (haveFMS) { + modifyableFlightplan = fms.getModifyableFlightplan(); + } + var fp = toFlightplan(ofpNode, modifyableFlightplan); + if (fp == nil) { + print("Error parsing flight plan"); + } + else { + if (haveFMS) { + if (getprop('/sim/simbrief/options/autocommit') or 0) { + fms.commitFlightplan(); + } + fms.kickRouteManager(); + } + else { + if (getprop('/sim/simbrief/options/autocommit') or 0) { + fgcommand("activate-flightplan", props.Node.new({"activate": 1})); + } + } + } + } + if (getprop('/sim/simbrief/options/import-fob') or 0) { + importFOB(ofpNode); + } + if (getprop('/sim/simbrief/options/import-payload') or 0) { + importPayload(ofpNode); + } + if (getprop('/sim/simbrief/options/import-perfinit') or 0) { + importPerfInit(ofpNode); + } + if (getprop('/sim/simbrief/options/import-winds-aloft') or 0) { + importWindsAloft(ofpNode); + } + }); +}; + +var findMenuNode = func (create=0) { + var equipmentMenuNode = props.globals.getNode('/sim/menubar/default/menu[5]'); + foreach (var item; equipmentMenuNode.getChildren('item')) { + if (item.getValue('name') == 'addon-simbrief') { + return item; + } + } + if (create) { + return equipmentMenuNode.addChild('item'); + } + else { + return nil; + } +}; + +var main = func(addon) { + if (globals['simbrief'] == nil) { + print("SIMBRIEF"); + globals['simbrief'] = { + 'loadFP': loadFP + }; + var myMenuNode = findMenuNode(1); + myMenuNode.setValues({ + enabled: 'true', + name: 'addon-simbrief', + label: 'SimBrief Import', + binding: { + 'command': 'dialog-show', + 'dialog-name': 'addon-simbrief-dialog', + }, + }); + fgcommand('reinit', {'subsystem': 'gui'}); + } + else { + print("SIMBRIEF importer already present, not activating add-on"); + } +}; diff --git a/addon-menubar-items.xml b/addon-menubar-items.xml new file mode 100644 index 0000000..bed4c74 --- /dev/null +++ b/addon-menubar-items.xml @@ -0,0 +1,9 @@ + + + + FlightGear add-on menu bar items + 1 + + + + diff --git a/addon-metadata.xml b/addon-metadata.xml new file mode 100644 index 0000000..668690d --- /dev/null +++ b/addon-metadata.xml @@ -0,0 +1,72 @@ + + + + + FlightGear add-on metadata + 1 + + + + nl.tobiasdammers.fg-simbrief-addon + SimBrief importer + 0.1.0 + + + + Tobias Dammers + tdammers@gmail.com + https://github.com/tdammers + + + + + + Tobias Dammers + tdammers@gmail.com + https://github.com/tdammers + + + + + SimBrief importer + + + + + + + + MIT + + + https://mit-license.org/ + + + 2017.4.0 + none + + + + https://github.com/tdammers/fg-simbrief-addon + + + + https://github.com/tdammers/fg-simbrief-addon + + + + + + + https://github.com/tdammers/fg-simbrief-addon + + + + + simbrief + flightplan + routemanager + + + + diff --git a/gui/dialogs/addon-simbrief-dialog.xml b/gui/dialogs/addon-simbrief-dialog.xml new file mode 100644 index 0000000..a079832 --- /dev/null +++ b/gui/dialogs/addon-simbrief-dialog.xml @@ -0,0 +1,186 @@ + + + addon-simbrief-dialog + vbox + 500 + + + hbox + + 1 + + + + + + + + 1 + + + + + + + + + vbox + + hbox + + + 100 + left + + + /sim/simbrief/username + + dialog-apply + + left + + + + 10 + + + hbox + + + 1 + + + + + + + + 1 + + + + + hbox + + /sim/simbrief/options/import-fp + + + dialog-apply + + left + top + 100 + + + 100 + vbox + + /sim/simbrief/options/import-departure + + + dialog-apply + + left + 100 + + + /sim/simbrief/options/import-arrival + + + dialog-apply + + left + 100 + + + /sim/simbrief/options/autocommit + + + dialog-apply + + left + 100 + + + + + hbox + + /sim/simbrief/options/import-perfinit + + + dialog-apply + + left + 100 + + + + hbox + + /sim/simbrief/options/import-payload + + + dialog-apply + + left + 100 + + + + hbox + + /sim/simbrief/options/import-fob + + + dialog-apply + + left + 100 + + + + hbox + + /sim/simbrief/options/import-winds-aloft + + + dialog-apply + + left + 100 + + + + + hbox + + + %s + /sim/simbrief/text-status + true + + + + hbox + + + + + +