diff --git a/Source/Orts.Formats.Msts/TrackDatabaseFile.cs b/Source/Orts.Formats.Msts/TrackDatabaseFile.cs
index 5ab0d0216f..526fee9a9e 100644
--- a/Source/Orts.Formats.Msts/TrackDatabaseFile.cs
+++ b/Source/Orts.Formats.Msts/TrackDatabaseFile.cs
@@ -164,15 +164,182 @@ public int TrackNodesIndexOf(TrackNode targetTN)
throw new InvalidOperationException("Program Bug: Can't Find Track Node");
}
+ // Note on how vector nodes connect:
+ // A Vector Node has a direction determined by the order of the vector sections.
+ // - Pin[0] is at the start (section[0]), Pin[1] is at the end (section[n-1]).
+ // - Pin Direction = 0 identifies that the vector node is connected to the trailing (out) side of a junction.
+ // - Pin Direction = 1 identifies that the vector node is connected to the leading (in) side of a junction.
+ // A Junction Node's direction is from the leading (in) side to the trailing (out) side.
+ // - A typical Junction Node has one in Pin (at index 0) and two out Pins. Either out pin may be the straight
+ // (primary) path.
+ // - Pin Direction = 0 indicates that the connected Vector Node is oriented towards the junction (ie. the
+ // last vector section connects to the junction).
+ // - Pin Direction = 1 indicates that the connected Vector Node is oriented away from the junction (ie. the
+ // first vector section connects to the junction).
+
+ ///
+ /// Get the index of the vector node that precedes the specified vector node.
+ /// Expects to traverse a junction node to find the preceding vector node.
+ ///
+ /// The index of the incoming vector node, and the connected end if it.
+ public (int, int) GetIncomingVectorNodeIndex(TrackNode vectorNode, int direction)
+ {
+ const int connectedToTrailingSide = 0; // re: direction of vector node link to junction
+
+ int inPinIdx = direction == 0 ? 0 : 1;
+ int incomingVectorNodeIdx = -1; // error
+ int incomingVectorNodeEnd = 0;
+
+ if (vectorNode == null || vectorNode.TrVectorNode == null || vectorNode.Inpins != 1)
+ {
+ Debug.Print("GetIncomingVectorNodeIndex() ERROR: source node is not a valid vector node");
+ return (-1, 0); // error
+ }
+
+ int junctionNodeIdx = vectorNode.TrPins[inPinIdx].Link;
+ if (junctionNodeIdx <= 0 || junctionNodeIdx >= TrackNodes.Length)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() ERROR: first incoming node index {0} is out of range (1..{1})", junctionNodeIdx, TrackNodes.Length - 1));
+ return (-1, 0); // error
+ }
+
+ TrackNode junctionNode = TrackNodes[junctionNodeIdx];
+
+ if (junctionNode.TrEndNode) { incomingVectorNodeIdx = 0; } // there is no incoming vector node
+
+ else if (junctionNode.TrVectorNode != null)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() WARNING: stitched vector nodes not expected; {0} - {1}", vectorNode.Index, junctionNode.Index));
+ incomingVectorNodeIdx = (int)junctionNode.Index;
+ }
+ else if (junctionNode.TrJunctionNode == null)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() WARNING: expected a junction node, got a {1} for the first incoming node {0}", junctionNode.Index, junctionNode.GetType()));
+ incomingVectorNodeIdx = 0; // there is no incoming vector node
+ }
+ else
+ {
+ int pinIdx = 0; // when connected to trailing side of junction, use in pin
+ if (vectorNode.TrPins[inPinIdx].Direction != connectedToTrailingSide)
+ {
+ // connected to leading (facing) side, use longer next vector node
+ var out1 = junctionNode.TrPins.Length < 2 ? null : TrackNodes[junctionNode.TrPins[1].Link]?.TrVectorNode?.TrVectorSections;
+ int l1 = out1 == null ? 0 : out1.Length;
+ var out2 = junctionNode.TrPins.Length < 3 ? null : TrackNodes[junctionNode.TrPins[2].Link]?.TrVectorNode?.TrVectorSections;
+ int l2 = out2 == null ? 0 : out2.Length;
+ pinIdx = l1 >= l2 ? 1 : 2;
+ }
+ incomingVectorNodeIdx = junctionNode.TrPins[pinIdx].Link;
+ incomingVectorNodeEnd = junctionNode.TrPins[pinIdx].Direction == 0 ? 1 : 0;
+
+ if (incomingVectorNodeIdx <= 0 || incomingVectorNodeIdx >= TrackNodes.Length)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() ERROR: incoming node index {0} is out of range (1..{1})", incomingVectorNodeIdx, TrackNodes.Length - 1));
+ return (-1, 0); // error
+ }
+ else if (incomingVectorNodeIdx == vectorNode.Index)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() ERROR: incoming node index {0} same as query node {1} (circular)", incomingVectorNodeIdx, vectorNode.Index));
+ return (-1, 0); // error
+
+ }
+ else if (TrackNodes[incomingVectorNodeIdx].TrVectorNode == null)
+ {
+ Debug.Print(String.Format("GetIncomingVectorNodeIndex() ERROR: incoming node index {0} is not a vector node (type {1})", incomingVectorNodeIdx, TrackNodes[incomingVectorNodeIdx].GetType()));
+ return (-1, 0); // error
+ }
+ }
+
+ return (incomingVectorNodeIdx, incomingVectorNodeEnd);
+ }
+
+ ///
+ /// Get the index of the vector node that follows the specified vector node.
+ /// Expects to traverse a junction node to find the next vector node.
+ ///
+ /// The index of the outgoing vector node, and the connected end if it.
+ public (int, int) GetOutgoingVectorNodeIndex(TrackNode vectorNode, int direction)
+ {
+ const int connectedToTrailingSide = 0; // re: direction of vector node link to junction
+
+ int outPinIdx = direction == 0 ? 0 : 1;
+ int outgoingVectorNodeIdx = -1; // error
+ int outgoingVectorNodeEnd = 0;
+
+ if (vectorNode == null || vectorNode.TrVectorNode == null || vectorNode.Inpins != 1)
+ {
+ Debug.Print("GetOutgoingVectorNodeIndex() ERROR: source node is not a valid vector node");
+ return (-1, 0); // error
+ }
+
+ int junctionNodeIdx = vectorNode.TrPins[outPinIdx].Link;
+ if (junctionNodeIdx <= 0 || junctionNodeIdx >= TrackNodes.Length)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() ERROR: first outgoing node index {0} is out of range (1..{1})", junctionNodeIdx, TrackNodes.Length - 1));
+ return (-1, 0); // error
+ }
+
+ TrackNode junctionNode = TrackNodes[junctionNodeIdx];
+
+ if (junctionNode.TrEndNode) { outgoingVectorNodeIdx = 0; } // there is no outgoing vector node
+
+ else if (junctionNode.TrVectorNode != null)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() WARNING: stitched vector nodes not expected; {0} - {1}", vectorNode.Index, junctionNode.Index));
+ outgoingVectorNodeIdx = (int)junctionNode.Index;
+ }
+ else if (junctionNode.TrJunctionNode == null)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() WARNING: expected a junction node, got a {1} for the first outgoing node {0}", junctionNode.Index, junctionNode.GetType()));
+ outgoingVectorNodeIdx = 0; // there is no incoming vector node
+ }
+ else
+ {
+ int pinIdx = 0; // when connected to trailing side of junction, use in pin
+ if (vectorNode.TrPins[outPinIdx].Direction != connectedToTrailingSide)
+ {
+ // connected to leading (facing) side, use longer next vector node
+ var out1 = junctionNode.TrPins.Length < 2 ? null : TrackNodes[junctionNode.TrPins[1].Link]?.TrVectorNode?.TrVectorSections;
+ int l1 = out1 == null ? 0 : out1.Length;
+ var out2 = junctionNode.TrPins.Length < 3 ? null : TrackNodes[junctionNode.TrPins[2].Link]?.TrVectorNode?.TrVectorSections;
+ int l2 = out2 == null ? 0 : out2.Length;
+ pinIdx = l1 >= l2 ? 1 : 2;
+ }
+ outgoingVectorNodeIdx = junctionNode.TrPins[pinIdx].Link;
+ outgoingVectorNodeEnd = junctionNode.TrPins[pinIdx].Direction == 0 ? 1 : 0;
+
+ if (outgoingVectorNodeIdx <= 0 || outgoingVectorNodeIdx >= TrackNodes.Length)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() ERROR: outgoing node index {0} is out of range (1..{1})", outgoingVectorNodeIdx, TrackNodes.Length - 1));
+ return (-1, 0); // error
+ }
+ else if (outgoingVectorNodeIdx == vectorNode.Index)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() ERROR: outgoing node index {0} same as query node {1} (circular)", outgoingVectorNodeIdx, vectorNode.Index));
+ return (-1, 0); // error
+
+ }
+ else if (TrackNodes[outgoingVectorNodeIdx].TrVectorNode == null)
+ {
+ Debug.Print(String.Format("GetOutgoingVectorNodeIndex() ERROR: outgoing node index {0} is not a vector node (type {1})", outgoingVectorNodeIdx, TrackNodes[outgoingVectorNodeIdx].GetType()));
+ return (-1, 0); // error
+ }
+ }
+
+ return (outgoingVectorNodeIdx, outgoingVectorNodeEnd);
+ }
+
///
/// Add a number of TrItems (Track Items), created outside of the file, to the table of TrItems.
/// This will also set the ID of the TrItems (since that gives the index in that array)
///
/// The array of new items.
+ /// The index of the first item added (ie. the size of the array before).
[SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Justification = "Keeping identifier consistent to use in MSTS")]
- public void AddTrItems(TrItem[] newTrItems)
+ public int AddTrItems(TrItem[] newTrItems)
{
TrItem[] newTrItemTable;
+ int firstInsertIdx = 0;
if (TrItemTable == null)
{
@@ -180,6 +347,7 @@ public void AddTrItems(TrItem[] newTrItems)
}
else
{
+ firstInsertIdx = TrItemTable.Length;
newTrItemTable = new TrItem[TrItemTable.Length + newTrItems.Length];
TrItemTable.CopyTo(newTrItemTable, 0);
}
@@ -192,6 +360,7 @@ public void AddTrItems(TrItem[] newTrItems)
}
TrItemTable = newTrItemTable;
+ return firstInsertIdx;
}
public void AddTrNodesToPointsOnApiMap(InfoApiMap infoApiMap)
@@ -660,6 +829,26 @@ public class TrVectorNode
/// The amount of TrItems in TrItemRefs
public int NoItemRefs { get; set; } // it would have been better to use TrItemRefs.Length instead of keeping count ourselve
+ // to following members are calculated, not read from the file
+ public float LengthM; // track length
+ public float[] GradePctAtEnd = new float[] { 0f, 0f }; // grade at each end [start, end]
+ public float[] GradeLengthMAtEnd = new float[] { 0f, 0f }; // length of steady grade at each end [start, end]
+ public float[] GradepostDistanceMAtEnd = new float[] { -1f, -1f }; // distance of the first grade marker at each end; -1 if none
+ public GradePostItem[] FirstGradePost = new GradePostItem[2]; // first gradepost in each direction; 0 = in track node direction, 1 = in reverse direction
+ public GradePostItem[] LastGradePost = new GradePostItem[2]; // last gradepost in each direction; 0 = in track node direction, 1 = in reverse direction
+
+ public struct GradeData // represents a segment of track with approx. the same grade, may span multiple vector sections
+ {
+ public float DistanceFromStartM; // for debug
+ public float LengthM; // length in meters
+ public float GradePct; // grade in percent
+ public int TileX, TileZ; // location of the start of this grade segment
+ public float X, Y, Z;
+ public GradeData(float distance, float length, float grade, int tileX, int tileZ, float x, float y, float z) { DistanceFromStartM = distance; LengthM = length; GradePct = grade; TileX = tileX; TileZ = tileZ; X = x; Y = y; Z = z; }
+ }
+ public List GradeList = new List();
+
+
///
/// Default constructor used during file parsing.
///
@@ -736,12 +925,428 @@ public int TrVectorSectionsIndexOf(TrVectorSection targetTvs)
public void AddTrItemRef(int newTrItemRef)
{
int[] newTrItemRefs = new int[NoItemRefs + 1];
- TrItemRefs.CopyTo(newTrItemRefs, 0);
+ TrItemRefs?.CopyTo(newTrItemRefs, 0);
newTrItemRefs[NoItemRefs] = newTrItemRef;
TrItemRefs = newTrItemRefs; //use the new item lists for the track node
NoItemRefs++;
}
- }
+
+ ///
+ /// Add a list of new TrItem references to the TrItemRefs.
+ ///
+ /// The reference to the new TrItem
+ [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", Justification = "Keeping identifier consistent to use in MSTS")]
+ public void AddTrItemRef(int startIndex, TrItem[] trItems)
+ {
+ int numNew = trItems.Length - startIndex;
+
+ // create a new array and copy old refs
+ var newTrItemRefs = new int[NoItemRefs + numNew];
+ TrItemRefs?.CopyTo(newTrItemRefs, 0);
+
+ // set refs for added items
+ for (int refIdx = NoItemRefs, itemIdx = startIndex; refIdx < newTrItemRefs.Length && itemIdx < trItems.Length; refIdx++, itemIdx++)
+ newTrItemRefs[refIdx] = (int)trItems[itemIdx].TrItemId;
+
+ // set vector node to use new array
+ TrItemRefs = newTrItemRefs;
+ NoItemRefs = TrItemRefs.Length;
+ }
+
+ ///
+ /// Add grade info to the track vector node. Traverse the track sections and build a grade profile.
+ /// Sections with approximately the same grade are combined.
+ ///
+ // length and grade calc taken from TrackViewer, PathChartData.cs, AddPointAndTrackItems(), GetCurvature(), SectionLengthAlongTrack()
+ public void AddGradeInfo(uint vectorNodeIdx, TrackSections trackSections, /* for debug */ int tvsZeroLenCnt)
+ {
+ if (TrVectorSections != null && TrVectorSections.Length > 0)
+ {
+ // handle first section; start a new grade segment
+ TrVectorSection firstVS = TrVectorSections[0];
+ TrackSection firstTS = trackSections[firstVS.SectionIndex];
+ float firstLength = firstTS.SectionCurve != null ? MathHelper.ToRadians(Math.Abs(firstTS.SectionCurve.Angle)) * firstTS.SectionCurve.Radius : firstTS.SectionSize.Length;
+ float firstGrade = firstVS.AX * -100f;
+ GradeData gradeItem = new GradeData(0f, firstLength, firstGrade, firstVS.TileX, firstVS.TileZ, firstVS.X, firstVS.Y, firstVS.Z);
+ float distanceFromStartM = firstLength;
+
+ for (int vsIdx = 1; vsIdx < TrVectorSections.Length; vsIdx++)
+ {
+ TrVectorSection vs = TrVectorSections[vsIdx];
+ TrackSection ts = trackSections[vs.SectionIndex];
+ float length = ts.SectionCurve != null ? MathHelper.ToRadians(Math.Abs(ts.SectionCurve.Angle)) * ts.SectionCurve.Radius : ts.SectionSize.Length;
+ float grade = vs.AX * -100f;
+
+ if (length < 0.01f) { tvsZeroLenCnt++; goto nextSegment; } // ignore very short sections (and division by zero)
+
+ // if less than one promille change
+ if (Math.Abs(grade - gradeItem.GradePct) < 0.1)
+ {
+ // combine with previous section
+ gradeItem.GradePct = (gradeItem.LengthM > 0f) ? (gradeItem.GradePct * gradeItem.LengthM + grade * length) / (gradeItem.LengthM + length) : grade;
+ gradeItem.LengthM += length;
+ }
+ else
+ {
+ // add the grade item and start a new one
+ if (gradeItem.LengthM > 0) { GradeList.Add(gradeItem); }
+ gradeItem = new GradeData(distanceFromStartM, length, grade, vs.TileX, vs.TileZ, vs.X, vs.Y, vs.Z);
+ }
+
+ nextSegment:
+ distanceFromStartM += length;
+ this.LengthM += length; // sum up the length of the vector section
+ }
+
+ // save the final segment
+ if (gradeItem.LengthM > 0) { GradeList.Add(gradeItem); }
+ }
+
+ if (GradeList.Count > 0)
+ {
+ GradePctAtEnd[0] = GradeList[0].GradePct;
+ GradeLengthMAtEnd[0] = GradeList[0].LengthM;
+ GradePctAtEnd[1] = GradeList[GradeList.Count - 1].GradePct;
+ GradeLengthMAtEnd[1] = GradeList[GradeList.Count - 1].LengthM;
+ }
+ }
+
+ ///
+ /// Process the forward grade info in the vector node, to determine where gradeposts should be placed.
+ /// Then create the gradeposts and add them to the vector node's existing list of track items.
+ ///
+ // Ensure that not too many gradeposts are created, as that would crowd the track monitor.
+ public void ProcessForwardGradeInfoAndAddGradeposts(uint vectorNodeIdx, TrackDB trackDB)
+ {
+ const int direction = 0; // forward
+
+ // for tuning the aggregation
+ const float shortGradeLengthM = 30f;
+ const float mediumGradeLengthM = 70f;
+ const float longGradeLengthM = 150f;
+ const float minDistanceBetweenGradepostM = 200f;
+
+ GradePostItem gradepost = null;
+
+ TrackNode trackNode = trackDB.TrackNodes[vectorNodeIdx];
+ TrVectorNode vectorNode = trackNode.TrVectorNode;
+
+ // start with the last gradepost in the preceding vector node
+ bool isExistingGradepost = false;
+ (int precedingVectorNodeIdx, int precedingVectorNodeEnd) = trackDB.GetIncomingVectorNodeIndex(trackNode, direction);
+ if (precedingVectorNodeIdx > 0)
+ {
+ TrVectorNode precedingVectorNode = trackDB.TrackNodes[precedingVectorNodeIdx].TrVectorNode;
+ GradePostItem precedingGradepost = null;
+ if (precedingVectorNodeEnd == 0)
+ precedingGradepost = precedingVectorNode.FirstGradePost[direction];
+ else
+ precedingGradepost = precedingVectorNode.LastGradePost[direction];
+ if (precedingGradepost != null && precedingGradepost.DistanceFromStartM + precedingGradepost.ForDistanceM >= precedingVectorNode.LengthM)
+ {
+ gradepost = precedingGradepost;
+ isExistingGradepost = true;
+ }
+ }
+
+ List newTrItems = new List(); // to collect all new gradeposts, and add them all at once at the end
+
+ GradePostItem firstGradepost = null;
+ GradePostItem lastGradepost = null;
+ float currentDistanceFromStartM = 0f;
+
+ for (int gradeIdx = 0; gradeIdx < GradeList.Count; gradeIdx++)
+ {
+ GradeData gradeInfo = GradeList[gradeIdx];
+
+ // if there is no current gradepost:
+ if (gradepost == null)
+ {
+ // start a new gradepost with the current grade
+ isExistingGradepost = false;
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct, gradeInfo.LengthM, direction, gradeInfo.TileX, gradeInfo.TileZ, gradeInfo.X, gradeInfo.Y, gradeInfo.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated first: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+
+ goto nextLoop;
+ }
+
+ // if the current grade is the same as the gradepost
+ if (Math.Abs(gradeInfo.GradePct - gradepost.GradePct) < 0.1f)
+ {
+ // add to the gradepost
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // if a long gradepost followed by a long grade (min+, long+, --)
+ if (gradepost.ForDistanceM > minDistanceBetweenGradepostM && gradeInfo.LengthM > longGradeLengthM)
+ {
+ // finalize (add) the gradepost and start a new one with the current grade
+ if (!isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ isExistingGradepost = false;
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct, gradeInfo.LengthM, direction, gradeInfo.TileX, gradeInfo.TileZ, gradeInfo.X, gradeInfo.Y, gradeInfo.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated long-long: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+
+ goto nextLoop;
+ }
+
+ // if a short gradepost followed by a long grade (short-, long+, --)
+ if (gradepost.ForDistanceM <= shortGradeLengthM && gradeInfo.LengthM > longGradeLengthM)
+ {
+ // adopt the current grade
+ gradepost.GradePct = gradeInfo.GradePct;
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // get the next grade segment
+ float nextGradePct = gradeIdx + 1 < GradeList.Count ? GradeList[gradeIdx + 1].GradePct : 918273f; // never matches a real grade
+ float nextGradeLengthM = gradeIdx + 1 < GradeList.Count ? GradeList[gradeIdx + 1].LengthM : 0f;
+
+ // if there is a brief deviation from a steady grade (same, short/medium, same)
+ if (nextGradeLengthM > 0f && Math.Abs(gradepost.GradePct - nextGradePct) < 0.1f &&
+ ((gradepost.ForDistanceM > longGradeLengthM && nextGradeLengthM > longGradeLengthM && gradeInfo.LengthM <= mediumGradeLengthM) ||
+ (gradepost.ForDistanceM > mediumGradeLengthM && nextGradeLengthM > mediumGradeLengthM && gradeInfo.LengthM <= shortGradeLengthM)))
+ {
+ // absorbe the current (short) grade into the gradepost (add length, but keep grade); next loop will also add the next grade
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // if gradepost is long enough
+ if (gradepost.ForDistanceM > minDistanceBetweenGradepostM)
+ {
+ // finalize (add) the gradepost and start a new one with the current grade
+ if (!isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ isExistingGradepost = false;
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct,gradeInfo.LengthM, direction, gradeInfo.TileX, gradeInfo.TileZ, gradeInfo.X, gradeInfo.Y, gradeInfo.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated long-gradepost: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+
+ goto nextLoop;
+ }
+
+ // else, average the grade
+ gradepost.GradePct = (gradepost.GradePct * gradepost.ForDistanceM + gradeInfo.GradePct * gradeInfo.LengthM) / (gradepost.ForDistanceM + gradeInfo.LengthM);
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+nextLoop:
+ currentDistanceFromStartM += gradeInfo.LengthM;
+ }
+
+ // add the last gradepost
+ if (gradepost?.ForDistanceM > minDistanceBetweenGradepostM && !isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ // append new items to the Track DB's TrItemTable, and update references
+ if (newTrItems.Count > 0)
+ {
+ int firstInsertIdx = trackDB.AddTrItems(newTrItems.ToArray());
+ AddTrItemRef(firstInsertIdx, trackDB.TrItemTable);
+
+ vectorNode.FirstGradePost[direction] = firstGradepost;
+ vectorNode.LastGradePost[direction] = lastGradepost;
+ }
+ }
+
+ /// Process the reverse grade info in the vector node, to determine where gradeposts should be placed.
+ /// Then create the gradeposts and add them to the vector node's existing list of track items.
+ ///
+ // Ensure that not too many gradeposts are created, as that would crowd the track monitor.
+ public void ProcessReverseGradeInfoAndAddGradeposts(uint vectorNodeIdx, TrackDB trackDB)
+ {
+ const int direction = 1; // reverse
+
+ // for tuning the aggregation
+ const float shortGradeLengthM = 30f;
+ const float mediumGradeLengthM = 70f;
+ const float longGradeLengthM = 150f;
+ const float minDistanceBetweenGradepostM = 200f;
+
+ GradePostItem gradepost = null;
+ GradeData precedingGrade = new GradeData(); // in reverse direction, we need the location of the preceding grade
+
+ TrackNode trackNode = trackDB.TrackNodes[vectorNodeIdx];
+ TrVectorNode vectorNode = trackNode.TrVectorNode;
+
+ // start with the last gradepost in the preceding vector node
+ bool isExistingGradepost = false;
+ (int precedingVectorNodeIdx, int precedingVectorNodeEnd) = trackDB.GetIncomingVectorNodeIndex(trackNode, direction);
+ if (precedingVectorNodeIdx > 0)
+ {
+ TrVectorNode precedingVectorNode = trackDB.TrackNodes[precedingVectorNodeIdx].TrVectorNode;
+ GradePostItem precedingGradepost = null;
+ if (precedingVectorNodeEnd == 0)
+ {
+ precedingGradepost = precedingVectorNode.FirstGradePost[direction];
+ if (precedingVectorNode.GradeList?.Count > 0) { precedingGrade = precedingVectorNode.GradeList[0]; }
+ }
+ else
+ {
+ precedingGradepost = precedingVectorNode.LastGradePost[direction];
+ if (precedingVectorNode.GradeList?.Count > 0) { precedingGrade = precedingVectorNode.GradeList[precedingVectorNode.GradeList.Count - 1]; }
+ }
+ if (precedingGradepost != null && precedingGradepost.DistanceFromStartM + precedingGradepost.ForDistanceM >= precedingVectorNode.LengthM)
+ {
+ gradepost = precedingGradepost;
+ isExistingGradepost = true;
+ }
+ }
+
+ List newTrItems = new List(); // to collect all new gradeposts, and add them all at once at the end
+
+ GradePostItem firstGradepost = null;
+ GradePostItem lastGradepost = null;
+ float currentDistanceFromStartM = vectorNode.LengthM;
+
+ for (int gradeIdx = GradeList.Count - 1; gradeIdx >= 0; gradeIdx--)
+ {
+ GradeData tempGradeInfo = GradeList[gradeIdx];
+ GradeData gradeInfo = tempGradeInfo;
+ gradeInfo.GradePct = -1 * gradeInfo.GradePct;
+
+ // if there is no current gradepost:
+ if (gradepost == null)
+ {
+ // start a new gradepost with the current grade, and preceding grade location
+ if (precedingGrade.LengthM <= 0f) { precedingGrade = gradeInfo; } // use current grade (location) if there is no preceding grade
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct, gradeInfo.LengthM, direction, precedingGrade.TileX, precedingGrade.TileZ, precedingGrade.X, precedingGrade.Y, precedingGrade.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated first: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+ isExistingGradepost = false;
+
+ goto nextLoop;
+ }
+
+ // if the current grade is the same as the gradepost
+ if (Math.Abs(gradeInfo.GradePct - gradepost.GradePct) < 0.1f)
+ {
+ // add to the gradepost
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // if a long gradepost followed by a long grade (min+, long+, --)
+ if (gradepost.ForDistanceM > minDistanceBetweenGradepostM && gradeInfo.LengthM > longGradeLengthM)
+ {
+ // finalize (add) the gradepost
+ if (!isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ // start a new gradepost with the current grade, and preceding grade location
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct, gradeInfo.LengthM, direction, precedingGrade.TileX, precedingGrade.TileZ, precedingGrade.X, precedingGrade.Y, precedingGrade.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated long-long: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+ isExistingGradepost = false;
+
+ goto nextLoop;
+ }
+
+ // if a short gradepost followed by a long grade (short-, long+, --)
+ if (gradepost.ForDistanceM <= shortGradeLengthM && gradeInfo.LengthM > longGradeLengthM)
+ {
+ // adopt the current grade
+ gradepost.GradePct = gradeInfo.GradePct;
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // get the next grade segment
+ float nextGradePct = gradeIdx - 1 >= 0 ? GradeList[gradeIdx - 1].GradePct : 918273f; // never matches a real grade
+ float nextGradeLengthM = gradeIdx - 1 >= 0 ? GradeList[gradeIdx - 1].LengthM : 0f;
+
+ // if there is a brief deviation from a steady grade (same, short/medium, same)
+ if (nextGradeLengthM > 0f && Math.Abs(gradepost.GradePct - nextGradePct) < 0.1f &&
+ ((gradepost.ForDistanceM > longGradeLengthM && nextGradeLengthM > longGradeLengthM && gradeInfo.LengthM <= mediumGradeLengthM) ||
+ (gradepost.ForDistanceM > mediumGradeLengthM && nextGradeLengthM > mediumGradeLengthM && gradeInfo.LengthM <= shortGradeLengthM)))
+ {
+ // absorbe the current (short) grade into the gradepost (add length, but keep grade); next loop will also add the next grade
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+ goto nextLoop;
+ }
+
+ // if gradepost is long enough
+ if (gradepost.ForDistanceM > minDistanceBetweenGradepostM)
+ {
+ // finalize (add) the gradepost
+ if (!isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ // start a new gradepost with the current grade, and preceding grade location
+ gradepost = new GradePostItem(currentDistanceFromStartM, gradeInfo.GradePct, gradeInfo.LengthM, direction, precedingGrade.TileX, precedingGrade.TileZ, precedingGrade.X, precedingGrade.Y, precedingGrade.Z);
+ gradepost.TrackNodeIndex = vectorNodeIdx; // for debug
+ gradepost.ItemName = String.Format("Calculated long-gradepost: TrackNode {0} at distance {1}: direction {2}, grade {3:F2} for {4:F1}",
+ vectorNodeIdx, currentDistanceFromStartM, direction, gradepost.GradePct, gradepost.ForDistanceM); // for debug
+ isExistingGradepost = false;
+
+ goto nextLoop;
+ }
+
+ // else, average the grade
+ gradepost.GradePct = (gradepost.GradePct * gradepost.ForDistanceM + gradeInfo.GradePct * gradeInfo.LengthM) / (gradepost.ForDistanceM + gradeInfo.LengthM);
+ gradepost.ForDistanceM += gradeInfo.LengthM;
+
+nextLoop:
+ currentDistanceFromStartM -= gradeInfo.LengthM;
+ precedingGrade = gradeInfo;
+ }
+
+ // add the last gradepost
+ if (gradepost?.ForDistanceM > minDistanceBetweenGradepostM && !isExistingGradepost)
+ {
+ newTrItems.Add(gradepost);
+ if (firstGradepost == null) { firstGradepost = gradepost; }
+ lastGradepost = gradepost;
+ }
+
+ // append new items to the Track DB's TrItemTable, and update references
+ if (newTrItems.Count > 0)
+ {
+ int firstInsertIdx = trackDB.AddTrItems(newTrItems.ToArray());
+ AddTrItemRef(firstInsertIdx, trackDB.TrItemTable);
+
+ vectorNode.FirstGradePost[direction] = firstGradepost;
+ vectorNode.LastGradePost[direction] = lastGradepost;
+ }
+ }
+ } // end class TrVectorNode
///
/// Describes a single section in a vector node.
@@ -890,7 +1495,9 @@ public enum trItemType
/// A pickup of fuel, water, ...
trPICKUP,
/// The place where cars are appear of disappear
- trCARSPAWNER
+ trCARSPAWNER,
+ /// A post indicating the grade of the track ahead // TODO: Should mileposts be here instead of speed posts?
+ trGRADEPOST
}
/// Type of track item
@@ -1518,6 +2125,53 @@ public PickupItem(STFReader stf, int idx)
}
}
+ ///
+ /// Represents a grade post, indicating the grade of the section ahead.
+ ///
+ // Initially these are calculated from the track profile. In the future, placing
+ // grade markers (or plates) with the track may be supported.
+ public class GradePostItem : TrItem
+ {
+ /// Grade in percent.
+ public float GradePct;
+ /// Distance (in meters) for which the grade applies.
+ public float ForDistanceM;
+ /// Direction in which the grade applies. 0 is in track node direction, 1 is in reverse direction.
+ public int Direction;
+
+ // fields not read from file, set in post-processing
+ /// Distance of the grade post from the start of the track node. TBD if alwasy from start, or if depends on direction. TBD if needed or for DEBUG.
+ public float DistanceFromStartM;
+ /// Set post construction. Reference (index) to the Track Node the grade post belongs to.
+ public uint TrackNodeIndex;
+ /// Set post construction. Reference to TrackCircuitGradepost (in signals).
+ public int TcGradepostIdx; // based on SigObj from speed post
+
+ ///
+ /// Default constructor used during file parsing.
+ ///
+ /// The STFreader containing the file stream
+ /// The index of this TrItem in the list of TrItems
+ public GradePostItem(STFReader stf, int idx)
+ {
+ Trace.TraceWarning("GradePostItem(STFReader stf, int idx) is not implemented. Placeholder for future.");
+ }
+
+ ///
+ /// Create a Grade Marker (Post) based on grade info from the track profile.
+ ///
+ public GradePostItem(float distFromStart, float grade, float length, int direction, int tileX, int tileZ, float x, float y, float z)
+ {
+ ItemType = trItemType.trGRADEPOST;
+ DistanceFromStartM = distFromStart;
+ GradePct = grade;
+ ForDistanceM = length;
+ Direction = direction;
+ TileX = tileX; TileZ = tileZ;
+ X = x; Y = y; Z = z;
+ }
+ }
+
#region CrossReference to TrackCircuitSection
///
/// To make it possible for a MSTS (vector) TrackNode to have information about the TrackCircuitSections that
diff --git a/Source/Orts.Simulation/Simulation/AIs/AIPath.cs b/Source/Orts.Simulation/Simulation/AIs/AIPath.cs
index 5d498612b8..09cef369bd 100644
--- a/Source/Orts.Simulation/Simulation/AIs/AIPath.cs
+++ b/Source/Orts.Simulation/Simulation/AIs/AIPath.cs
@@ -262,6 +262,7 @@ public class AIPathNode
public WorldLocation Location; // coordinates for this path node
public int JunctionIndex = -1; // index of junction node, -1 if none
public bool IsFacingPoint; // true if this node entered from the facing point end
+ public bool IsIntermediateNode; // true if the path node is an intermediate node within a vector node
//public bool IsLastSwitchUse; //true if this node is last to touch a switch
public bool IsVisited; // true if the train has visited this node
@@ -300,6 +301,7 @@ public AIPathNode(AIPathNode otherNode)
Location = otherNode.Location;
JunctionIndex = otherNode.JunctionIndex;
IsFacingPoint = otherNode.IsFacingPoint;
+ IsIntermediateNode = otherNode.IsIntermediateNode;
IsVisited = otherNode.IsVisited;
}
@@ -318,6 +320,8 @@ public AIPathNode(AIPathNode otherNode)
// in principle it would be more logical to have it in PATfile.cs. But this leads to too much code duplication
private void InterpretPathNodeFlags(TrPathNode tpn, TrackPDP pdp, bool isTimetableMode)
{
+ if ((tpn.pathFlags & 04) != 0) IsIntermediateNode = true;
+
if ((tpn.pathFlags & 03) == 0) return;
// Bit 0 and/or bit 1 is set.
@@ -386,6 +390,7 @@ public AIPathNode(BinaryReader inf)
NextSidingTVNIndex = inf.ReadInt32();
JunctionIndex = inf.ReadInt32();
IsFacingPoint = inf.ReadBoolean();
+ IsIntermediateNode = inf.ReadBoolean();
Location = new WorldLocation();
Location.TileX = inf.ReadInt32();
Location.TileZ = inf.ReadInt32();
@@ -407,6 +412,7 @@ public void Save(BinaryWriter outf)
outf.Write(NextSidingTVNIndex);
outf.Write(JunctionIndex);
outf.Write(IsFacingPoint);
+ outf.Write(IsIntermediateNode);
outf.Write(Location.TileX);
outf.Write(Location.TileZ);
outf.Write(Location.Location.X);
diff --git a/Source/Orts.Simulation/Simulation/Activity.cs b/Source/Orts.Simulation/Simulation/Activity.cs
index 4337c8cc64..bd8df6aadb 100644
--- a/Source/Orts.Simulation/Simulation/Activity.cs
+++ b/Source/Orts.Simulation/Simulation/Activity.cs
@@ -528,9 +528,10 @@ public void AddRestrictZones(Tr_RouteFile routeFile, TrackSectionsFile tsectionD
zones.ActivityRestrictedSpeedZoneList[idxZone].EndPosition, false, worldPosition2, false);
// Add the speedposts to the track database. This will set the TrItemId's of all speedposts
- trackDB.AddTrItems(newSpeedPostItems);
+ trackDB.AddTrItems(newSpeedPostItems);
+ // TODO: shoulde the item be added to the TrVectorNode's TrItemRefs?
- // And now update the various (vector) tracknodes (this needs the TrItemIds.
+ // And now update the various (vector) tracknodes (this needs the TrItemIds.
var endOffset = AddItemIdToTrackNode(ref zones.ActivityRestrictedSpeedZoneList[idxZone].EndPosition,
tsectionDat, trackDB, newSpeedPostItems[1], out traveller);
var startOffset = AddItemIdToTrackNode(ref zones.ActivityRestrictedSpeedZoneList[idxZone].StartPosition,
diff --git a/Source/Orts.Simulation/Simulation/Physics/Train.cs b/Source/Orts.Simulation/Simulation/Physics/Train.cs
index 1ff683c027..2f7f6173ad 100644
--- a/Source/Orts.Simulation/Simulation/Physics/Train.cs
+++ b/Source/Orts.Simulation/Simulation/Physics/Train.cs
@@ -69,6 +69,7 @@
using ORTS.Common;
using ORTS.Scripting.Api;
using ORTS.Settings;
+using static System.Collections.Specialized.BitVector32;
using Event = Orts.Common.Event;
namespace Orts.Simulation.Physics
@@ -14678,6 +14679,7 @@ public String[] AddRestartTime(String[] stateString)
public List[] PlayerTrainSpeedposts; // 0 forward, 1 backward
public List[,] PlayerTrainDivergingSwitches; // 0 forward, 1 backward; second index 0 facing, 1 trailing
public List[] PlayerTrainMileposts; // 0 forward, 1 backward
+ public List[] PlayerTrainGradeposts; // 0 forward, 1 backward
public List[] PlayerTrainTunnels; // 0 forward, 1 backward
///
@@ -14691,6 +14693,7 @@ public void InitializePlayerTrainData()
PlayerTrainSpeedposts = new List[2];
PlayerTrainDivergingSwitches = new List[2, 2];
PlayerTrainMileposts = new List[2];
+ PlayerTrainGradeposts = new List[2];
PlayerTrainTunnels = new List[2];
for (int dir = 0; dir < 2; dir++)
{
@@ -14702,6 +14705,7 @@ public void InitializePlayerTrainData()
for (int i = 0; i < 2; i++)
PlayerTrainDivergingSwitches[dir, i] = new List();
PlayerTrainMileposts[dir] = new List();
+ PlayerTrainGradeposts[dir] = new List();
PlayerTrainTunnels[dir] = new List();
}
}
@@ -14717,6 +14721,8 @@ public void InitializePlayerTrainData()
playerTrainDivergingSwitchList?.Clear();
foreach (var playerTrainMilepostList in PlayerTrainMileposts)
playerTrainMilepostList?.Clear();
+ foreach (var playerTrainGradepostList in PlayerTrainGradeposts)
+ playerTrainGradepostList?.Clear();
foreach (var playerTrainTunnelList in PlayerTrainTunnels)
playerTrainTunnelList?.Clear();
}
@@ -14731,6 +14737,11 @@ public void UpdatePlayerTrainData()
//TODO add generation of other train data
}
+#if DEBUG
+ public int NextDumpTime = 0; // DEBUG
+ public int CallCount = 2;
+#endif
+
///
/// Updates the Player train data;
/// For every section it adds the TrainObjectItems to the various lists;
@@ -14816,6 +14827,9 @@ public void UpdatePlayerTrainData(float maxDistanceM)
var routePath = ValidRoute[dir];
var prevMilepostValue = -1f;
var prevMilepostDistance = -1f;
+ var prevGradepostValue = -1f;
+ var prevGradepostDistance = -1f;
+
while (index < routePath.Count && totalLength - lengthOffset < maxDistanceNORMALM)
{
var sectionDistanceToTrainM = totalLength - lengthOffset;
@@ -14983,6 +14997,34 @@ public void UpdatePlayerTrainData(float maxDistanceM)
}
}
+ // search for grade posts
+ // no change if using sectionDirection, match as 0 or match as 1
+ int relativeDirection = sectionDirection == dir ? 0 : 1;
+ if (thisSection.CircuitItems.TrackCircuitGradeposts[relativeDirection] != null)
+ {
+ foreach (TrackCircuitGradepost thisGradepostItem in thisSection.CircuitItems.TrackCircuitGradeposts[sectionDirection])
+ {
+ Gradepost thisGradepost = thisGradepostItem.GradepostRef;
+ var distanceToTrainM = thisGradepostItem.GradepostLocation + sectionDistanceToTrainM;
+ if (distanceToTrainM < maxDistanceM)
+ {
+ if (!(distanceToTrainM - prevGradepostDistance < 50 && thisGradepost.GradePct == prevGradepostValue) && distanceToTrainM > 0)
+ {
+ thisItem = new TrainObjectItem(thisGradepost.GradePct, distanceToTrainM);
+ prevGradepostDistance = distanceToTrainM;
+ prevGradepostValue = thisGradepost.GradePct;
+ PlayerTrainGradeposts[dir].Add(thisItem);
+#if DEBUG
+ if (System.DateTime.Now.Second > NextDumpTime || CallCount > 0)
+ Debug.WriteLine(String.Format("Train-Adding: dir {0}, TNIdx {1}, distance {2:F1}, grade {3:F2}, circuitIDX {4}, TrItemId {5}",
+ dir, thisGradepost.TrackNodeIdx, thisItem.DistanceToTrainM, thisItem.GradePct, thisSection.Index, thisGradepost.TrItemId));
+#endif
+ }
+ }
+ else break;
+ }
+ }
+
// search for tunnels
if (thisSection.TunnelInfo != null)
{
@@ -15019,6 +15061,30 @@ public void UpdatePlayerTrainData(float maxDistanceM)
continue;
}
}
+#if DEBUG
+ if (System.DateTime.Now.Second > NextDumpTime || CallCount > 0)
+ {
+ int cnt = 0;
+ foreach (var gradepost in PlayerTrainGradeposts[0])
+ {
+ Debug.WriteLine(String.Format("TrainGradepost-Fwd: {0}, distance {1:F1}, grade = {2:F2}",
+ cnt, gradepost.DistanceToTrainM, gradepost.GradePct));
+ cnt++;
+
+ }
+ int cnt1 = 0;
+ foreach (var gradepost in PlayerTrainGradeposts[1])
+ {
+ Debug.WriteLine(String.Format("TrainGradepost-Rev: {0}, distance {1:F1}, grade = {2:F2}",
+ cnt1, gradepost.DistanceToTrainM, gradepost.GradePct));
+ cnt1++;
+
+ }
+ Debug.WriteLine(String.Format("TrainGradepost-count: fwd {0}, rev {1}", cnt, cnt1));
+ NextDumpTime = System.DateTime.Now.Second + 60;
+ CallCount--;
+ }
+#endif
}
///
@@ -15136,6 +15202,13 @@ public void GetTrainInfoAuto(ref TrainInfo thisInfo)
else break;
}
+ // Add all grade posts within maximum distance
+ foreach (TrainObjectItem thisTrainItem in PlayerTrainGradeposts[0])
+ {
+ if (thisTrainItem.DistanceToTrainM <= maxDistanceM) thisInfo.ObjectInfoForward.Add(thisTrainItem);
+ else break;
+ }
+
// Add all diverging switches within maximum distance
foreach (TrainObjectItem thisTrainItem in PlayerTrainDivergingSwitches[0, 0])
{
@@ -15311,6 +15384,13 @@ public void GetTrainInfoManual(ref TrainInfo thisInfo)
else break;
}
+ // Add all grade posts within maximum distance
+ foreach (TrainObjectItem thisTrainItem in PlayerTrainGradeposts[0])
+ {
+ if (thisTrainItem.DistanceToTrainM <= maxDistanceM) thisInfo.ObjectInfoForward.Add(thisTrainItem);
+ else break;
+ }
+
// Add all diverging switches within maximum distance
foreach (TrainObjectItem thisTrainItem in PlayerTrainDivergingSwitches[0, 0])
{
@@ -15358,6 +15438,13 @@ public void GetTrainInfoManual(ref TrainInfo thisInfo)
else break;
}
+ // Add all grade posts within maximum distance
+ foreach (TrainObjectItem thisTrainItem in PlayerTrainGradeposts[1])
+ {
+ if (thisTrainItem.DistanceToTrainM <= maxDistanceM) thisInfo.ObjectInfoBackward.Add(thisTrainItem);
+ else break;
+ }
+
// Add all diverging switches within maximum distance
foreach (TrainObjectItem thisTrainItem in PlayerTrainDivergingSwitches[1, 0])
{
@@ -21431,6 +21518,7 @@ public enum TRAINOBJECTTYPE
TRAILING_SWITCH,
GENERIC_SIGNAL,
TUNNEL,
+ GRADEPOST,
}
public enum SpeedItemType
@@ -21452,6 +21540,7 @@ public enum SpeedItemType
public SpeedItemType SpeedObjectType;
public bool Valid;
public string ThisMile;
+ public float GradePct;
public bool IsRightSwitch;
public SignalObject SignalObject;
@@ -21595,6 +21684,17 @@ public TrainObjectItem(string thisMile, float thisDistanceM)
ThisMile = thisMile;
}
+ // Constructor for Gradepost
+ public TrainObjectItem(float gradePct, float thisDistanceM)
+ {
+ ItemType = TRAINOBJECTTYPE.GRADEPOST;
+ AuthorityType = END_AUTHORITY.NO_PATH_RESERVED;
+ SignalState = TrackMonitorSignalAspect.Clear_2;
+ AllowedSpeedMpS = -1;
+ DistanceToTrainM = thisDistanceM;
+ GradePct = gradePct;
+ }
+
// Constructor for facing or trailing Switch
public TrainObjectItem(bool isRightSwitch, float thisDistanceM, TRAINOBJECTTYPE type)
{
diff --git a/Source/Orts.Simulation/Simulation/Signalling/Gradepost.cs b/Source/Orts.Simulation/Simulation/Signalling/Gradepost.cs
new file mode 100644
index 0000000000..738b19f28e
--- /dev/null
+++ b/Source/Orts.Simulation/Simulation/Signalling/Gradepost.cs
@@ -0,0 +1,62 @@
+// COPYRIGHT 2025 by the Open Rails project.
+//
+// This file is part of Open Rails.
+//
+// Open Rails is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Open Rails is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Open Rails. If not, see .
+
+// This is part of the effort to respresent grade information in the track monitor.
+// The initial version determinates significant grade changes from the track database.
+//
+// Milempost are taken as a model for presenting the grade information in the simulation.
+// This will allow, in the future, to add grade-posts to the track, as is the case at some
+// railways (eg. in Switzerland).
+
+namespace Orts.Simulation.Signalling
+{
+ ///
+ /// Represents either a world-object grade post, or a calculated grade post.
+ /// Only the latter is currently supported.
+ ///
+ public class Gradepost
+ {
+ /// Reference to TrItem; index into TrackDB.TrItemTable.
+ public uint TrItemId;
+ /// Reference to TrackCircuitSection; index into Signals.TrackCircuitList.
+ public int TCReference = -1; // undefined
+ /// Position within TrackCircuit. Distance im meters?
+ public float TCOffset;
+ /// Grade in percent.
+ public float GradePct;
+ /// Distance in meters for which the grade applies.
+ public float ForDistanceM;
+ /// The direction in which the grade applies. 0 is in track circuit direction, 1 is in reverse direction.
+ public int Direction;
+ /// Reference to TrackNode; index into TrackDB.TrackNodes.
+ public int TrackNodeIdx;
+
+ /// Constructor with base attributes.
+ public Gradepost(uint trItemId, float gradePct, float distanceM, int dir)
+ {
+ TrItemId = trItemId;
+ GradePct = gradePct;
+ ForDistanceM = distanceM;
+ Direction = dir;
+ }
+
+ /// Dummy constructor
+ public Gradepost()
+ {
+ }
+ }
+}
diff --git a/Source/Orts.Simulation/Simulation/Signalling/Signals.cs b/Source/Orts.Simulation/Simulation/Signalling/Signals.cs
index 16f1863b0c..dd3a7f7f0e 100644
--- a/Source/Orts.Simulation/Simulation/Signalling/Signals.cs
+++ b/Source/Orts.Simulation/Simulation/Signalling/Signals.cs
@@ -81,6 +81,8 @@ public class Signals
public List MilepostList = new List(); // list of mileposts
private int foundMileposts;
+ public List[] GradepostList = new List[2]; // list of gradeposts in each direction (0 = forward)
+
public Signals(Simulator simulator, SignalConfigurationFile sigcfg, CancellationToken cancellation)
{
Simulator = simulator;
@@ -89,6 +91,9 @@ public Signals(Simulator simulator, SignalConfigurationFile sigcfg, Cancellation
File.Delete(@"C:\temp\printproc.txt");
#endif
+ // needed in all constructors
+ GradepostList[0] = new List(); GradepostList[1] = new List();
+
SignalRefList = new Dictionary();
SignalHeadList = new Dictionary();
Dictionary platformList = new Dictionary();
@@ -264,6 +269,62 @@ public Signals(Simulator simulator, SignalConfigurationFile sigcfg, Cancellation
DeadlockInfoList = new Dictionary();
deadlockIndex = 1;
DeadlockReference = new Dictionary();
+
+#if DEBUG
+ // dump grade posts
+ int cnt1 = 0; int cnt2 = 0;
+ if (GradepostList[0] != null)
+ {
+ foreach (var gradepost in GradepostList[0])
+ {
+ Debug.WriteLine(String.Format("Signals-Gradepost-Fwd: TrackNode = {0}, idx {1}, grade {2:F2}, for {3:F1}, dir {4}, TrItemId = {5}, TCReference = {6}, tcOffset = {7}",
+ gradepost.TrackNodeIdx, cnt1, gradepost.GradePct, gradepost.ForDistanceM, gradepost.Direction, gradepost.TrItemId, gradepost.TCReference, gradepost.TCOffset));
+ cnt1++;
+ }
+ }
+ if (GradepostList[1] != null)
+ {
+ foreach (var gradepost in GradepostList[1])
+ {
+ Debug.WriteLine(String.Format("Signals-Gradepost-Rev: TrackNode = {0}, idx {1}, grade {2:F2}, for {3:F1}, dir {4}, TrItemId = {5}, TCReference = {6}, tcOffset = {7}",
+ gradepost.TrackNodeIdx, cnt2, gradepost.GradePct, gradepost.ForDistanceM, gradepost.Direction, gradepost.TrItemId, gradepost.TCReference, gradepost.TCOffset));
+ cnt2++;
+ }
+ }
+ Debug.WriteLine(String.Format("Signals-Gradepost-Count: {0}/{1}", cnt1, cnt2));
+
+ // dump track circuit items of type grade post
+ int cnt3 = 0; int cnt4 = 0;
+ if (TrackCircuitList != null)
+ {
+ foreach (var tcSection in TrackCircuitList)
+ {
+ if (tcSection?.CircuitItems?.TrackCircuitGradeposts[0] != null)
+ {
+ foreach (var tcGradeItem in tcSection.CircuitItems.TrackCircuitGradeposts[0])
+ {
+ var gradepost = tcGradeItem.GradepostRef;
+ Debug.WriteLine(String.Format("Signals-TrackCircuitGradepost-Fwd: TrackCircuitSection {0}/{1}, TrackNodeIdx {2}, TrItemIdx {3}, location {4:F1}, grade = {5:F2}, for {6:F1}, direction {7}, gp-TrItemId {8}, gp-Reference {9}, gp-Offset {10}",
+ tcSection.Index, tcSection.OriginalIndex, tcGradeItem.TrackNodeIdx, tcGradeItem.TrItemIdx, tcGradeItem.GradepostLocation, gradepost.GradePct, gradepost.ForDistanceM, gradepost.Direction, gradepost.TrItemId, gradepost.TCReference, gradepost.TCOffset));
+ cnt3++;
+ }
+ }
+ else { Debug.WriteLine(String.Format("Signals-TrackCircuitGradepost-Fwd: TCIdx {0}/{1}, none", tcSection.Index, tcSection.OriginalIndex)); }
+ if (tcSection?.CircuitItems?.TrackCircuitGradeposts[1] != null)
+ {
+ foreach (var tcGradeItem in tcSection.CircuitItems.TrackCircuitGradeposts[1])
+ {
+ var gradepost = tcGradeItem.GradepostRef;
+ Debug.WriteLine(String.Format("Signals-TrackCircuitGradepost-Rev: TrackCircuitSection {0}/{1}, TrackNodeIdx {2}, TrItemIdx {3}, location {4:F1}, grade = {5:F2}, for {6:F1}, direction {7}, gp-TrItemId {8}, gp-Reference {9}, gp-Offset {10}",
+ tcSection.Index, tcSection.OriginalIndex, tcGradeItem.TrackNodeIdx, tcGradeItem.TrItemIdx, tcGradeItem.GradepostLocation, gradepost.GradePct, gradepost.ForDistanceM, gradepost.Direction, gradepost.TrItemId, gradepost.TCReference, gradepost.TCOffset));
+ cnt4++;
+ }
+ }
+ else { Debug.WriteLine(String.Format("Signals-TrackCircuitGradepost-Rev: TCIdx {0}/{1}, none", tcSection.Index, tcSection.OriginalIndex)); }
+ }
+ }
+ Debug.WriteLine(String.Format("Signals-TrackCircuitGradepost-Count: {0}/{1}", cnt3, cnt4));
+#endif
}
///
@@ -272,6 +333,9 @@ public Signals(Simulator simulator, SignalConfigurationFile sigcfg, Cancellation
public Signals(Simulator simulator, SignalConfigurationFile sigcfg, BinaryReader inf, CancellationToken cancellation)
: this(simulator, sigcfg, cancellation)
{
+ // needed in all constructors
+ GradepostList[0] = new List(); GradepostList[1] = new List();
+
int signalIndex = inf.ReadInt32();
while (signalIndex >= 0)
{
@@ -899,7 +963,8 @@ private void ScanSection(TrItem[] TrItems, TrackNode[] trackNodes, int index,
// Is it a vector node then it may contain objects.
if (trackNodes[index].TrVectorNode != null && trackNodes[index].TrVectorNode.NoItemRefs > 0)
{
- // Any objects ?
+ // for each TrItem belonging to the Track Vector Node
+ // TODO: rename index to trackNodeIdx, rename i to trItemRefIdx, rename TDBRef trItemIdx
for (int i = 0; i < trackNodes[index].TrVectorNode.NoItemRefs; i++)
{
if (TrItems[trackNodes[index].TrVectorNode.TrItemRefs[i]] != null)
@@ -964,10 +1029,16 @@ private void ScanSection(TrItem[] TrItems, TrackNode[] trackNodes, int index,
platformList.Add(TDBRef, index);
}
}
+ else if (TrItems[TDBRef].ItemType == TrItem.trItemType.trGRADEPOST)
+ {
+ GradePostItem gradepostItem = (GradePostItem)TrItems[TDBRef];
+ int gradePostIdx = AddGradepost(index, gradepostItem.GradePct, gradepostItem.ForDistanceM, gradepostItem.Direction, TDBRef);
+ gradepostItem.TcGradepostIdx = gradePostIdx;
+ }
}
}
}
- }
+ }
///
/// Merge Heads
@@ -1127,6 +1198,17 @@ private int AddMilepost(int trackNode, int nodeIndx, SpeedPostItem speedItem, in
return foundMileposts - 1;
}
+ /// This method adds a new Gradepost to the GradepostList in Signals.
+ /// The index of the gradepost added.
+ private int AddGradepost(int trackNodeIdx, float gradePct, float distance, int dir, int TDBRef)
+ {
+ Gradepost gradepost = new Gradepost((uint)TDBRef, gradePct,distance, dir);
+ gradepost.TrackNodeIdx = trackNodeIdx;
+ GradepostList[dir].Add(gradepost);
+
+ return GradepostList[dir].Count - 1;
+ }
+
///
/// Add the sigcfg reference to each signal object.
///
@@ -2130,7 +2212,7 @@ public float[] InsertNode(TrackCircuitSection thisCircuit, TrItem thisItem,
if (speedItem.SigObj >= 0)
{
if (!speedItem.IsMilePost)
- {
+ {
SignalObject thisSpeedpost = SignalObjects[speedItem.SigObj];
float speedpostDistance = thisSpeedpost.DistanceTo(TDBTrav);
if (thisSpeedpost.direction == 1)
@@ -2178,6 +2260,25 @@ public float[] InsertNode(TrackCircuitSection thisCircuit, TrItem thisItem,
}
}
}
+ // Insert gradepost
+ else if (thisItem.ItemType == TrItem.trItemType.trGRADEPOST)
+ {
+ GradePostItem gradePostItem = (GradePostItem)thisItem;
+ int direction = gradePostItem.Direction;
+
+ Gradepost gradepost = GradepostList[direction][gradePostItem.TcGradepostIdx];
+
+ float gradepostDistance = TDBTrav.DistanceTo(thisItem.TileX, thisItem.TileZ, thisItem.X, thisItem.Y, thisItem.Z);
+ // if (direction != 0) { gradepostDistance = thisCircuit.Length - gradepostDistance; }
+
+ TrackCircuitGradepost newTCGradepost = new TrackCircuitGradepost(gradepost, gradepostDistance, direction);
+ newTCGradepost.TrackNodeIdx = thisCircuit.OriginalIndex;
+ newTCGradepost.TrItemIdx = thisItem.TrItemId;
+ thisCircuit.CircuitItems.TrackCircuitGradeposts[direction].Add(newTCGradepost);
+
+ Debug.WriteLine(String.Format("Adding TrackCircuitGradepost {0} to TrackCircuitSection {1}, grade {2:F2}, for {3:F1}, direction {4}, from TrackNode {5}, TrItem {6}, named {7}",
+ thisCircuit.CircuitItems.TrackCircuitGradeposts[direction].Count - 1, thisCircuit.Index, gradePostItem.GradePct, gradePostItem.ForDistanceM, gradePostItem.Direction, thisCircuit.OriginalIndex, thisItem.TrItemId, thisItem.ItemName));
+ }
// Insert crossover in special crossover list
else if (thisItem.ItemType == TrItem.trItemType.trCROSSOVER)
{
@@ -2607,7 +2708,6 @@ private void splitSection(int orgSectionIndex, int newSectionIndex, float positi
}
// copy milepost information
-
foreach (TrackCircuitMilepost thisMilepost in orgSection.CircuitItems.TrackCircuitMileposts)
{
if (thisMilepost.MilepostLocation[0] > replSection.Length)
@@ -2622,6 +2722,40 @@ private void splitSection(int orgSectionIndex, int newSectionIndex, float positi
}
}
+ // copy forward gradepost information
+ var orgGradepostList = orgSection.CircuitItems.TrackCircuitGradeposts[0];
+ var replGradepostList = replSection.CircuitItems.TrackCircuitGradeposts[0];
+ var newGreadpostList = newSection.CircuitItems.TrackCircuitGradeposts[0];
+ foreach (TrackCircuitGradepost thisGradepost in orgGradepostList)
+ {
+ if (thisGradepost.GradepostLocation <= newSection.Length)
+ {
+ newGreadpostList.Add(thisGradepost);
+ }
+ else
+ {
+ thisGradepost.GradepostLocation -= newSection.Length;
+ replGradepostList.Add(thisGradepost);
+ }
+ }
+
+ // copy reverse gradepost information
+ orgGradepostList = orgSection.CircuitItems.TrackCircuitGradeposts[1];
+ replGradepostList = replSection.CircuitItems.TrackCircuitGradeposts[1];
+ newGreadpostList = newSection.CircuitItems.TrackCircuitGradeposts[1];
+ foreach (TrackCircuitGradepost thisGradepost in orgGradepostList)
+ {
+ if (thisGradepost.GradepostLocation > replSection.Length)
+ {
+ thisGradepost.GradepostLocation -= replSection.Length;
+ newGreadpostList.Add(thisGradepost);
+ }
+ else
+ {
+ replGradepostList.Add(thisGradepost);
+ }
+ }
+
#if ACTIVITY_EDITOR
// copy TrackCircuitElements
@@ -3016,6 +3150,28 @@ private void setSignalCrossReference(int thisNode)
thisMilepost.TCOffset = thisItem.MilepostLocation[0];
}
}
+
+ // process gradeposts, each direction
+ foreach (TrackCircuitGradepost thisItem in thisSection.CircuitItems.TrackCircuitGradeposts[0])
+ {
+ Gradepost thisGradepost = thisItem.GradepostRef;
+
+ if (thisGradepost.TCReference <= 0)
+ {
+ thisGradepost.TCReference = thisNode;
+ thisGradepost.TCOffset = thisItem.GradepostLocation;
+ }
+ }
+ foreach (TrackCircuitGradepost thisItem in thisSection.CircuitItems.TrackCircuitGradeposts[1])
+ {
+ Gradepost thisGradepost = thisItem.GradepostRef;
+
+ if (thisGradepost.TCReference <= 0)
+ {
+ thisGradepost.TCReference = thisNode;
+ thisGradepost.TCOffset = thisItem.GradepostLocation;
+ }
+ }
}
///
diff --git a/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitGradepost.cs b/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitGradepost.cs
new file mode 100644
index 0000000000..0043f4e157
--- /dev/null
+++ b/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitGradepost.cs
@@ -0,0 +1,43 @@
+// COPYRIGHT 2025 by the Open Rails project.
+//
+// This file is part of Open Rails.
+//
+// Open Rails is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Open Rails is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Open Rails. If not, see .
+
+namespace Orts.Simulation.Signalling
+{
+ ///
+ /// Represents the track circuit (signalling) view of a grade marker.
+ ///
+ public class TrackCircuitGradepost
+ {
+ /// Reference to objecty in Signals' GradepostList (by direction).
+ public Gradepost GradepostRef;
+ /// Gradepost location (distance) from the start of the section. End of section for the reverse direction.
+ public float GradepostLocation;
+ /// 0 is in track circuit direction, 1 is in reverse direction.
+ public int GradepostDirection;
+ /// Reference to grade post in TrItemTable; index into TrackDB's TrItemTable.
+ public uint TrItemIdx;
+ /// Reference to Track Node this gradepost is in; index into TrackDB's TrackNodes.
+ public int TrackNodeIdx;
+
+ public TrackCircuitGradepost(Gradepost thisRef, float distanceFromStart, int dir)
+ {
+ GradepostRef = thisRef;
+ GradepostLocation = distanceFromStart;
+ GradepostDirection = dir;
+ }
+ }
+}
diff --git a/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitItems.cs b/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitItems.cs
index a0b2790ef8..b2f725b800 100644
--- a/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitItems.cs
+++ b/Source/Orts.Simulation/Simulation/Signalling/TrackCircuitItems.cs
@@ -26,6 +26,7 @@ public class TrackCircuitItems
public Dictionary[] TrackCircuitSignals = new Dictionary[2]; // List of signals (per direction and per type) //
public TrackCircuitSignalList[] TrackCircuitSpeedPosts = new TrackCircuitSignalList[2]; // List of speedposts (per direction) //
public List TrackCircuitMileposts = new List(); // List of mileposts //
+ public List[] TrackCircuitGradeposts = new List[2]; // List of gradeposts (per direction, 0 = in track circuit direction)
#if ACTIVITY_EDITOR
// List of all Element coming from OR configuration in a generic form.
@@ -43,6 +44,7 @@ public TrackCircuitItems(IDictionary signalFunctions)
}
TrackCircuitSpeedPosts[iDirection] = new TrackCircuitSignalList();
+ TrackCircuitGradeposts[iDirection] = new List();
}
}
}
diff --git a/Source/Orts.Simulation/Simulation/Simulator.cs b/Source/Orts.Simulation/Simulation/Simulator.cs
index 1ab4a2e6d8..c96004a5b8 100644
--- a/Source/Orts.Simulation/Simulation/Simulator.cs
+++ b/Source/Orts.Simulation/Simulation/Simulator.cs
@@ -37,6 +37,7 @@
using System.Diagnostics;
using System.IO;
using Event = Orts.Common.Event;
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
namespace Orts.Simulation
{
@@ -177,6 +178,10 @@ public class Simulator
public bool OpenDoorsInAITrains;
public int ActiveMovingTableIndex = -1;
+
+ // rwf-rr: for debug
+ int TvsWithZeroLenCnt = 0;
+
public MovingTable ActiveMovingTable
{
get
@@ -326,9 +331,48 @@ public Simulator(UserSettings settings, string activityPath, bool useOpenRailsDi
if (File.Exists(RoutePath + @"\TSECTION.DAT"))
TSectionDat.AddRouteTSectionDatFile(RoutePath + @"\TSECTION.DAT");
+ // add grade info to the vector nodes
+ foreach (var trackNode in TDB.TrackDB.TrackNodes)
+ {
+ if (trackNode?.TrVectorNode != null) { trackNode.TrVectorNode.AddGradeInfo(trackNode.Index, TSectionDat.TrackSections, TvsWithZeroLenCnt); }
+ }
+
+ // create grade markers from the grade info in the vector nodes
+ foreach (var trackNode in TDB.TrackDB.TrackNodes)
+ {
+ if (trackNode?.TrVectorNode != null)
+ {
+ trackNode.TrVectorNode.ProcessForwardGradeInfoAndAddGradeposts(trackNode.Index, TDB.TrackDB);
+ trackNode.TrVectorNode.ProcessReverseGradeInfoAndAddGradeposts(trackNode.Index, TDB.TrackDB);
+ }
+ }
+
+#if DEBUG
+ // dump grade data of each track nodes
+ int gradeCnt = 0, tnCnt = 0, tvnCnt = 0, tvsCnt = 0;
+ foreach (var trackNode in TDB.TrackDB.TrackNodes)
+ {
+ if (trackNode is null) continue; // first track node in list is empty
+ tnCnt++;
+ if (trackNode.TrVectorNode == null) continue; // only vector nodes have grades
+ tvnCnt++;
+ if (trackNode.TrVectorNode.GradeList is null) { Debug.WriteLine(String.Format("Track-GradeData: TrackNode = {0}, none", trackNode.Index)); }
+ else {
+ tvsCnt += trackNode.TrVectorNode.TrVectorSections.Length;
+ foreach (var gradeSegment in trackNode.TrVectorNode.GradeList)
+ {
+ Debug.WriteLine(String.Format("Track-GradeData: TrackNode = {0}, idx = {1}, grade = {2:F2}, length = {3:F1}, distance = {4:F1}, TX = {5}, TZ = {6}",
+ trackNode.Index, gradeCnt, gradeSegment.GradePct, gradeSegment.LengthM, gradeSegment.DistanceFromStartM, gradeSegment.TileX, gradeSegment.TileZ));
+ gradeCnt++;
+ }
+ }
+ }
+ Debug.WriteLine(String.Format("Track-GradeData-Count: grades {0}, vectorSections {1}, vectorNodes {2}, trackNodes {3}; zero-length-sections {4}", gradeCnt, tvsCnt, tvnCnt, tnCnt, TvsWithZeroLenCnt));
+#endif
+
#if ACTIVITY_EDITOR
- // Where we try to load OR's specific data description (Station, connectors, etc...)
- orRouteConfig = ORRouteConfig.LoadConfig(TRK.Tr_RouteFile.FileName, RoutePath, TypeEditor.NONE);
+ // Where we try to load OR's specific data description (Station, connectors, etc...)
+ orRouteConfig = ORRouteConfig.LoadConfig(TRK.Tr_RouteFile.FileName, RoutePath, TypeEditor.NONE);
orRouteConfig.SetTraveller(TSectionDat, TDB);
#endif
@@ -372,6 +416,23 @@ public Simulator(UserSettings settings, string activityPath, bool useOpenRailsDi
ContainerManager = new ContainerManager(this);
ScriptManager = new ScriptManager();
Log = new CommandLog(this);
+
+#if DEBUG
+ // dump track items of type grade post
+ if (TDB.TrackDB.TrItemTable != null)
+ {
+ int cnt2 = 0;
+ foreach (var trItem in TDB.TrackDB.TrItemTable)
+ {
+ if (!(trItem is GradePostItem)) continue;
+ GradePostItem gradePost = (GradePostItem)trItem;
+ Debug.WriteLine(String.Format("Track-GradePostItem: TrackNode = {0}, TrItemId = {1}, grade = {2:F2}, for = {3:F1}, distance = {4:F1}, direction {5}, TX = {6}, TZ = {7}, name = {8}",
+ gradePost.TrackNodeIndex, gradePost.TrItemId, gradePost.GradePct, gradePost.ForDistanceM, gradePost.DistanceFromStartM, gradePost.Direction, gradePost.TileX, gradePost.TileZ, gradePost.ItemName));
+ cnt2++;
+ }
+ Debug.WriteLine(String.Format("Track-GradePostItem-Count: {0}", cnt2));
+ }
+#endif
}
public void SetActivity(string activityPath)
@@ -1415,6 +1476,56 @@ private Train InitializePlayerTrain()
// if ((PlayerLocomotive as MSTSLocomotive).EOTEnabled != MSTSLocomotive.EOTenabled.no)
// train.EOT = new EOT((PlayerLocomotive as MSTSLocomotive).EOTEnabled, false, train);
+#if DEBUG
+ // dump path, for now just grade posts
+ float distanceFromPathStart = 0f; // does not account for offset of path start
+ int maxNodes = aiPath.Nodes.Count; // limit, in case there is a loop
+ AIPathNode currentPathNode = aiPath.FirstNode;
+ while (currentPathNode != null && maxNodes >= 0)
+ {
+ if (!currentPathNode.IsIntermediateNode)
+ {
+ // only follow the main path
+ int tvnIdx = currentPathNode.NextMainTVNIndex;
+ if (tvnIdx > 0)
+ {
+ TrackNode trackNode = TDB.TrackDB.TrackNodes[tvnIdx];
+ TrVectorNode vectorNode = trackNode.TrVectorNode;
+
+ int trackDirection = 0;
+ if (currentPathNode.JunctionIndex > 0 && trackNode.TrPins[1].Link == currentPathNode.JunctionIndex) { trackDirection = 1; }
+ else if (currentPathNode.NextMainNode.JunctionIndex > 0 && trackNode.TrPins[0].Link == currentPathNode.NextMainNode.JunctionIndex) { trackDirection = 1; }
+
+ // for now assuming that gradeposts (their refs) are in distance order
+ bool foundGradepost = false;
+ for (int refIdx = 0; refIdx < vectorNode.NoItemRefs; refIdx++)
+ {
+ int trItemIdx = vectorNode.TrItemRefs[refIdx];
+ TrItem item = TDB.TrackDB.TrItemTable[trItemIdx];
+ if (item is GradePostItem)
+ {
+ GradePostItem gpItem = (GradePostItem)item;
+ if (gpItem.Direction == trackDirection)
+ {
+ float distanceInNode = gpItem.DistanceFromStartM;
+ Debug.WriteLine("TrainInit-Gradepost: TrackNodeIdx {0}, RefIdx {1}, ItemIdx {2}, estFromPathStart {3:F0}, fromNodeStart {4:F0}, grade {5:F1}, length {6:F0}",
+ gpItem.TrackNodeIndex, refIdx, gpItem.TrItemId, distanceFromPathStart + distanceInNode, distanceInNode, gpItem.GradePct, gpItem.ForDistanceM);
+ foundGradepost = true;
+ }
+ }
+ }
+
+ if (!foundGradepost) { Debug.WriteLine("Gradepost: TrackNode {0}, no gradeposts in {1} items", tvnIdx, vectorNode.NoItemRefs); }
+
+ distanceFromPathStart += vectorNode.LengthM;
+ }
+ }
+
+ currentPathNode = currentPathNode.NextMainNode;
+ maxNodes--;
+ }
+#endif
+
return (train);
}
diff --git a/Source/RunActivity/Viewer3D/Popups/TrackMonitorWindow.cs b/Source/RunActivity/Viewer3D/Popups/TrackMonitorWindow.cs
index 816b5b5316..718a4a8c47 100644
--- a/Source/RunActivity/Viewer3D/Popups/TrackMonitorWindow.cs
+++ b/Source/RunActivity/Viewer3D/Popups/TrackMonitorWindow.cs
@@ -71,7 +71,7 @@ public class TrackMonitorWindow : Window
};
public TrackMonitorWindow(WindowManager owner)
- : base(owner, Window.DecorationSize.X + owner.TextFontDefault.Height * 10, Window.DecorationSize.Y + owner.TextFontDefault.Height * (5 + TrackMonitorHeightInLinesOfText) + ControlLayout.SeparatorSize * 3, Viewer.Catalog.GetString("Track Monitor"))
+ : base(owner, Window.DecorationSize.X + owner.TextFontDefault.Height * 12, Window.DecorationSize.Y + owner.TextFontDefault.Height * (5 + TrackMonitorHeightInLinesOfText) + ControlLayout.SeparatorSize * 3, Viewer.Catalog.GetString("Track Monitor"))
{
ControlModeLabels = new Dictionary
{
@@ -116,7 +116,7 @@ protected override ControlLayout Layout(ControlLayout layout)
vbox.AddHorizontalSeparator();
{
var hbox = vbox.AddLayoutHorizontalLineOfText();
- hbox.Add(new Label(hbox.RemainingWidth, hbox.RemainingHeight, Viewer.Catalog.GetString(" Milepost Limit Dist")));
+ hbox.Add(new Label(hbox.RemainingWidth, hbox.RemainingHeight, Viewer.Catalog.GetString(" Milepost Grade Limit Dist")));
}
vbox.AddHorizontalSeparator();
vbox.Add(Monitor = new TrackMonitor(vbox.RemainingWidth, vbox.RemainingHeight, Owner));
@@ -222,7 +222,15 @@ public enum DisplayMode
Train.TrainInfo validInfo;
- const int DesignWidth = 150; // All Width/X values are relative to this width.
+ // default dimensions and positions
+ const int DesignWidth = 192; // All Width/X values are relative to this width.
+ const int DfltMilepostTextOffset = 0;
+ const int DfltGradPostTextOffset = 42;
+ const int DfltArrowOffset = 64;
+ const int DfltTrackOffset = 84;
+ const int DfltSpeedTextOffset = 112;
+ const int DfltSignalOffset = 137;
+ const int DfltDistanceTextOffset = 159;
// position constants
readonly int additionalInfoHeight = 16; // vertical offset on window for additional out-of-range info at top and bottom
@@ -237,11 +245,12 @@ public enum DisplayMode
// Vertical offset for text for forwards ([0]) and backwards ([1]).
readonly int[] textOffset = new int[2] { -11, -3 };
- // Horizontal offsets for various elements.
- readonly int distanceTextOffset = 117;
- readonly int trackOffset = 42;
- readonly int speedTextOffset = 70;
- readonly int milepostTextOffset = 0;
+ // Horizontal offsets for various elements. Will be scaled.
+ readonly int distanceTextOffset = DfltDistanceTextOffset;
+ readonly int trackOffset = DfltTrackOffset;
+ readonly int speedTextOffset = DfltSpeedTextOffset;
+ readonly int milepostTextOffset = DfltMilepostTextOffset;
+ readonly int gradepostTextOffset = DfltGradPostTextOffset;
// position definition arrays
// contents :
@@ -251,18 +260,18 @@ public enum DisplayMode
// cell 3 : X size
// cell 4 : Y size
- int[] eyePosition = new int[5] { 42, -4, -20, 24, 24 };
- int[] trainPosition = new int[5] { 42, -12, -12, 24, 24 }; // Relative positioning
- int[] otherTrainPosition = new int[5] { 42, -24, 0, 24, 24 }; // Relative positioning
- int[] stationPosition = new int[5] { 42, 0, -24, 24, 12 }; // Relative positioning
- int[] reversalPosition = new int[5] { 42, -21, -3, 24, 24 }; // Relative positioning
- int[] waitingPointPosition = new int[5] { 42, -21, -3, 24, 24 }; // Relative positioning
- int[] endAuthorityPosition = new int[5] { 42, -14, -10, 24, 24 }; // Relative positioning
- int[] signalPosition = new int[5] { 95, -16, 0, 16, 16 }; // Relative positioning
- int[] arrowPosition = new int[5] { 22, -12, -12, 24, 24 };
- int[] invalidReversalPosition = new int[5] { 42, -14, -10, 24, 24 }; // Relative positioning
- int[] leftSwitchPosition = new int[5] { 37, -14, -10, 24, 24 }; // Relative positioning
- int[] rightSwitchPosition = new int[5] { 47, -14, -10, 24, 24 }; // Relative positioning
+ int[] eyePosition = new int[5] { DfltTrackOffset, -4, -20, 24, 24 };
+ int[] trainPosition = new int[5] { DfltTrackOffset, -12, -12, 24, 24 }; // Relative positioning
+ int[] otherTrainPosition = new int[5] { DfltTrackOffset, -24, 0, 24, 24 }; // Relative positioning
+ int[] stationPosition = new int[5] { DfltTrackOffset, 0, -24, 24, 12 }; // Relative positioning
+ int[] reversalPosition = new int[5] { DfltTrackOffset, -21, -3, 24, 24 }; // Relative positioning
+ int[] waitingPointPosition = new int[5] { DfltTrackOffset, -21, -3, 24, 24 }; // Relative positioning
+ int[] endAuthorityPosition = new int[5] { DfltTrackOffset, -14, -10, 24, 24 }; // Relative positioning
+ int[] signalPosition = new int[5] { DfltSignalOffset, -16, 0, 16, 16 }; // Relative positioning
+ int[] arrowPosition = new int[5] { DfltArrowOffset, -12, -12, 24, 24 };
+ int[] invalidReversalPosition = new int[5] { DfltTrackOffset, -14, -10, 24, 24 }; // Relative positioning
+ int[] leftSwitchPosition = new int[5] { DfltTrackOffset - 5, -14, -10, 24, 24 }; // Relative positioning
+ int[] rightSwitchPosition = new int[5] { DfltTrackOffset + 5, -14, -10, 24, 24 }; // Relative positioning
// texture rectangles : X-offset, Y-offset, width, height
Rectangle eyeSprite = new Rectangle(0, 144, 24, 24);
@@ -331,6 +340,8 @@ public TrackMonitor(int width, int height, WindowManager owner)
ScaleDesign(ref distanceTextOffset);
ScaleDesign(ref trackOffset);
ScaleDesign(ref speedTextOffset);
+ ScaleDesign(ref milepostTextOffset);
+ ScaleDesign(ref gradepostTextOffset);
ScaleDesign(ref eyePosition);
ScaleDesign(ref trainPosition);
@@ -672,6 +683,7 @@ void drawItems(SpriteBatch spriteBatch, Point offset, int startObjectArea, int e
var signalShown = false;
var firstLabelShown = false;
var borderSignalShown = false;
+ float precedingGradePct = forward ? -validInfo.currentElevationPercent : validInfo.currentElevationPercent;
foreach (var thisItem in itemList)
{
@@ -705,6 +717,10 @@ void drawItems(SpriteBatch spriteBatch, Point offset, int startObjectArea, int e
lastLabelPosition = drawMilePost(spriteBatch, offset, startObjectArea, endObjectArea, zeroPoint, maxDistance, distanceFactor, firstLabelPosition, forward, lastLabelPosition, thisItem, ref firstLabelShown);
break;
+ case Train.TrainObjectItem.TRAINOBJECTTYPE.GRADEPOST:
+ drawGradePost(spriteBatch, offset, startObjectArea, endObjectArea, zeroPoint, maxDistance, distanceFactor, firstLabelPosition, forward, lastLabelPosition, thisItem, ref firstLabelShown, ref precedingGradePct);
+ break;
+
case Train.TrainObjectItem.TRAINOBJECTTYPE.FACING_SWITCH:
drawSwitch(spriteBatch, offset, startObjectArea, endObjectArea, zeroPoint, maxDistance, distanceFactor, firstLabelPosition, forward, lastLabelPosition, thisItem, ref firstLabelShown);
break;
@@ -1031,6 +1047,42 @@ int drawMilePost(SpriteBatch spriteBatch, Point offset, int startObjectArea, int
return newLabelPosition;
}
+ ///
+ /// Draw Grade.
+ ///
+ int drawGradePost(SpriteBatch spriteBatch, Point offset, int startObjectArea, int endObjectArea, int zeroPoint, float maxDistance, float distanceFactor, int firstLabelPosition, bool forward, int lastLabelPosition, Train.TrainObjectItem thisItem, ref bool firstLabelShown, ref float precedingGradePct)
+ {
+ var newLabelPosition = lastLabelPosition;
+
+ if (thisItem.DistanceToTrainM < (maxDistance - textSpacing / distanceFactor))
+ {
+ var itemOffset = Convert.ToInt32(thisItem.DistanceToTrainM * distanceFactor);
+ var itemLocation = forward ? zeroPoint - itemOffset : zeroPoint + itemOffset;
+ newLabelPosition = forward ? Math.Min(itemLocation, lastLabelPosition - textSpacing) : Math.Max(itemLocation, lastLabelPosition + textSpacing);
+ var labelPoint = new Point(offset.X + gradepostTextOffset, offset.Y + newLabelPosition + textOffset[forward ? 0 : 1]);
+ // TODO: support promille or 1-in-x
+ String gradeStr; var gradeColor = Color.White; char trendChar;
+ if (Math.Abs(thisItem.GradePct - precedingGradePct) < 0.1f) { trendChar = ' '; }
+ else if (precedingGradePct < thisItem.GradePct) { trendChar = '\u2197'; }
+ else { trendChar = '\u2198'; }
+ if (thisItem.GradePct < -0.00015)
+ {
+ gradeStr = String.Format("{0:F1}% {1} ", thisItem.GradePct, trendChar);
+ gradeColor = Color.LightSkyBlue;
+ }
+ else if (thisItem.GradePct > 0.00015)
+ {
+ gradeStr = String.Format("+{0:F1}% {1} ", thisItem.GradePct, trendChar);
+ gradeColor = Color.Yellow;
+ }
+ else { gradeStr = String.Format(" 0% {1} ", thisItem.GradePct, trendChar); }
+ Font.Draw(spriteBatch, labelPoint, gradeStr, gradeColor);
+ precedingGradePct = thisItem.GradePct;
+ }
+
+ return newLabelPosition;
+ }
+
// draw switch information
int drawSwitch(SpriteBatch spriteBatch, Point offset, int startObjectArea, int endObjectArea, int zeroPoint, float maxDistance, float distanceFactor, float firstLabelDistance, bool forward, int lastLabelPosition, Train.TrainObjectItem thisItem, ref bool firstLabelShown)
{