diff --git a/Astrogator.netkan b/Astrogator.netkan
index 43ce698..4766498 100644
--- a/Astrogator.netkan
+++ b/Astrogator.netkan
@@ -15,6 +15,9 @@
"bugtracker": "https://github.com/HebaruSan/Astrogator/issues",
"repository": "https://github.com/HebaruSan/Astrogator"
},
+ "depends": [
+ { "name": "ModuleManager" }
+ ],
"suggests": [
{ "name": "GravityTurn" },
{ "name": "Trajectories" },
@@ -23,6 +26,7 @@
{ "name": "PreciseManeuver" },
{ "name": "LandingHeight" },
{ "name": "NavballDockingIndicator" },
- { "name": "WarpEverywhere" }
+ { "name": "WarpEverywhere" },
+ { "name": "RasterPropMonitor" }
]
}
diff --git a/Astrogator.version b/Astrogator.version
index d4d54b3..b05d72b 100644
--- a/Astrogator.version
+++ b/Astrogator.version
@@ -10,7 +10,7 @@
"VERSION": {
"MAJOR": 0,
"MINOR": 5,
- "PATCH": 1,
+ "PATCH": 2,
"BUILD": 0
},
"KSP_VERSION_MIN": {
diff --git a/Makefile b/Makefile
index fa58016..42015ff 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ SOURCEDIR=src
SOURCE=$(wildcard $(SOURCEDIR)/*.cs)
ASSETDIR=assets
ICONS=$(wildcard $(ASSETDIR)/*.png)
+CONFIGS=$(wildcard $(ASSETDIR)/*.cfg)
README=README.md
GAMELINK=$(SOURCEDIR)/KSP_x64_Data
DEFAULTGAMEDIR="$(HOME)/.local/share/Steam/SteamApps/common/Kerbal Space Program"
@@ -31,7 +32,7 @@ $(DEBUGDLL): $(SOURCE) $(GAMELINK)
$(RELEASEDLL): $(SOURCE) $(GAMELINK)
cd $(SOURCEDIR) && xbuild /p:Configuration=Release
-$(RELEASEZIP): $(DEBUGDLL) $(ICONS) $(README) $(DLLDOCS) $(DLLSYMBOLS) $(LICENSE) $(VERSION)
+$(RELEASEZIP): $(DEBUGDLL) $(ICONS) $(README) $(DLLDOCS) $(DLLSYMBOLS) $(LICENSE) $(VERSION) $(CONFIGS)
mkdir -p $(DISTDIR)
cp $^ $(DISTDIR)
zip -r $@ $(DISTDIR)
diff --git a/TODO.md b/TODO.md
index 7a2f9f1..06530ba 100644
--- a/TODO.md
+++ b/TODO.md
@@ -10,10 +10,6 @@
- Only rule out a range once the entire thing is overdue
- [ ] i18n / l10n (once SQUAD releases their version of it)
-## [Kottabos](https://www.youtube.com/watch?v=bcl9sy6CJAY) feedback
-
-- [ ] RasterPropMonitor integration
-
## More transfer types
- [ ] Launch to orbit
diff --git a/assets/AstrogatorRPM.cfg b/assets/AstrogatorRPM.cfg
new file mode 100644
index 0000000..a342a27
--- /dev/null
+++ b/assets/AstrogatorRPM.cfg
@@ -0,0 +1,25 @@
+// Config format based on NavInstruments
+// https://github.com/kujuman/NavInstruments/blob/master/GameData/KerbalScienceFoundation/NavInstruments/MFD/addToRPM018.cfg
+@PROP[RasterPropMonitorBasicMFD]:Final
+{
+ @MODULE[RasterPropMonitor]
+ {
+ PAGE
+ {
+ // https://github.com/Mihara/RasterPropMonitor/wiki/Page-handlers
+ name = astrogator
+ // Share the autopilot button with MechJeb
+ button = button_D
+ PAGEHANDLER
+ {
+ name = AstrogatorMenu
+ method = ShowMenu
+ pageActiveMethod = PageActive
+ buttonClickMethod = ButtonClick
+ buttonReleaseMethod = ButtonRelease
+ pageTitle = Astrogator
+ }
+ textureURL = JSI/RasterPropMonitor/Library/Textures/bg01
+ }
+ }
+}
diff --git a/screenshots/rpm-display.png b/screenshots/rpm-display.png
new file mode 100644
index 0000000..bc629c3
Binary files /dev/null and b/screenshots/rpm-display.png differ
diff --git a/src/AstrogationLoadBehaviorette.cs b/src/AstrogationLoadBehaviorette.cs
new file mode 100644
index 0000000..683a0ff
--- /dev/null
+++ b/src/AstrogationLoadBehaviorette.cs
@@ -0,0 +1,272 @@
+using System;
+using System.Threading;
+using System.ComponentModel;
+
+namespace Astrogator {
+
+ using static DebugTools;
+ using static KerbalTools;
+
+ ///
+ /// The logic of loading our model got too complicated to stay embedded in the main class.
+ /// Also, we want to share it with the RasterPropMonitor display screen.
+ /// This isn't a full "behavior" object, but it can be used by one to take care of a lot of tasks.
+ ///
+ /// This class is responsible for loading the main model object and providing functions to
+ /// refresh it as needed.
+ /// It defers some operations to the background and keeps track of when it's OK to do that.
+ ///
+ /// There's no point in calculating if the window isn't open, so we suppress calculations in that case.
+ /// We also abort any load request that happens while another load is already in progress.
+ /// We call it an "open display" so our Raster Prop Monitor widget can get in on the act.
+ ///
+ /// We need to refresh the data when the orbit changes, but if you do a ten-minute burn,
+ /// we can't churn the CPU constantly for that entire time.
+ /// So we require that at least 5 seconds have passed since the last calculation.
+ ///
+ /// However, if the user toggles the window rapidly, or switches focus in the tracking station,
+ /// we need to calculate regardless of whether 5 seconds have passed!
+ /// The same goes for if another calculation is in progress.
+ ///
+ /// Burns can expire if their burn time elapses into the past, so we need to check them once per second.
+ ///
+ public class AstrogationLoadBehaviorette {
+
+ ///
+ /// Construct a loader object for the given model
+ ///
+ /// Model object for us to manage
+ /// Function to call if we trigger our own refresh without being asked (usually because a burn expired)
+ public AstrogationLoadBehaviorette(AstrogationModel m, LoadDoneCallback unReqNotif)
+ {
+ model = m;
+ unrequestedLoadNotification = unReqNotif;
+
+ // Watch for expiring burns
+ StartBurnTimePolling();
+ }
+
+ private AstrogationModel model { get; set; }
+ private bool loading { get; set; }
+ private static readonly object bgLoadMutex = new object();
+ private const double minSecondsBetweenLoads = 5;
+ private double lastUpdateTime { get; set; }
+ private LoadDoneCallback unrequestedLoadNotification { get; set; }
+ private int numOpenDisplays { get; set; } = 0;
+
+ ///
+ /// Tell the loader that we are currently displaying the data.
+ /// If it thinks we aren't, it won't calculate anything.
+ ///
+ public void OnDisplayOpened() { ++numOpenDisplays; }
+
+ ///
+ /// Tell the loader that we're closing a display.
+ /// If it thinks they're all gone, it won't calculate anything.
+ ///
+ public void OnDisplayClosed() { --numOpenDisplays; }
+
+ private bool AllowStart(ITargetable newOrigin) {
+ return model != null
+ && numOpenDisplays > 0
+ && (
+ // If you've switched origins, we have to update now
+ newOrigin != model.origin
+ // Otherwise we only update if there isn't already one in progress
+ // and the minimum refresh interval has elapsed since the last one.
+ || (!loading && lastUpdateTime + minSecondsBetweenLoads < Planetarium.GetUniversalTime())
+ );
+ }
+
+ ///
+ /// Callback type for notifications of load completion or failure.
+ ///
+ public delegate void LoadDoneCallback();
+
+ ///
+ /// Request a refresh of the data, intended to be called by event handlers.
+ /// Will often refuse to refresh to save CPU time!
+ /// Note that the callbacks may be called from background jobs, and so they
+ /// should never do any Unity UI manipulation (unless you like hard crashes).
+ /// Setting member variables seems to be safe.
+ /// This is supposed to be the single entry point when any other class needs to
+ /// load data for the plug-in.
+ ///
+ /// Body for which to calculate; can override some throttling behaviors if it's different from the last one
+ /// Function to call when we have enough data for a simple display, but not quite complete
+ /// Function to call on successful completion of the load
+ /// Function to call if we decide not to load
+ ///
+ /// True if we kicked off an actual refresh, false otherwise.
+ ///
+ public bool TryStartLoad(ITargetable newOrigin, LoadDoneCallback partialLoaded, LoadDoneCallback fullyLoaded, LoadDoneCallback aborted)
+ {
+ if (newOrigin != null) {
+ // 1. Check whether we should even do anything, if not call aborted() and return
+ if (!AllowStart(newOrigin)) {
+ if (aborted != null) {
+ aborted();
+ }
+ return false;
+ } else {
+ // 2. Start background job
+ DbgFmt("Starting background job");
+ BackgroundWorker bgworker = new BackgroundWorker();
+ bgworker.DoWork += Load;
+ bgworker.RunWorkerAsync(new BackgroundParameters() {
+ origin = newOrigin,
+ partialLoaded = partialLoaded,
+ fullyLoaded = fullyLoaded,
+ aborted = aborted
+ });
+ return true;
+ }
+ } else {
+ DbgFmt("Somebody tried to load with a null origin");
+ if (aborted != null) {
+ aborted();
+ }
+ return false;
+ }
+ }
+
+ private class BackgroundParameters {
+ public ITargetable origin { get; set; }
+ public LoadDoneCallback partialLoaded { get; set; }
+ public LoadDoneCallback fullyLoaded { get; set; }
+ public LoadDoneCallback aborted { get; set; }
+ }
+
+ ///
+ /// Main driver for calculations.
+ /// Always runs in a background thread!
+ ///
+ /// Standard parameter from BackgroundWorker
+ /// Params from main thread, e.Argument is a BackgroundParameters object with our actual info in it
+ private void Load(object sender, DoWorkEventArgs e)
+ {
+ BackgroundParameters bp = e.Argument as BackgroundParameters;
+ if (bp != null) {
+ lock (bgLoadMutex) {
+ loading = true;
+
+ // 3. In background, load the first pass of stuff
+ model.Reset(bp.origin);
+
+ if (PlaneChangesEnabled) {
+
+ // Ejection burns are relatively cheap to calculate and needed for the display to look good
+ RecalculateEjections();
+
+ // 4. Call partialLoaded() (window can open, view can sort)
+ if (bp.partialLoaded != null) {
+ bp.partialLoaded();
+ }
+
+ // 5. Load everything else
+ RecalculatePlaneChanges();
+
+ // 6. Call fullyLoaded() (sort again)
+ if (bp.fullyLoaded != null) {
+ bp.fullyLoaded();
+ }
+
+ } else {
+
+ // Ejection burns are all we need
+ RecalculateEjections();
+
+ // 6. Call fullyLoaded() (sort again)
+ if (bp.fullyLoaded != null) {
+ bp.fullyLoaded();
+ }
+
+ }
+
+ lastUpdateTime = Planetarium.GetUniversalTime();
+ loading = false;
+ }
+ } else {
+ if (bp.aborted != null) {
+ bp.aborted();
+ }
+ }
+ }
+
+ ///
+ /// Check once per second if any of the transfers are out of date.
+ /// Note, this could mean we need to re-sort the view!
+ /// That's what the unrequestedLoadNotification is for.
+ ///
+ private void StartBurnTimePolling()
+ {
+ System.Timers.Timer t = new System.Timers.Timer() { Interval = 1000 };
+ t.Elapsed += BurnTimePoll;
+ t.Start();
+ }
+
+ private void BurnTimePoll(object source, System.Timers.ElapsedEventArgs e)
+ {
+ if (numOpenDisplays > 0 && !loading) {
+ bool found = false;
+ lock (bgLoadMutex) {
+ double now = Planetarium.GetUniversalTime();
+ for (int i = 0; i < model.transfers.Count; ++i) {
+ if (model.transfers[i].ejectionBurn != null
+ && model.transfers[i].ejectionBurn.atTime < now) {
+
+ DbgFmt("Recalculating expired burn");
+ found = true;
+
+ model.transfers[i].CalculateEjectionBurn();
+ if (PlaneChangesEnabled) {
+ model.transfers[i].CalculatePlaneChangeBurn();
+ }
+ }
+ }
+ }
+ // Tell the main behavior object to refresh the view if the time of any of the burns is changed
+ if (found && unrequestedLoadNotification != null) {
+ unrequestedLoadNotification();
+ }
+ }
+ }
+
+ private void RecalculateEjections()
+ {
+ for (int i = 0; i < model.transfers.Count; ++i) {
+ try {
+ model.transfers[i].CalculateEjectionBurn();
+ } catch (Exception ex) {
+ DbgExc("Problem with load of ejection burn", ex);
+ }
+ }
+ }
+
+ private bool PlaneChangesEnabled {
+ get {
+ return Settings.Instance.GeneratePlaneChangeBurns
+ && Settings.Instance.AddPlaneChangeDeltaV;
+ }
+ }
+
+ private void RecalculatePlaneChanges()
+ {
+ if (PlaneChangesEnabled) {
+ for (int i = 0; i < model.transfers.Count; ++i) {
+ try {
+ Thread.Sleep(200);
+ model.transfers[i].CalculatePlaneChangeBurn();
+ } catch (Exception ex) {
+ DbgExc("Problem with background load of plane change burn", ex);
+
+ // If a route calculation crashes, it can leave behind a temporary node.
+ ClearManeuverNodes();
+ }
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/src/AstrogationModel.cs b/src/AstrogationModel.cs
index ae7f02a..5d980ff 100644
--- a/src/AstrogationModel.cs
+++ b/src/AstrogationModel.cs
@@ -25,6 +25,14 @@ public AstrogationModel(ITargetable org)
}
}
+ ///
+ /// We need to allow an empty object to be valid so we can load most of it in the background.
+ ///
+ public AstrogationModel()
+ {
+ transfers = new List();
+ }
+
///
/// The vessel or body that we're starting from.
///
diff --git a/src/Astrogator.cs b/src/Astrogator.cs
index 0322e7a..e00ed65 100644
--- a/src/Astrogator.cs
+++ b/src/Astrogator.cs
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Threading;
using KSP.UI.Screens;
namespace Astrogator {
@@ -29,6 +27,13 @@ public class SpaceCenterAstrogator : Astrogator { }
/// Our main plugin behavior.
public class Astrogator : MonoBehavior {
+ public Astrogator()
+ : base()
+ {
+ model = new AstrogationModel();
+ loader = new AstrogationLoadBehaviorette(model, ResetViewBackground);
+ }
+
private bool VesselMode { get; set; }
///
@@ -58,10 +63,6 @@ public void Start()
// This event fires when switching focus in the tracking station
GameEvents.onPlanetariumTargetChanged.Add(TrackingStationTargetChanged);
- // This is called in the flight scene when the vessel is fully loaded.
- // We need that to be able to calculate plane changes.
- GameEvents.onFlightReady.Add(OnFlightReady);
-
// Reset the view when we take off or land, etc.
GameEvents.onVesselSituationChange.Add(OnSituationChanged);
@@ -94,10 +95,6 @@ public void OnDisable()
// This event fires when switching focus in the tracking station
GameEvents.onPlanetariumTargetChanged.Remove(TrackingStationTargetChanged);
- // This is called in the flight scene when the vessel is fully loaded.
- // We need that to be able to calculate plane changes.
- GameEvents.onFlightReady.Remove(OnFlightReady);
-
// Reset the view when we take off or land, etc.
GameEvents.onVesselSituationChange.Remove(OnSituationChanged);
@@ -183,12 +180,16 @@ private void onAppLaunchHoverOut()
///
private void onAppLaunchToggleOn()
{
- DbgFmt("Ready for action");
- if (model == null) {
- StartLoadingModel((ITargetable)FlightGlobals.ActiveVessel
- ?? (ITargetable)FlightGlobals.getMainBody());
- }
- ShowMainWindow();
+ // TryStartLoad aborts if no displays are open, so we don't have to track that from every event handler
+ loader.OnDisplayOpened();
+
+ // Begin loading, open window when partially complete, refresh it when fully complete, also open on abort.
+ loader.TryStartLoad(
+ (ITargetable)FlightGlobals.ActiveVessel ?? (ITargetable)FlightGlobals.getMainBody(),
+ () => { needViewOpen = true; },
+ () => { needViewOpen = true; },
+ () => { needViewOpen = true; }
+ );
}
///
@@ -197,106 +198,21 @@ private void onAppLaunchToggleOn()
private void onAppLaunchToggleOff()
{
DbgFmt("Returning to hangar");
- HideMainWindow();
- }
-
- #endregion App launcher
-
- private bool flightReady { get; set; }
+ HideMainWindow(true);
- #region Background loading
-
- private void OnFlightReady()
- {
- flightReady = true;
- if (Settings.Instance.GeneratePlaneChangeBurns
- && Settings.Instance.AddPlaneChangeDeltaV) {
- StartLoadingModel(model.origin);
- ResetView();
- }
+ // Tell the loader the window is closed so it can stop processing
+ loader.OnDisplayClosed();
}
- private void StartLoadingModel(ITargetable origin, bool fromScratch = false)
- {
- // Set up the very basics of the model so the view has something to display during load
- if (fromScratch || model == null) {
- DbgFmt("Assembling model");
- model = new AstrogationModel(origin);
- DbgFmt("Model assembled");
- } else {
- model.Reset(origin);
- }
-
- // Do the easy calculations in the foreground so the view can sort properly right away
- CalculateEjectionBurns();
-
- if (Settings.Instance.GeneratePlaneChangeBurns
- && Settings.Instance.AddPlaneChangeDeltaV) {
-
- DbgFmt("Delegating load to background");
-
- BackgroundWorker bgworker = new BackgroundWorker();
- bgworker.DoWork += bw_LoadModel;
- bgworker.RunWorkerCompleted += bw_DoneLoadingModel;
- bgworker.RunWorkerAsync();
-
- DbgFmt("Launched background");
- }
- }
-
- private static readonly object bgLoadMutex = new object();
-
- private void bw_LoadModel(object sender, DoWorkEventArgs e)
- {
- lock (bgLoadMutex) {
- DbgFmt("Beginning background model load");
- CalculatePlaneChangeBurns();
- DbgFmt("Finished background model load");
- }
- }
-
- private void CalculateEjectionBurns()
- {
- // Blast through the ejection burns so the popup has numbers ASAP
- for (int i = 0; i < model.transfers.Count; ++i) {
- try {
- model.transfers[i].CalculateEjectionBurn();
- } catch (Exception ex) {
- DbgExc("Problem with load of ejection burn", ex);
- }
- }
- }
-
- private void CalculatePlaneChangeBurns()
- {
- if (flightReady
- && Settings.Instance.GeneratePlaneChangeBurns
- && Settings.Instance.AddPlaneChangeDeltaV) {
- for (int i = 0; i < model.transfers.Count; ++i) {
- try {
- Thread.Sleep(200);
- model.transfers[i].CalculatePlaneChangeBurn();
- } catch (Exception ex) {
- DbgExc("Problem with background load of plane change burn", ex);
-
- // If a route calculation crashes, it can leave behind a temporary node.
- ClearManeuverNodes();
- }
- }
- }
- }
-
- private void bw_DoneLoadingModel(object sender, RunWorkerCompletedEventArgs e)
- {
- DbgFmt("Background load complete");
- }
-
- #endregion Background loading
+ #endregion App launcher
#region Main window
- private AstrogationModel model { get; set; }
- private AstrogationView view { get; set; }
+ private AstrogationModel model { get; set; }
+ private AstrogationLoadBehaviorette loader { get; set; }
+ private AstrogationView view { get; set; }
+ private bool needViewClose { get; set; }
+ private bool needViewOpen { get; set; }
///
/// Open the main window listing transfers.
@@ -318,7 +234,7 @@ private void ShowMainWindow()
///
/// Close the main window.
///
- private void HideMainWindow(bool userInitiated = true)
+ private void HideMainWindow(bool userInitiated)
{
if (view != null) {
view.Dismiss();
@@ -335,14 +251,23 @@ private void HideMainWindow(bool userInitiated = true)
private void ResetView(bool resetModel = false)
{
if (resetModel) {
- StartLoadingModel(model.origin);
- }
- if (view != null) {
- HideMainWindow();
+ loader.TryStartLoad(model.origin, null, ResetViewBackground, null);
+ } else if (view != null) {
+ HideMainWindow(false);
ShowMainWindow();
}
}
+ ///
+ /// Unity completely freaks out, sometimes with a hard crash,
+ /// if you try to open a window from a background thread when it's not ready.
+ /// So instead we'll just make a note and do it in the next Update() call.
+ ///
+ private void ResetViewBackground()
+ {
+ needViewClose = needViewOpen = true;
+ }
+
private static void AdjustManeuver(BurnModel burn, Vector3d direction, double fraction = 1.0)
{
const double DELTA_V_INCREMENT_LARGE = 0.5,
@@ -449,6 +374,17 @@ private static void AdjustManeuver(BurnModel burn, Vector3d direction, double fr
///
public void Update()
{
+ // Close the window if a background thread asked us to.
+ if (view != null && (needViewClose || needViewOpen)) {
+ HideMainWindow(false);
+ needViewClose = false;
+ }
+ // Open the window if a background thread asked us to.
+ if (needViewOpen) {
+ ShowMainWindow();
+ needViewOpen = false;
+ }
+
CheckIfNodesDisappeared();
if (Settings.Instance.TranslationAdjust
@@ -504,8 +440,7 @@ private void OnTargetChanged()
if (model != null) {
if (!model.HasDestination(FlightGlobals.fetch.VesselTarget)) {
DbgFmt("Reloading model and view on target change");
- StartLoadingModel(model.origin);
- ResetView();
+ loader.TryStartLoad(model.origin, null, ResetViewBackground, null);
}
}
}
@@ -520,31 +455,14 @@ private bool OrbitChanged()
private void OnOrbitChanged()
{
- if (prevOrbit == null) {
- DbgFmt("No previous orbit.");
- } else {
- DbgFmt(prevOrbit.ComparisonDescription(FlightGlobals.ActiveVessel.orbit));
- }
-
- if (model != null) {
-
- // Just recalculate the ejection burns since those are relatively simple
- for (int i = 0; i < model.transfers.Count; ++i) {
- try {
- model.transfers[i].CalculateEjectionBurn();
- } catch (Exception ex) {
- DbgExc("Problem after orbit change", ex);
- }
- }
- }
+ loader.TryStartLoad(model.origin, null, ResetViewBackground, null);
}
private void OnSituationChanged(GameEvents.HostedFromToAction e)
{
if (model != null && view != null && e.host == FlightGlobals.ActiveVessel) {
DbgFmt("Situation of {0} changed from {1} to {2}", TheName(e.host), e.from, e.to);
- StartLoadingModel(e.host);
- ResetView();
+ loader.TryStartLoad(e.host, null, ResetViewBackground, null);
}
}
@@ -556,8 +474,7 @@ private void SOIChanged(CelestialBody newBody)
if (model != null && view != null) {
DbgFmt("Entered {0}'s sphere of influence", newBody.theName);
// The old list no longer applies because reachable bodies depend on current SOI
- StartLoadingModel(model.origin ?? (ITargetable)FlightGlobals.ActiveVessel);
- ResetView();
+ loader.TryStartLoad(model.origin, null, ResetViewBackground, null);
}
}
@@ -573,9 +490,10 @@ private void TrackingStationTargetChanged(MapObject target)
&& target != null) {
DbgFmt("Tracking station changed target to {0}", target);
- StartLoadingModel((ITargetable)target.vessel
- ?? (ITargetable)target.celestialBody);
- ResetView();
+ loader.TryStartLoad(
+ (ITargetable)target.vessel ?? (ITargetable)target.celestialBody,
+ null, ResetViewBackground, null
+ );
}
}
diff --git a/src/Astrogator.csproj b/src/Astrogator.csproj
index ff3e62d..4a84a55 100644
--- a/src/Astrogator.csproj
+++ b/src/Astrogator.csproj
@@ -58,9 +58,11 @@
+
+
diff --git a/src/AstrogatorMenu.cs b/src/AstrogatorMenu.cs
new file mode 100644
index 0000000..ab06262
--- /dev/null
+++ b/src/AstrogatorMenu.cs
@@ -0,0 +1,270 @@
+using System;
+using System.Text;
+using System.Globalization;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Astrogator {
+
+ using static DebugTools;
+ using static KerbalTools;
+ using static ViewTools;
+
+ ///
+ /// https://github.com/Mihara/RasterPropMonitor/wiki/Page-handlers
+ ///
+ public class AstrogatorMenu : InternalModule {
+
+ AstrogatorMenu()
+ : base()
+ {
+ model = new AstrogationModel(
+ (ITargetable)FlightGlobals.ActiveVessel
+ ?? (ITargetable)FlightGlobals.getMainBody());
+ loader = new AstrogationLoadBehaviorette(model, null);
+ timeToWait = new List();
+ cursorTransfer = 0;
+
+ loader.TryStartLoad(model.origin, null, null, null);
+ }
+
+ [KSPField]
+ public int buttonUp = 0;
+
+ [KSPField]
+ public int buttonDown = 1;
+
+ [KSPField]
+ public int buttonEnter = 2;
+
+ [KSPField]
+ public int buttonEsc = 3;
+
+ [KSPField]
+ public int buttonHome = 4;
+
+ private AstrogationModel model { get; set; }
+ private AstrogationLoadBehaviorette loader { get; set; }
+ private List timeToWait { get; set; }
+ private double lastUniversalTime { get; set; }
+ private int cursorTransfer { get; set; }
+ private bool cursorMoved { get; set; }
+ private string menu { get; set; }
+ private int? activeButton { get; set; }
+
+ private void addHeaders(StringBuilder sb)
+ {
+ bool firstCol = true;
+ for (int i = 0; i < Columns.Length; ++i) {
+ ColumnDefinition col = Columns[i];
+ int width = 0;
+ for (int span = 0; span < col.headerColSpan; ++span) {
+ width += Columns[i + span].monospaceWidth;
+ }
+ if (width > 0) {
+ width += (col.headerColSpan - 1);
+ if (firstCol) {
+ firstCol = false;
+ width += 2;
+ }
+ switch (col.headerStyle.alignment) {
+ case TextAnchor.LowerLeft:
+ sb.AppendFormat(
+ string.Format("{0}0,-{1}{2}", "{", width, "}"),
+ col.header
+ );
+ break;
+ case TextAnchor.LowerCenter:
+ sb.Append(centerString(col.header, width));
+ break;
+ case TextAnchor.LowerRight:
+ sb.AppendFormat(
+ string.Format("{0}0,{1}{2}", "{", width, "}"),
+ col.header
+ );
+ break;
+ }
+ sb.Append(" ");
+ }
+ }
+ }
+
+ private string colContentFormat(ColumnDefinition col)
+ {
+ switch (col.contentStyle.alignment) {
+ case TextAnchor.MiddleLeft:
+ return string.Format("{0}0,-{1}{2}", "{", col.monospaceWidth, "}");
+ break;
+ case TextAnchor.MiddleRight:
+ return string.Format("{0}0,{1}{2}", "{", col.monospaceWidth, "}");
+ break;
+ }
+ return "{0}";
+ }
+
+ private void addRow(StringBuilder sb, TransferModel m, DateTimeParts dt, bool selected)
+ {
+ string destLabel = CultureInfo.InstalledUICulture.TextInfo.ToTitleCase(TheName(m.destination));
+
+ sb.Append(Environment.NewLine);
+ sb.Append(selected ? "> " : " ");
+ for (int i = 0; i < Columns.Length; ++i) {
+ ColumnDefinition col = Columns[i];
+ // TODO - check style's text color and convert to [#rrggbbaa]
+ switch (col.content) {
+ case ContentEnum.PlanetName:
+ sb.AppendFormat(
+ colContentFormat(col),
+ (destLabel.Length > col.monospaceWidth ? destLabel.Substring(0, col.monospaceWidth) : destLabel)
+ );
+ break;
+
+ case ContentEnum.YearsTillBurn:
+ sb.AppendFormat(
+ colContentFormat(col),
+ TimePieceString("{0}y", dt.years, dt.needYears)
+ );
+ break;
+
+ case ContentEnum.DaysTillBurn:
+ sb.AppendFormat(
+ colContentFormat(col),
+ TimePieceString("{0}d", dt.days, dt.needDays)
+ );
+ break;
+
+ case ContentEnum.HoursTillBurn:
+ sb.AppendFormat(
+ colContentFormat(col),
+ TimePieceString("{0}h", dt.hours, dt.needHours)
+ );
+ break;
+
+ case ContentEnum.MinutesTillBurn:
+ sb.AppendFormat(
+ colContentFormat(col),
+ TimePieceString("{0}m", dt.minutes, dt.needMinutes)
+ );
+ break;
+
+ case ContentEnum.SecondsTillBurn:
+ sb.AppendFormat(
+ colContentFormat(col),
+ TimePieceString("{0}s", dt.seconds, true)
+ );
+ break;
+
+ case ContentEnum.DeltaV:
+ sb.AppendFormat(
+ colContentFormat(col),
+ FormatSpeed(
+ ((m.planeChangeBurn == null || !Settings.Instance.AddPlaneChangeDeltaV)
+ ? m.ejectionBurn?.totalDeltaV
+ : (m.ejectionBurn?.totalDeltaV + m.planeChangeBurn.totalDeltaV)) ?? 0,
+ Settings.Instance.DisplayUnits)
+ );
+ break;
+
+ }
+ sb.Append(" ");
+ }
+ }
+
+ public string ShowMenu(int columns, int rows)
+ {
+ if ((Refresh() || cursorMoved) && model.transfers.Count == timeToWait.Count) {
+ StringBuilder sb = new StringBuilder();
+ sb.Append(centerString(" " + AstrogationView.DisplayName + " ", columns, '-'));
+ sb.Append(Environment.NewLine);
+ sb.Append("[#a0a0a0ff]");
+ sb.Append(centerString(String.Format("Transfers from {0}", TheName(model.origin)), columns));
+ sb.Append(Environment.NewLine);
+ sb.Append(Environment.NewLine);
+
+ // [#rrggbbaa]
+ sb.Append("[#22ff22ff]");
+ addHeaders(sb);
+
+ // Wrap the cursor around the edges now because it only tells us dimensions here.
+ while (cursorTransfer < 0) {
+ cursorTransfer += model.transfers.Count;
+ }
+ while (cursorTransfer >= model.transfers.Count) {
+ cursorTransfer -= model.transfers.Count;
+ }
+ // TODO - handle multiple pages of transfers
+
+ for (int i = 0; i < model.transfers.Count && i < rows - 4; ++i) {
+ addRow(sb, model.transfers[i], timeToWait[i], (cursorTransfer == i));
+ }
+ menu = sb.ToString();
+ cursorMoved = false;
+ }
+ return menu;
+ }
+
+ public void PageActive(bool pageActive, int pageNumber)
+ {
+ if (pageActive) {
+ loader.OnDisplayOpened();
+ loader.TryStartLoad(model.origin, null, null, null);
+ } else {
+ loader.OnDisplayClosed();
+ }
+ }
+
+ private string centerString(string val, int columns, char padding = ' ')
+ {
+ int numPads = columns - val.Length;
+ return val.PadLeft(columns - numPads/2, padding).PadRight(columns, padding);
+ }
+
+ public void ButtonClick(int buttonNumber)
+ {
+ DbgFmt("ButtonClick: {0}", buttonNumber);
+ activeButton = buttonNumber;
+
+ if (activeButton == buttonUp) {
+ --cursorTransfer;
+ cursorMoved = true;
+ } else if (activeButton == buttonDown) {
+ ++cursorTransfer;
+ cursorMoved = true;
+ } else if (activeButton == buttonEnter) {
+ model.transfers[cursorTransfer].CreateManeuvers();
+ } else if (activeButton == buttonEsc) {
+ ClearManeuverNodes();
+ } else if (activeButton == buttonHome) {
+ model.transfers[cursorTransfer].WarpToBurn();
+ }
+ }
+
+ public void ButtonRelease(int buttonNumber)
+ {
+ DbgFmt("ButtonRelease: {0}", buttonNumber);
+ activeButton = null;
+ }
+
+ private bool Refresh()
+ {
+ double now = Math.Floor(Planetarium.GetUniversalTime());
+ if (lastUniversalTime != now) {
+ timeToWait = new List();
+ for (int i = 0; i < model.transfers.Count; ++i) {
+
+ if (model.transfers[i].ejectionBurn != null) {
+ timeToWait.Add(new DateTimeParts(model.transfers[i].ejectionBurn.atTime - Planetarium.GetUniversalTime()));
+ } else {
+ timeToWait.Add(new DateTimeParts(0));
+ }
+
+ }
+ lastUniversalTime = now;
+ return true;
+ }
+ return false;
+ }
+
+ }
+
+}
diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs
index fb41a4e..66ee228 100644
--- a/src/Properties/AssemblyInfo.cs
+++ b/src/Properties/AssemblyInfo.cs
@@ -31,5 +31,5 @@
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
-[assembly: AssemblyVersion("0.5.1.0")]
-[assembly: AssemblyFileVersion("0.5.1.0")]
+[assembly: AssemblyVersion("0.5.2.0")]
+[assembly: AssemblyFileVersion("0.5.2.0")]
diff --git a/src/TransferModel.cs b/src/TransferModel.cs
index 6b0c71f..f8deca3 100644
--- a/src/TransferModel.cs
+++ b/src/TransferModel.cs
@@ -41,7 +41,7 @@ public TransferModel(ITargetable org, ITargetable dest)
/// True if the transfer portion of this trajectory is retrograde, false otherwise.
/// So for a retrograde Kerbin orbit, this is true for Mun and false for Duna.
///
- public bool retrogradeTransfer { get; private set; }
+ public bool retrogradeTransfer { get; private set; }
///
/// The body we're transferring from.
@@ -86,6 +86,11 @@ private BurnModel GenerateEjectionBurn(Orbit currentOrbit)
// Sanity check just in case something unexpected happens.
return null;
+ } else if (destination.GetOrbit().eccentricity > 1) {
+ DbgFmt("{0} is on an escape trajectory; bailing", TheName(destination));
+
+ return null;
+
} else if (Landed) {
// TODO - Launch to orbit
@@ -305,7 +310,8 @@ public void CalculatePlaneChangeBurn()
{
if (FlightGlobals.ActiveVessel?.patchedConicSolver?.maneuverNodes != null
&& transferDestination != null
- && transferParent != null) {
+ && transferParent != null
+ && destination.GetOrbit().eccentricity < 1) {
bool ejectionAlreadyActive = false;
@@ -412,42 +418,121 @@ public bool HaveEncounter()
return false;
}
- /// Returns true if UI needs an update
- public bool Refresh()
+ ///
+ /// Check whether the user opened any manuever node editing gizmos since the last tick.
+ /// There doesn't seem to be event-based notification for this, so we just have to poll.
+ ///
+ public void CheckIfNodesDisappeared()
+ {
+ ejectionBurn?.CheckIfNodeDisappeared();
+ planeChangeBurn?.CheckIfNodeDisappeared();
+ }
+
+ ///
+ /// Turn this transfer's burns into user visible maneuver nodes.
+ /// This is the behavior for the maneuver node icon.
+ ///
+ public void CreateManeuvers()
{
- if (ejectionBurn != null) {
- if (ejectionBurn.atTime < Planetarium.GetUniversalTime()) {
- CalculateEjectionBurn();
+ if (FlightGlobals.ActiveVessel != null) {
- // Apply the same filters we do everywhere else to suppress phantom nodes
- if (Settings.Instance.GeneratePlaneChangeBurns
- && Settings.Instance.AddPlaneChangeDeltaV) {
+ // Remove all maneuver nodes because they'd conflict with the ones we're about to add
+ ClearManeuverNodes();
- try {
- CalculatePlaneChangeBurn();
- } catch (Exception ex) {
- DbgExc("Problem with plane change at expiration", ex);
- ClearManeuverNodes();
- }
+ if (Settings.Instance.AutoTargetDestination) {
+ // Switch to target mode, targeting the destination body
+ FlightGlobals.fetch.SetVesselTarget(destination);
+ }
+
+ // Create a maneuver node for the ejection burn
+ ejectionBurn.ToActiveManeuver();
+
+ if (Settings.Instance.GeneratePlaneChangeBurns) {
+ if (planeChangeBurn == null) {
+ DbgFmt("Calculating plane change on the fly");
+ CalculatePlaneChangeBurn();
+ }
+
+ if (planeChangeBurn != null) {
+ planeChangeBurn.ToActiveManeuver();
+ } else {
+ DbgFmt("No plane change found");
}
- return true;
} else {
- return false;
+ DbgFmt("Plane changes disabled");
+ }
+
+ if (Settings.Instance.AutoEditEjectionNode) {
+ // Open the initial node for fine tuning
+ ejectionBurn.EditNode();
+ } else if (Settings.Instance.AutoEditPlaneChangeNode) {
+ if (planeChangeBurn != null) {
+ planeChangeBurn.EditNode();
+ }
+ }
+
+ if (Settings.Instance.AutoFocusDestination) {
+ if (HaveEncounter()) {
+ // Move the map to the target for fine-tuning if we have an encounter
+ FocusMap(destination);
+ } else if (transferParent != null) {
+ // Otherwise focus on the parent of the transfer orbit so we can get an encounter
+ // Try to explain why this is happening with a screen message
+ ScreenFmt("Adjust maneuvers to establish encounter");
+ FocusMap(transferParent, transferDestination);
+ }
+ }
+
+ if (Settings.Instance.AutoSetSAS
+ && FlightGlobals.ActiveVessel != null
+ && FlightGlobals.ActiveVessel.Autopilot.CanSetMode(VesselAutopilot.AutopilotMode.Maneuver)) {
+ // The API for SAS is ... peculiar.
+ // http://forum.kerbalspaceprogram.com/index.php?/topic/153420-enabledisable-autopilot/
+ try {
+ if (FlightGlobals.ActiveVessel.Autopilot.Enabled) {
+ FlightGlobals.ActiveVessel.Autopilot.SetMode(VesselAutopilot.AutopilotMode.Maneuver);
+ } else {
+ DbgFmt("Not enabled, trying to enable");
+ FlightGlobals.ActiveVessel.ActionGroups.SetGroup(KSPActionGroup.SAS, true);
+ FlightGlobals.ActiveVessel.Autopilot.Enable(VesselAutopilot.AutopilotMode.Maneuver);
+ }
+ } catch (Exception ex) {
+ DbgExc("Problem setting SAS to maneuver mode", ex);
+ }
}
- } else {
- return false;
}
}
///
- /// Check whether the user opened any manuever node editing gizmos since the last tick.
- /// There doesn't seem to be event-based notification for this, so we just have to poll.
+ /// Warp to (near) the burn.
+ /// Since you usually need to start burning before the actual node,
+ /// we use some simple padding logic to determine how far to warp.
+ /// If you're more than five minutes from the burn, then we warp
+ /// to that five minute mark. This should allow for most of the long burns.
+ /// If you're closer than five minutes from the burn, then we warp
+ /// right up to the moment of the actual burn.
+ /// If you're _already_ warping, cancel the warp (suggested by Kottabos).
///
- public void CheckIfNodesDisappeared()
+ public void WarpToBurn()
{
- ejectionBurn?.CheckIfNodeDisappeared();
- planeChangeBurn?.CheckIfNodeDisappeared();
+ if (TimeWarp.CurrentRate > 1) {
+ DbgFmt("Warp button clicked while already in warp, cancelling warp");
+ TimeWarp.fetch?.CancelAutoWarp();
+ TimeWarp.SetRate(0, false);
+ } else {
+ DbgFmt("Attempting to warp to burn from {0} to {1}", Planetarium.GetUniversalTime(), ejectionBurn.atTime);
+ if (Planetarium.GetUniversalTime() < ejectionBurn.atTime - BURN_PADDING ) {
+ DbgFmt("Warping to burn minus offset");
+ TimeWarp.fetch.WarpTo(ejectionBurn.atTime - BURN_PADDING);
+ } else if (Planetarium.GetUniversalTime() < ejectionBurn.atTime) {
+ DbgFmt("Already within offset; warping to burn");
+ TimeWarp.fetch.WarpTo(ejectionBurn.atTime);
+ } else {
+ DbgFmt("Can't warp to the past!");
+ }
+ }
}
+
}
}
diff --git a/src/TransferView.cs b/src/TransferView.cs
index e782333..119ca33 100644
--- a/src/TransferView.cs
+++ b/src/TransferView.cs
@@ -86,12 +86,12 @@ private void CreateLayout()
case ContentEnum.CreateManeuverNodeButton:
AddChild(iconButton(maneuverIcon,
- col.contentStyle, "Create maneuver", CreateManeuvers));
+ col.contentStyle, "Create maneuver", model.CreateManeuvers));
break;
case ContentEnum.WarpToBurnButton:
AddChild(iconButton(warpIcon,
- col.contentStyle, "Warp to window", WarpToBurn));
+ col.contentStyle, "Warp to window", model.WarpToBurn));
break;
}
@@ -106,15 +106,9 @@ private void CreateLayout()
///
public bool Refresh()
{
- bool modelNeedsUIUpdate = model.Refresh();
double now = Math.Floor(Planetarium.GetUniversalTime());
- if (modelNeedsUIUpdate) {
- // We have a new ejection burn, so we might need a totally new view
- // because the sort could be wrong now.
- resetCallback();
- return true;
- } else if (lastUniversalTime != now && model.ejectionBurn != null) {
+ if (lastUniversalTime != now && model.ejectionBurn != null) {
timeToWait = new DateTimeParts(model.ejectionBurn.atTime - Planetarium.GetUniversalTime());
lastUniversalTime = now;
return true;
@@ -124,13 +118,19 @@ public bool Refresh()
private const string LoadingText = "---";
+ private bool showLoadingText {
+ get {
+ return timeToWait == null || model.ejectionBurn.atTime < Planetarium.GetUniversalTime();
+ }
+ }
+
///
/// String representing years till burn.
///
public string getYearValue()
{
Refresh();
- if (timeToWait == null) {
+ if (showLoadingText) {
return LoadingText;
} else {
return TimePieceString("{0}y", timeToWait.years, timeToWait.needYears);
@@ -143,7 +143,7 @@ public string getYearValue()
public string getDayValue()
{
Refresh();
- if (timeToWait == null) {
+ if (showLoadingText) {
return LoadingText;
} else {
return TimePieceString("{0}d", timeToWait.days, timeToWait.needDays);
@@ -156,7 +156,7 @@ public string getDayValue()
public string getHourValue()
{
Refresh();
- if (timeToWait == null) {
+ if (showLoadingText) {
return LoadingText;
} else {
return TimePieceString("{0}h", timeToWait.hours, timeToWait.needHours);
@@ -169,7 +169,7 @@ public string getHourValue()
public string getMinuteValue()
{
Refresh();
- if (timeToWait == null) {
+ if (showLoadingText) {
return LoadingText;
} else {
return TimePieceString("{0}m", timeToWait.minutes, timeToWait.needMinutes);
@@ -182,7 +182,7 @@ public string getMinuteValue()
public string getSecondValue()
{
Refresh();
- if (timeToWait == null) {
+ if (showLoadingText) {
return LoadingText;
} else {
return TimePieceString("{0}s", timeToWait.seconds, true);
@@ -209,106 +209,6 @@ public string getDeltaV()
}
}
- ///
- /// Turn this transfer's burns into user visible maneuver nodes.
- /// This is the behavior for the maneuver node icon.
- ///
- public void CreateManeuvers()
- {
- if (FlightGlobals.ActiveVessel != null) {
-
- // Remove all maneuver nodes because they'd conflict with the ones we're about to add
- ClearManeuverNodes();
-
- if (Settings.Instance.AutoTargetDestination) {
- // Switch to target mode, targeting the destination body
- FlightGlobals.fetch.SetVesselTarget(model.destination);
- }
-
- // Create a maneuver node for the ejection burn
- model.ejectionBurn.ToActiveManeuver();
-
- if (Settings.Instance.GeneratePlaneChangeBurns) {
- if (model.planeChangeBurn == null) {
- DbgFmt("Calculating plane change on the fly");
- model.CalculatePlaneChangeBurn();
- }
-
- if (model.planeChangeBurn != null) {
- model.planeChangeBurn.ToActiveManeuver();
- } else {
- DbgFmt("No plane change found");
- }
- } else {
- DbgFmt("Plane changes disabled");
- }
-
- if (Settings.Instance.AutoEditEjectionNode) {
- // Open the initial node for fine tuning
- model.ejectionBurn.EditNode();
- } else if (Settings.Instance.AutoEditPlaneChangeNode) {
- if (model.planeChangeBurn != null) {
- model.planeChangeBurn.EditNode();
- }
- }
-
- if (Settings.Instance.AutoFocusDestination) {
- if (model.HaveEncounter()) {
- // Move the map to the target for fine-tuning if we have an encounter
- FocusMap(model.destination);
- } else if (model.transferParent != null) {
- // Otherwise focus on the parent of the transfer orbit so we can get an encounter
- // Try to explain why this is happening with a screen message
- ScreenFmt("Adjust maneuvers to establish encounter");
- FocusMap(model.transferParent, model.transferDestination);
- }
- }
-
- if (Settings.Instance.AutoSetSAS
- && FlightGlobals.ActiveVessel != null
- && FlightGlobals.ActiveVessel.Autopilot.CanSetMode(VesselAutopilot.AutopilotMode.Maneuver)) {
- try {
- if (FlightGlobals.ActiveVessel.Autopilot.Enabled) {
- FlightGlobals.ActiveVessel.Autopilot.SetMode(VesselAutopilot.AutopilotMode.Maneuver);
- } else {
- FlightGlobals.ActiveVessel.Autopilot.Enable(VesselAutopilot.AutopilotMode.Maneuver);
- }
- } catch (Exception ex) {
- DbgExc("Problem setting SAS to maneuver mode", ex);
- }
- }
- }
- }
-
- ///
- /// Warp to (near) the burn.
- /// Since you usually need to start burning before the actual node,
- /// we use some simple padding logic to determine how far to warp.
- /// If you're more than five minutes from the burn, then we warp
- /// to that five minute mark. This should allow for most of the long burns.
- /// If you're closer than five minutes from the burn, then we warp
- /// right up to the moment of the actual burn.
- /// If you're _already_ warping, cancel the warp (suggested by Kottabos).
- ///
- public void WarpToBurn()
- {
- if (TimeWarp.CurrentRate > 1) {
- DbgFmt("Warp button clicked while already in warp, cancelling warp");
- TimeWarp.fetch?.CancelAutoWarp();
- TimeWarp.SetRate(0, false);
- } else {
- DbgFmt("Attempting to warp to burn from {0} to {1}", Planetarium.GetUniversalTime(), model.ejectionBurn.atTime);
- if (Planetarium.GetUniversalTime() < model.ejectionBurn.atTime - BURN_PADDING ) {
- DbgFmt("Warping to burn minus offset");
- TimeWarp.fetch.WarpTo(model.ejectionBurn.atTime - BURN_PADDING);
- } else if (Planetarium.GetUniversalTime() < model.ejectionBurn.atTime) {
- DbgFmt("Already within offset; warping to burn");
- TimeWarp.fetch.WarpTo(model.ejectionBurn.atTime);
- } else {
- DbgFmt("Can't warp to the past!");
- }
- }
- }
}
}
diff --git a/src/ViewTools.cs b/src/ViewTools.cs
index 231859a..1cad96d 100644
--- a/src/ViewTools.cs
+++ b/src/ViewTools.cs
@@ -571,6 +571,11 @@ public class ColumnDefinition {
/// Sort order to use when the user clicks the header.
///
public SortEnum sortKey { get; set; }
+
+ ///
+ /// How wide the column is when rendering in a fixed-width font text screen.
+ ///
+ public int monospaceWidth { get; set; }
}
///
@@ -584,7 +589,8 @@ public class ColumnDefinition {
headerStyle = leftHdrStyle,
contentStyle = planetStyle,
content = ContentEnum.PlanetName,
- sortKey = SortEnum.Position
+ sortKey = SortEnum.Position,
+ monospaceWidth = 7
}, new ColumnDefinition() {
header = "Time Till Burn",
width = 30,
@@ -592,35 +598,40 @@ public class ColumnDefinition {
headerStyle = midHdrStyle,
contentStyle = numberStyle,
content = ContentEnum.YearsTillBurn,
- sortKey = SortEnum.Time
+ sortKey = SortEnum.Time,
+ monospaceWidth = 4
}, new ColumnDefinition() {
header = "",
width = 30,
headerColSpan = 0,
headerStyle = rightHdrStyle,
contentStyle = numberStyle,
- content = ContentEnum.DaysTillBurn
+ content = ContentEnum.DaysTillBurn,
+ monospaceWidth = 4
}, new ColumnDefinition() {
header = "",
width = 20,
headerColSpan = 0,
headerStyle = rightHdrStyle,
contentStyle = numberStyle,
- content = ContentEnum.HoursTillBurn
+ content = ContentEnum.HoursTillBurn,
+ monospaceWidth = 2
}, new ColumnDefinition() {
header = "",
width = 25,
headerColSpan = 0,
headerStyle = rightHdrStyle,
contentStyle = numberStyle,
- content = ContentEnum.MinutesTillBurn
+ content = ContentEnum.MinutesTillBurn,
+ monospaceWidth = 3
}, new ColumnDefinition() {
header = "",
width = 25,
headerColSpan = 0,
headerStyle = rightHdrStyle,
contentStyle = numberStyle,
- content = ContentEnum.SecondsTillBurn
+ content = ContentEnum.SecondsTillBurn,
+ monospaceWidth = 3,
}, new ColumnDefinition() {
header = "Δv",
width = 60,
@@ -628,7 +639,8 @@ public class ColumnDefinition {
headerStyle = rightHdrStyle,
contentStyle = numberStyle,
content = ContentEnum.DeltaV,
- sortKey = SortEnum.DeltaV
+ sortKey = SortEnum.DeltaV,
+ monospaceWidth = 9
}, new ColumnDefinition() {
header = "",
width = buttonIconWidth,
@@ -638,13 +650,15 @@ public class ColumnDefinition {
content = ContentEnum.CreateManeuverNodeButton,
vesselSpecific = true,
requiresPatchedConics = true,
+ monospaceWidth = 0,
}, new ColumnDefinition() {
header = "",
width = buttonIconWidth,
headerColSpan = 1,
headerStyle = rightHdrStyle,
contentStyle = warpStyle,
- content = ContentEnum.WarpToBurnButton
+ content = ContentEnum.WarpToBurnButton,
+ monospaceWidth = 0
},
};