diff --git a/README.md b/README.md index cab8be8..cb1844f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ Click the wrench to open and close the settings panel: - Useful numbers for launches/landed - Return-to-LKO burns from Mun and Minmus +- Allow low-inclination retrograde orbits +- Represent starting point with ITargetable +- Split CalculateEjectionBurn into transfer versus ejection functions ## Building diff --git a/src/AstrogationModel.cs b/src/AstrogationModel.cs index 7e7862a..ca3feb7 100644 --- a/src/AstrogationModel.cs +++ b/src/AstrogationModel.cs @@ -145,6 +145,7 @@ private void CreateTransfers(CelestialBody body, Vessel vessel) DbgFmt("Fabricating transfers"); bool foundTarget = false; + int discoveryOrder = 0; CelestialBody origin = StartBody(body, vessel); @@ -162,7 +163,7 @@ private void CreateTransfers(CelestialBody body, Vessel vessel) CelestialBody satellite = b.orbitingBodies[i]; if (satellite != toSkip) { DbgFmt("Plotting transfer to {0}", satellite.theName); - transfers.Add(new TransferModel(origin, satellite, vessel)); + transfers.Add(new TransferModel(origin, satellite, vessel, ++discoveryOrder)); DbgFmt("Finalized transfer to {0}", satellite.theName); if (satellite == FlightGlobals.fetch.VesselTarget as CelestialBody) { @@ -180,7 +181,7 @@ private void CreateTransfers(CelestialBody body, Vessel vessel) if (!foundTarget && FlightGlobals.ActiveVessel != null && FlightGlobals.fetch.VesselTarget != null) { - transfers.Insert(0, new TransferModel(origin, FlightGlobals.fetch.VesselTarget, vessel)); + transfers.Insert(0, new TransferModel(origin, FlightGlobals.fetch.VesselTarget, vessel, -1)); } DbgFmt("Shipping completed transfers"); diff --git a/src/AstrogationView.cs b/src/AstrogationView.cs index 82ffb25..bd0bbd4 100644 --- a/src/AstrogationView.cs +++ b/src/AstrogationView.cs @@ -78,6 +78,12 @@ private bool ShowSettings { } } + private void toggleSettingsVisible() + { + ShowSettings = !ShowSettings; + resetCallback(); + } + /// /// The user-facing name for this mod. /// Use Astrogator.Name for filenames, internal representations, CKAN, etc. @@ -87,27 +93,37 @@ private bool ShowSettings { /// /// UI object representing the top row of the table /// - private static DialogGUIHorizontalLayout ColumnHeaders { get; set; } + private DialogGUIHorizontalLayout ColumnHeaders { get; set; } - private void toggleSettingsVisible() + private string columnSortIndicator(ColumnDefinition col) { - ShowSettings = !ShowSettings; - resetCallback(); + return col.sortKey != Settings.Instance.TransferSort ? "" + : Settings.Instance.DescendingSort ? " ↓" + : " ↑"; } private void createHeaders() { - if (ColumnHeaders == null) { - ColumnHeaders = new DialogGUIHorizontalLayout(); - for (int i = 0; i < Columns.Length; ++i) { - ColumnDefinition col = Columns[i]; - // Skip columns that require an active vessel if we don't have one - if (!col.vesselSpecific || model.vessel != null) { - int width = 0; - for (int span = 0; span < col.headerColSpan; ++span) { - width += Columns[i + span].width; - } - if (width > 0) { + ColumnHeaders = new DialogGUIHorizontalLayout(); + for (int i = 0; i < Columns.Length; ++i) { + ColumnDefinition col = Columns[i]; + // Skip columns that require an active vessel if we don't have one + if (!col.vesselSpecific || model.vessel != null) { + float width = 0; + for (int span = 0; span < col.headerColSpan; ++span) { + width += Columns[i + span].width; + } + if (width > 0) { + // Add in the spacing gaps that got left out from colspanning + width += (col.headerColSpan - 1) * spacing; + if (col.header != "") { + ColumnHeaders.AddChild(headerButton( + col.header + columnSortIndicator(col), + col.headerStyle, "Sort", width, rowHeight, () => { + SortClicked(col.sortKey); + } + )); + } else { ColumnHeaders.AddChild(LabelWithStyleAndSize(col.header, col.headerStyle, width, rowHeight)); } } @@ -116,10 +132,56 @@ private void createHeaders() AddChild(ColumnHeaders); } + private void SortClicked(SortEnum which) + { + if (Settings.Instance.TransferSort == which) { + Settings.Instance.DescendingSort = !Settings.Instance.DescendingSort; + } else { + Settings.Instance.TransferSort = which; + Settings.Instance.DescendingSort = false; + } + resetCallback(); + } + + private List SortTransfers(AstrogationModel m, SortEnum how, bool descend) + { + List transfers = new List(m.transfers); + switch (how) { + case SortEnum.Name: + transfers.Sort((a, b) => + a?.destination?.GetName().CompareTo(b?.destination?.GetName()) ?? 0); + break; + case SortEnum.Position: + transfers.Sort((a, b) => + a?.DiscoveryOrder.CompareTo(b?.DiscoveryOrder) ?? 0); + break; + case SortEnum.Time: + transfers.Sort((a, b) => + a?.ejectionBurn?.atTime.CompareTo(b?.ejectionBurn?.atTime) ?? 0); + break; + case SortEnum.DeltaV: + transfers.Sort((a, b) => + a?.ejectionBurn?.totalDeltaV.CompareTo(b?.ejectionBurn?.totalDeltaV) ?? 0); + break; + default: + DbgFmt("Bad sort argument: {0}", how.ToString()); + break; + } + if (descend) { + transfers.Reverse(); + } + return transfers; + } + private void createRows() { - for (int i = 0; i < model.transfers.Count; ++i) { - AddChild(new TransferView(model.transfers[i])); + List transfers = SortTransfers( + model, + Settings.Instance.TransferSort, + Settings.Instance.DescendingSort + ); + for (int i = 0; i < transfers.Count; ++i) { + AddChild(new TransferView(transfers[i])); } } diff --git a/src/Astrogator.cs b/src/Astrogator.cs index f8edda3..a513e14 100644 --- a/src/Astrogator.cs +++ b/src/Astrogator.cs @@ -412,8 +412,10 @@ private bool SituationChanged() private void OnSituationChanged() { - StartLoadingModel(FlightGlobals.ActiveVessel?.mainBody, FlightGlobals.ActiveVessel); - ResetView(); + if (model != null && view != null) { + StartLoadingModel(FlightGlobals.ActiveVessel?.mainBody, FlightGlobals.ActiveVessel); + ResetView(); + } } /// @@ -423,9 +425,11 @@ public void SOIChanged(CelestialBody newBody) { DbgFmt("Entered {0}'s sphere of influence", newBody.theName); - // The old list no longer applies because reachable bodies depend on current SOI - StartLoadingModel(newBody, FlightGlobals.ActiveVessel); - ResetView(); + if (model != null && view != null) { + // The old list no longer applies because reachable bodies depend on current SOI + StartLoadingModel(newBody, FlightGlobals.ActiveVessel); + ResetView(); + } } /// diff --git a/src/Settings.cs b/src/Settings.cs index 5668fa2..3cf00bd 100644 --- a/src/Settings.cs +++ b/src/Settings.cs @@ -47,6 +47,8 @@ private const string MainWindowVisibleKey = "MainWindowVisible", ShowSettingsKey = "ShowSettings", + TransferSortKey = "TransferSort", + DescendingSortKey = "DescendingSort", GeneratePlaneChangeBurnsKey = "GeneratePlaneChangeBurns", AddPlaneChangeDeltaVKey = "AddPlaneChangeDeltaV", @@ -275,6 +277,45 @@ public bool AutoEditPlaneChangeNode { } } + /// + /// How to sort the table. + /// + public SortEnum TransferSort { + get { + SortEnum ts = SortEnum.Position; + string savedValue = ""; + if (config.TryGetValue(TransferSortKey, ref savedValue)) { + ts = ParseEnum(savedValue, SortEnum.Position); + } + return ts; + } + set { + if (config.HasValue(TransferSortKey)) { + config.SetValue(TransferSortKey, value.ToString()); + } else { + config.AddValue(TransferSortKey, value.ToString()); + } + } + } + + /// + /// True if the sort should be largest value at the top, false otherwise. + /// + public bool DescendingSort { + get { + bool descend = false; + config.TryGetValue(DescendingSortKey, ref descend); + return descend; + } + set { + if (config.HasValue(DescendingSortKey)) { + config.SetValue(DescendingSortKey, value); + } else { + config.AddValue(DescendingSortKey, value); + } + } + } + } } diff --git a/src/TransferModel.cs b/src/TransferModel.cs index 5aee7eb..08834be 100644 --- a/src/TransferModel.cs +++ b/src/TransferModel.cs @@ -15,11 +15,12 @@ public class TransferModel { /// /// Construct a model object. /// - public TransferModel(CelestialBody org, ITargetable dest, Vessel v) + public TransferModel(CelestialBody org, ITargetable dest, Vessel v, int order) { origin = org; destination = dest; vessel = v; + DiscoveryOrder = order; } /// @@ -52,6 +53,11 @@ public TransferModel(CelestialBody org, ITargetable dest, Vessel v) /// public BurnModel planeChangeBurn { get; private set; } + /// + /// Number representing the position of this row when sorted by position. + /// + public int DiscoveryOrder { get; private set; } + private BurnModel GenerateEjectionBurn(Orbit currentOrbit) { if (currentOrbit == null || destination == null) { diff --git a/src/ViewTools.cs b/src/ViewTools.cs index 6b7b7a8..1d84c0d 100644 --- a/src/ViewTools.cs +++ b/src/ViewTools.cs @@ -5,6 +5,8 @@ namespace Astrogator { + using static DebugTools; + /// Anything UI-related that needs to be used from multiple places. public static class ViewTools { @@ -32,6 +34,25 @@ public static string FilePath(string filename) return string.Format("GameData/{0}/{1}", Astrogator.Name, filename); } + /// + /// Parse a string into an enum for Settings + /// Inverse of Enum.ToString() + /// + /// String from the settings + /// Default to use if can't match to any value from the enum + /// + /// Enum value matching the string, if any + /// + public static T ParseEnum(string val, T defaultVal) + { + try { + return (T) Enum.Parse(typeof(T), val, true); + } catch (Exception ex) { + DbgExc("Problem parsing enum", ex); + return defaultVal; + } + } + /// /// The icon to show for this mod in the app launcher. /// @@ -92,7 +113,13 @@ public static Sprite SolidColorSprite(Color c) /// /// Black image with 50% opacity. /// - public static Sprite halfTransparentBlack = SolidColorSprite(new Color(0.0f, 0.0f, 0.0f, 0.5f)); + public static Sprite halfTransparentBlack = SolidColorSprite(new Color(0f, 0f, 0f, 0.5f)); + + /// + /// Completely transparent sprite so we can use buttons for the headers + /// without the default button graphic. + /// + public static Sprite transparent = SolidColorSprite(new Color(0f, 0f, 0f, 0f)); /// /// Backgrounds and text colors for the tooltip and main window. @@ -106,6 +133,7 @@ public static Sprite SolidColorSprite(Color c) /// Text color for table headers. /// public static UIStyleState headingFont = new UIStyleState() { + background = transparent, textColor = Color.HSVToRGB(0.3f, 0.8f, 0.8f) }; @@ -372,24 +400,156 @@ public static Sprite SolidColorSprite(Color c) toggle = UISkinManager.defaultSkin.toggle, }; + /// + /// Types of columns in our table. + /// + public enum ContentEnum { + + /// + /// Left most column, left aligned, sort of a header, contains planet names + /// + PlanetName, + + /// + /// First time column + /// + YearsTillBurn, + + /// + /// Second time columnm + /// + DaysTillBurn, + + /// + /// Third time column + /// + HoursTillBurn, + + /// + /// Fourth time column + /// + MinutesTillBurn, + + /// + /// Fifth time column + /// + SecondsTillBurn, + + /// + /// Delta V column + /// + DeltaV, + + /// + /// Maneuver node creation button column + /// + CreateManeuverNodeButton, + + /// + /// Warp button column + /// + WarpToBurnButton, + } + + /// + /// A type defining the different sort orders available. + /// Can't be the same as the column list, because we have + /// four different columns for time data. + /// + public enum SortEnum { + /// + /// Sort by discovery order; first the satellites of the current + /// body in inner->outer order, then satellites of its parent, etc. + /// + Position, + + /// + /// Sort by name (currently not available in UI) + /// + Name, + + /// + /// Sort by time till burn + /// + Time, + + /// + /// Sort by delta V + /// + DeltaV + } + + /// + /// Structure defining the properties of a column of our table. + /// + public class ColumnDefinition { + + /// + /// The string to display at the top of the column + /// + public string header { get; set; } + + /// + /// Width of the column + /// + public int width { get; set; } + + /// + /// Number of cells occupied horizontally by the header + /// + public int headerColSpan { get; set; } + + /// + /// Font, color, and alignment of the header + /// + public UIStyle headerStyle { get; set; } + + /// + /// Font, color, and alignment of the normal content + /// + public UIStyle contentStyle { get; set; } + + /// + /// How to generate the content for this column + /// + public ContentEnum content { get; set; } + + /// + /// True to hide this column when there's no active vessel (tracking station, KSC) + /// + public bool vesselSpecific { get; set; } + + /// + /// True to hide this column if maneuver nodes aren't available in this game mode. + /// + public bool requiresPatchedConics { get; set; } + + /// + /// Sort order to use when the user clicks the header. + /// + public SortEnum sortKey { get; set; } + } + /// /// Columns for our table. /// public static ColumnDefinition[] Columns = new ColumnDefinition[] { new ColumnDefinition() { - header = "", - width = 45, + header = "Transfer", + width = 60, headerColSpan = 1, headerStyle = leftHdrStyle, contentStyle = planetStyle, - content = ContentEnum.PlanetName + content = ContentEnum.PlanetName, + sortKey = SortEnum.Position }, new ColumnDefinition() { header = "Time Till Burn", width = 30, headerColSpan = 5, headerStyle = midHdrStyle, contentStyle = numberStyle, - content = ContentEnum.YearsTillBurn + content = ContentEnum.YearsTillBurn, + sortKey = SortEnum.Time }, new ColumnDefinition() { header = "", width = 30, @@ -424,7 +584,8 @@ public static Sprite SolidColorSprite(Color c) headerColSpan = 1, headerStyle = rightHdrStyle, contentStyle = numberStyle, - content = ContentEnum.DeltaV + content = ContentEnum.DeltaV, + sortKey = SortEnum.DeltaV }, new ColumnDefinition() { header = "", width = buttonIconWidth, @@ -554,6 +715,27 @@ public static string TimePieceString(string fmt, double val, bool forceShow = fa } } + /// + /// Create a button that looks like a label + /// + /// String to display + /// Style to use for the text + /// Tooltip to use (not currently visible) + /// Horizontal space to take up + /// Vertical space to take up + /// Function to call when the user clicks the button + /// + /// Button with the given properties + /// + public static DialogGUIButton headerButton(string text, UIStyle style, string tooltip, float width, float height, Callback cb) + { + // The 'transparent' Sprite makes the default button borders go away + return new DialogGUIButton(transparent, text, cb, width, height, false) { + guiStyle = style, + tooltipText = tooltip + }; + } + /// /// A button with parameterized icon, tooltip, and callback. /// @@ -570,103 +752,6 @@ public static DialogGUIButton iconButton(Sprite icon, UIStyle style, string tool } } - /// - /// Types of columns in our table. - /// - public enum ContentEnum { - - /// - /// Left most column, left aligned, sort of a header, contains planet names - /// - PlanetName, - - /// - /// First time column - /// - YearsTillBurn, - - /// - /// Second time columnm - /// - DaysTillBurn, - - /// - /// Third time column - /// - HoursTillBurn, - - /// - /// Fourth time column - /// - MinutesTillBurn, - - /// - /// Fifth time column - /// - SecondsTillBurn, - - /// - /// Delta V column - /// - DeltaV, - - /// - /// Maneuver node creation button column - /// - CreateManeuverNodeButton, - - /// - /// Warp button column - /// - WarpToBurnButton, - } - - /// - /// Structure defining the properties of a column of our table. - /// - public class ColumnDefinition { - - /// - /// The string to display at the top of the column - /// - public string header { get; set; } - - /// - /// Width of the column - /// - public int width { get; set; } - - /// - /// Number of cells occupied horizontally by the header - /// - public int headerColSpan { get; set; } - - /// - /// Font, color, and alignment of the header - /// - public UIStyle headerStyle { get; set; } - - /// - /// Font, color, and alignment of the normal content - /// - public UIStyle contentStyle { get; set; } - - /// - /// How to generate the content for this column - /// - public ContentEnum content { get; set; } - - /// - /// True to hide this column when there's no active vessel (tracking station, KSC) - /// - public bool vesselSpecific { get; set; } - - /// - /// True to hide this column if maneuver nodes aren't available in this game mode. - /// - public bool requiresPatchedConics { get; set; } - } - /// /// An object that holds the parts of a time span, /// broken down into years, days, hours, minutes, and seconds.