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) {