internal static Leg[] CreateLegs(PathItem[] items) { // Count the number of legs. int numLeg = PathItem.GetMaxLegNumber(items); if (numLeg == 0) throw new Exception("PathParser.Create -- No connection legs"); List<Leg> result = new List<Leg>(numLeg); // Create each leg. int legnum = 0; // Current leg number int nexti = 0; // Index of the start of the next leg for (int si = 0; si < items.Length; si = nexti) { // Skip if no leg number (could be new units spec). if (items[si].LegNumber == 0) { nexti = si + 1; continue; } // Confirm the leg count is valid. if (legnum + 1 > numLeg) throw new Exception("PathParser.Create -- Bad number of path legs."); // Create the leg. Leg newLeg; if (items[si].ItemType == PathItemType.BC) newLeg = CreateCircularLeg(items, si, out nexti); else newLeg = CreateStraightLeg(items, si, out nexti); // Exit if we failed to create the leg. if (newLeg == null) throw new Exception("PathParser.Create -- Unable to create leg"); result.Add(newLeg); } // Confirm we created the number of legs we expected. if (numLeg != result.Count) throw new Exception("PathParser.Create -- Unexpected number of legs"); return result.ToArray(); }
/// <summary> /// Copy constructor /// </summary> /// <param name="copy">The item to copy from</param> internal PathItem(PathItem copy) { m_Item = copy.m_Item; m_Unit = copy.m_Unit; m_Value = copy.m_Value; m_Leg = copy.m_Leg; }
/// <summary> /// Obtains the highest leg number in the supplied item array. /// </summary> /// <param name="items">Array of path items.</param> /// <returns>The maximum value for the leg number in the supplied items.</returns> internal static int GetMaxLegNumber(PathItem[] items) { // Each path item contains a leg number, arranged sequentially. int nleg=0; foreach (PathItem item in items) nleg = Math.Max(nleg, item.LegNumber); return nleg; }
/// <summary> /// Repeats the last path item a specific number of times. The thing to /// repeat HAS to be of type PAT_VALUE (or possibly a PAT_MC that has been /// automatically inserted by <see cref="AddItem"/>). /// </summary> /// <param name="repeat">The number of times the last item should appear (we /// will append n-1 copies of the last item).</param> void AddRepeats(int repeat) { // Confirm that we have something to repeat. if (m_Items.Count == 0) throw new ApplicationException("Nothing to repeat"); // If the last item was a PAT_MC, get to the value before that. int prev = m_Items.Count - 1; PathItemType type = m_Items[prev].ItemType; if (type == PathItemType.MissConnect && prev > 0) { prev--; type = m_Items[prev].ItemType; } // It can only be a PAT_VALUE. if (type != PathItemType.Value) throw new ApplicationException("Unexpected repeat multiplier"); // Make copies of the last value. for (int i = 1; i < repeat; i++) { PathItem copy = new PathItem(m_Items[prev]); AddItem(copy); } }
void ParseWord(string str) { // Return if string is empty (could be empty if this function // has been called recursively from below). str = str.Trim(); int nc = str.Length; if (nc == 0) return; // If we have a new default units specification, make it // the default. There should be whitespace after the "..." if (str.Contains("...")) { DistanceUnit unit = GetUnits(str, true); PathItem item = new PathItem(PathItemType.Units, unit, 0.0); AddItem(item); return; } // If we have a counter-clockwise indicator, just remember it // and parse anything that comes after it. if (nc >= 2 && String.Compare(str.Substring(0, 2), "cc", true) == 0) { AddItem(PathItemType.CounterClockwise); ParseWord(str.Substring(2)); return; } // If we have a BC, remember it & parse anything that follows. if (str[0] == '(') { AddItem(PathItemType.BC); ParseWord(str.Substring(1)); return; } // If we have a EC, remember it & parse anything that follows. if (str[0] == ')') { AddItem(PathItemType.EC); ParseWord(str.Substring(1)); return; } // If we have a single slash character (possibly followed by // a digit or a decimal point), record the single slash & // parse anything that follows. if (str[0] == '/') { // Check for a free-standing slash, or a slash that is // followed by a numeric digit or decimal point. if (nc == 1 || Char.IsDigit(str, 1) || str[1] == '.') { AddItem(PathItemType.Slash); ParseWord(str.Substring(1)); return; } // More than one character, or what follows is not a digit. // So we are dealing with either a miss-connect, or an // omit-point. In either case, there should be whitespace // after that. if (nc == 2) { if (str[1] == '-') { AddItem(PathItemType.MissConnect); return; } if (str[1] == '*') { AddItem(PathItemType.OmitPoint); return; } } // Allow CADCOR-style data entry else if (nc == 3) { if (String.Compare(str, "/mc", true) == 0) { AddItem(PathItemType.MissConnect); return; } if (String.Compare(str, "/op", true) == 0) { AddItem(PathItemType.OmitPoint); return; } } string msg = String.Format("Unexpected qualifier '{0}'", str); throw new ApplicationException(msg); } // If we have a multiplier, it must be immediately followed // by a numeric (integer) value. if (str[0] == '*') { if (nc == 1) throw new ApplicationException("Unexpected '*' character"); // Pick up the repeat count (not sure if the digits need to be // followed by white space, or whether non-numeric digits are valid, // so pick up only the digits). string num = GetIntDigits(str.Substring(1)); // Error if repeat count is less than 2. int repeat; if (!Int32.TryParse(num, out repeat) || repeat < 2) { string msg = String.Format("Unexpected repeat count in '{0}'", str); throw new ApplicationException(msg); } if (repeat < 2) { string msg = String.Format("Unexpected repeat count in '{0}'", str); throw new ApplicationException(msg); } // Duplicate the last item using the repeat count. AddRepeats(repeat); // Continue parsing after the repeat count. ParseWord(str.Substring(1+num.Length)); return; } // If the string contains an embedded qualifier (a "*" or a "/" // character), process the portion of any string prior to the // qualifier. Note that we have just handled the cases where // the qualifier was at the very start of the string. int starIndex = str.IndexOf('*'); int slashIndex = str.IndexOf('/'); if (starIndex >= 0 || slashIndex >= 0) { int qualIndex = starIndex; if (qualIndex < 0 || (slashIndex>=0 && qualIndex > slashIndex)) qualIndex = slashIndex; // Process the stuff prior to the qualifier. string copy = str.Substring(0, qualIndex); ParseWord(copy); // Process the stuff, starting with the qualifier character ParseWord(str.Substring(qualIndex)); return; } // Process this string. We should have either a value or an angle. if (str.IndexOf('-') >= 0 || IsLastItemBC()) { // If the string contains a "c" character, it's a central // angle; process the string only as far as that. PathItemType type = PathItemType.Angle; int caIndex = str.ToUpper().IndexOf('C'); if (caIndex>=0) { str = str.Substring(0, caIndex); type = PathItemType.CentralAngle; } else { // Check if it's a deflection (if so, strip out the "d"). int dIndex = str.ToUpper().IndexOf('D'); if (dIndex >= 0) { str = str.Substring(0, dIndex) + str.Substring(dIndex + 1); type = PathItemType.Deflection; } } // Try to parse an angular value into radians. double radval; if (RadianValue.TryParse(str, out radval)) { PathItem item = new PathItem(type, null, radval); AddItem(item); return; } // Bad angle. string msg = String.Format("Malformed angle '{0}'", str); throw new ApplicationException(msg); } else { // Get the current distance units. DistanceUnit unit = GetUnits(null, false); // Grab characters that look like a floating point number string num = GetDoubleDigits(str); double val; if (!Double.TryParse(num, out val)) { string msg = String.Format("Malformed value '{0}'", str); throw new ApplicationException(msg); } // If we didn't get right to the end, we may have distance units, // or the ")" character indicating an EC. if (num.Length < str.Length && str[num.Length] != ')') { unit = GetUnits(str.Substring(num.Length), false); if (unit == null) { string msg = String.Format("Malformed value '{0}'", str); throw new ApplicationException(msg); } } PathItem item = new PathItem(PathItemType.Value, unit, val); AddItem(item); if (str.Length>num.Length && str[num.Length] == ')') ParseWord(str.Substring(num.Length)); return; } }
/// <summary> /// Holds on to an additional path item. /// </summary> /// <param name="item">The item to add.</param> void AddItem(PathItem item) { // If no items have been added, ensure omitted flag has been // freshly initialized. if (m_Items.Count == 0) m_Omit = false; // Ignore an attempt to add 2 miss-connects in a row (ValidPath // will complain). PathItemType type = item.ItemType; if (m_Items.Count > 0 && type == PathItemType.MissConnect && m_Items[m_Items.Count - 1].ItemType == PathItemType.MissConnect) return; // Add the supplied item into the list. m_Items.Add(item); // If we have just appended a PAT_VALUE, append an additional // miss-connect item if we previously omitted a point. if (m_Omit && type == PathItemType.Value) { m_Omit = false; AddItem(PathItemType.MissConnect); } // Remember whether we just omitted a point. if (type == PathItemType.OmitPoint) m_Omit = true; }
/// <summary> /// Holds on to an additional path item. Good for items that /// do not have an associated value. /// </summary> /// <param name="type">The type of item to add.</param> void AddItem(PathItemType type) { PathItem item = new PathItem(type, null, 0.0); AddItem(item); }
/// <summary> /// Creates a straight leg. /// </summary> /// <param name="items">Array of path items.</param> /// <param name="si">Index to the item where the leg data starts.</param> /// <param name="nexti">Index of the item where the next leg starts.</param> /// <returns>The new leg.</returns> static StraightLeg CreateStraightLeg(PathItem[] items, int si, out int nexti) { // Get the leg ID. int legnum = items[si].LegNumber; // How many distances have we got? int ndist = 0; for (nexti = si; nexti < items.Length && items[nexti].LegNumber == legnum; nexti++) { if (items[nexti].IsDistance) ndist++; } // Create the leg. StraightLeg leg = new StraightLeg(ndist); // Assign each distance. ndist = 0; for (int i = si; i < nexti; i++) { Distance d = items[i].GetDistance(); if (d != null) { // See if there is a qualifier after the distance LegItemFlag qual = LegItemFlag.Null; if ((i + 1) < nexti) { PathItemType nexttype = items[i + 1].ItemType; if (nexttype == PathItemType.MissConnect) qual = LegItemFlag.MissConnect; if (nexttype == PathItemType.OmitPoint) qual = LegItemFlag.OmitPoint; } leg.PrimaryFace.SetDistance(d, ndist, qual); ndist++; } } // If the first item is an angle, remember it as part of the leg. if (items[si].ItemType == PathItemType.Angle) leg.StartAngle = items[si].Value; else if (items[si].ItemType == PathItemType.Deflection) leg.SetDeflection(items[si].Value); // Return a reference to the new leg return leg; }
/// <summary> /// Creates a circular leg. /// </summary> /// <param name="items">Array of path items.</param> /// <param name="si">Index to the item where the leg data starts.</param> /// <param name="nexti">Index of the item where the next leg starts.</param> /// <returns>The new leg.</returns> static CircularLeg CreateCircularLeg(PathItem[] items, int si, out int nexti) { // Confirm that the first item refers to the BC. if (items[si].ItemType != PathItemType.BC) throw new Exception("PathParser.CreateCircularLeg - Not starting at BC"); // The BC has to be followed by at least 3 items: angle, radius // and EC (add an extra 1 to account for 0-based indexing). if (items.Length < si + 4) throw new Exception("PathParser.CreateCircularLeg - Insufficient curve data"); double bangle = 0.0; // Angle at BC double cangle = 0.0; // Central angle double eangle = 0.0; // Angle at EC bool twoangles = false; // True if bangle & eangle are both defined. bool clockwise = true; // True if curve is clockwise int irad = 0; // Index of the radius item bool cul = false; // True if cul-de-sac case // Point to item following the BC. nexti = si + 1; PathItemType type = items[nexti].ItemType; // If the angle following the BC is a central angle if (type == PathItemType.CentralAngle) { // We have a cul-de-sac cul = true; // Get the central angle. cangle = items[nexti].Value; nexti++; } else if (type == PathItemType.BcAngle) { // Get the entry angle. bangle = items[nexti].Value; nexti++; // Does an exit angle follow? if (items[nexti].ItemType == PathItemType.EcAngle) { eangle = items[nexti].Value; twoangles = true; nexti++; } } else { // The field after the BC HAS to be an angle. throw new ApplicationException("Angle does not follow BC"); } // Must be followed by radius. if (items[nexti].ItemType != PathItemType.Radius) throw new ApplicationException("Radius does not follow angle"); // Get the radius Distance radius = items[nexti].GetDistance(); irad = nexti; nexti++; // The item after the radius indicates whether the curve is counterclockwise. if (items[nexti].ItemType == PathItemType.CounterClockwise) { nexti++; clockwise = false; } // Get the leg ID. int legnum = items[si].LegNumber; // How many distances have we got? int ndist = 0; for (; nexti < items.Length && items[nexti].LegNumber == legnum; nexti++) { if (items[nexti].IsDistance) ndist++; } // Create the leg. CircularLeg leg = new CircularLeg(radius, clockwise, ndist); CircularLegMetrics metrics = leg.Metrics; // Set the entry angle or the central angle, depending on what we have. if (cul) metrics.SetCentralAngle(cangle); else metrics.SetEntryAngle(bangle); // Assign second angle if we have one. if (twoangles) metrics.SetExitAngle(eangle); // Assign each distance, starting one after the radius. ndist = 0; for (int i = irad + 1; i < nexti; i++) { Distance dist = items[i].GetDistance(); if (dist != null) { // See if there is a qualifier after the distance LegItemFlag qual = LegItemFlag.Null; if (i + 1 < nexti) { PathItemType nexttype = items[i + 1].ItemType; if (nexttype == PathItemType.MissConnect) qual = LegItemFlag.MissConnect; if (nexttype == PathItemType.OmitPoint) qual = LegItemFlag.OmitPoint; } leg.PrimaryFace.SetDistance(dist, ndist, qual); ndist++; } } // Return the new leg. return leg; }
/// <summary> /// Validates path items. Prior to call, the path should be parsed by /// making a series of calls to ParseWord. This generates a set of /// items that are generated without consideration to their context. /// This function validates the context, elaborating on the meaning /// of PAT_ANGLE and PAT_VALUE item codes. /// </summary> /// <returns></returns> void ValidPath() { // The path must contain at least one item. if (m_Items == null || m_Items.Count == 0) { throw new Exception("Path has not been specified."); } // All PAT_VALUE's outside of a curve definition should have // type PAT_DISTANCE. Within curves, it's a bit more complicated. int ibc = 0; // Index of last BC bool curve = false; // Not in a curve to start with for (int i = 0; i < m_Items.Count; i++) { PathItem item = m_Items[i]; switch (item.ItemType) { case PathItemType.BC: { // If we have a BC, confirm that we are not already in // a curve. Also confirm that there are at least 3 items // after the BC (enough for an angle, a radius, and an EC). if (curve) { throw new ApplicationException("Nested curve detected"); } curve = true; if ((ibc + 4) > m_Items.Count) { throw new ApplicationException("BC not followed by angle, radius, and EC"); } ibc = i; break; } case PathItemType.EC: { // If we have an EC, confirm that we were in a curve. if (!curve) { throw new ApplicationException("EC was not preceded by BC"); } curve = false; break; } case PathItemType.Value: { // If not in a curve, change all PAT_VALUE types to // PAT_DISTANCE types. Inside a curve, PAT_VALUES may // actually be angles that need to be converted into // radians. // All values must point to the data entry units. if (item.Units == null) { throw new ApplicationException("Value has no unit of measurement"); } if (!curve) { item.ItemType = PathItemType.Distance; } else { // The value immediately after the BC is always an angle. if (i == (ibc + 1)) { item.ItemType = PathItemType.BcAngle; item.Value *= MathConstants.DEGTORAD; } else if (i == (ibc + 2)) { // Could be an angle, or a radius. If the NEXT // item is a value, we must have an exit angle. if (m_Items[i + 1].ItemType == PathItemType.Value) { item.ItemType = PathItemType.EcAngle; item.Value *= MathConstants.DEGTORAD; } else { item.ItemType = PathItemType.Radius; } } else if (i == (ibc + 3)) { item.ItemType = PathItemType.Radius; } else { item.ItemType = PathItemType.Distance; } } break; } case PathItemType.Deflection: case PathItemType.Angle: { // Angles inside curve definitions have to be qualified. If // they appear, they MUST follow immediately after the BC. // For angles NOT in a curve, you can only have one angle // at a time. if (curve) { // Can't have deflections inside a curve. if (item.ItemType == PathItemType.Deflection) { throw new ApplicationException("Deflection not allowed within curve definition"); } if (i == (ibc + 1)) { item.ItemType = PathItemType.BcAngle; } else if (i == (ibc + 2)) { item.ItemType = PathItemType.EcAngle; } else { throw new ApplicationException("Extraneous angle inside curve definition"); } } else { if (i > 0 && m_Items[i - 1].ItemType == PathItemType.Angle) { throw new ApplicationException("More than 1 angle at the end of a straight"); } // Also, it makes no sense to have an angle right after an EC. if (i > 0 && m_Items[i - 1].ItemType == PathItemType.EC) { throw new ApplicationException("Angle after EC makes no sense"); } } break; } case PathItemType.Slash: { // A free-standing slash character is only valid within // a curve definition. It has to appear at a specific // location in the sequence. // // BC -> BCAngle -> Radius -> Slash // BC -> BCAngle -> Radius -> CCMarker -> Slash // BC -> BCAngle -> ECAngle -> Radius -> CCMarker -> Slash // // In other words, it can come at ibc+3 through ibc+5. if (!curve) { throw new ApplicationException("Extraneous '/' character"); } if (i < ibc + 3 || i > ibc + 5) { throw new ApplicationException("Misplaced '/' character"); } break; } case PathItemType.CounterClockwise: { // Counter-clockwise indicator. Similar to PAT_SLASH, it has // a specific range of valid positions with respect to the BC. if (!curve) { throw new ApplicationException("Counter-clockwise indicator detected outside curve definition"); } if (i < ibc + 3 || i > ibc + 4) { throw new ApplicationException("Misplaced 'cc' characters"); } break; } case PathItemType.CentralAngle: { // A central angle is valid only within a curve definition // and must be immediately after the BC. if (!curve) { throw new ApplicationException("Central angle detected outside curve definition"); } if (i != ibc + 1) { throw new ApplicationException("Central angle does not follow immediately after BC"); } break; } case PathItemType.MissConnect: case PathItemType.OmitPoint: { // Miss-connections & omit points must always follow on from a PAT_DISTANCE. if (i == 0 || m_Items[i - 1].ItemType != PathItemType.Distance) { throw new ApplicationException("Miss-Connect or Omit-Point is not preceded by a distance"); } break; } case PathItemType.Units: { // No checks break; } default: { // All item types generated via ParseWord should have been // listed above, even if there is no check. If any got missed, // drop through to a message, but keep going. string msg = String.Format("PathForm.ValidPath - Unhandled check for {0}", item.ItemType); throw new Exception(msg); break; } } // end switch } // next item // Error if we got to the end, and any curve was not closed. if (curve) { throw new ApplicationException("Circular arc does not have an EC"); } }
void ParseWord(string str) { // Return if string is empty (could be empty if this function // has been called recursively from below). str = str.Trim(); int nc = str.Length; if (nc == 0) { return; } // If we have a new default units specification, make it // the default. There should be whitespace after the "..." if (str.Contains("...")) { DistanceUnit unit = GetUnits(str, true); PathItem item = new PathItem(PathItemType.Units, unit, 0.0); AddItem(item); return; } // If we have a counter-clockwise indicator, just remember it // and parse anything that comes after it. if (nc >= 2 && String.Compare(str.Substring(0, 2), "cc", true) == 0) { AddItem(PathItemType.CounterClockwise); ParseWord(str.Substring(2)); return; } // If we have a BC, remember it & parse anything that follows. if (str[0] == '(') { AddItem(PathItemType.BC); ParseWord(str.Substring(1)); return; } // If we have a EC, remember it & parse anything that follows. if (str[0] == ')') { AddItem(PathItemType.EC); ParseWord(str.Substring(1)); return; } // If we have a single slash character (possibly followed by // a digit or a decimal point), record the single slash & // parse anything that follows. if (str[0] == '/') { // Check for a free-standing slash, or a slash that is // followed by a numeric digit or decimal point. if (nc == 1 || Char.IsDigit(str, 1) || str[1] == '.') { AddItem(PathItemType.Slash); ParseWord(str.Substring(1)); return; } // More than one character, or what follows is not a digit. // So we are dealing with either a miss-connect, or an // omit-point. In either case, there should be whitespace // after that. if (nc == 2) { if (str[1] == '-') { AddItem(PathItemType.MissConnect); return; } if (str[1] == '*') { AddItem(PathItemType.OmitPoint); return; } } // Allow CADCOR-style data entry else if (nc == 3) { if (String.Compare(str, "/mc", true) == 0) { AddItem(PathItemType.MissConnect); return; } if (String.Compare(str, "/op", true) == 0) { AddItem(PathItemType.OmitPoint); return; } } string msg = String.Format("Unexpected qualifier '{0}'", str); throw new ApplicationException(msg); } // If we have a multiplier, it must be immediately followed // by a numeric (integer) value. if (str[0] == '*') { if (nc == 1) { throw new ApplicationException("Unexpected '*' character"); } // Pick up the repeat count (not sure if the digits need to be // followed by white space, or whether non-numeric digits are valid, // so pick up only the digits). string num = GetIntDigits(str.Substring(1)); // Error if repeat count is less than 2. int repeat; if (!Int32.TryParse(num, out repeat) || repeat < 2) { string msg = String.Format("Unexpected repeat count in '{0}'", str); throw new ApplicationException(msg); } if (repeat < 2) { string msg = String.Format("Unexpected repeat count in '{0}'", str); throw new ApplicationException(msg); } // Duplicate the last item using the repeat count. AddRepeats(repeat); // Continue parsing after the repeat count. ParseWord(str.Substring(1 + num.Length)); return; } // If the string contains an embedded qualifier (a "*" or a "/" // character), process the portion of any string prior to the // qualifier. Note that we have just handled the cases where // the qualifier was at the very start of the string. int starIndex = str.IndexOf('*'); int slashIndex = str.IndexOf('/'); if (starIndex >= 0 || slashIndex >= 0) { int qualIndex = starIndex; if (qualIndex < 0 || (slashIndex >= 0 && qualIndex > slashIndex)) { qualIndex = slashIndex; } // Process the stuff prior to the qualifier. string copy = str.Substring(0, qualIndex); ParseWord(copy); // Process the stuff, starting with the qualifier character ParseWord(str.Substring(qualIndex)); return; } // Process this string. We should have either a value or an angle. if (str.IndexOf('-') >= 0 || IsLastItemBC()) { // If the string contains a "c" character, it's a central // angle; process the string only as far as that. PathItemType type = PathItemType.Angle; int caIndex = str.ToUpper().IndexOf('C'); if (caIndex >= 0) { str = str.Substring(0, caIndex); type = PathItemType.CentralAngle; } else { // Check if it's a deflection (if so, strip out the "d"). int dIndex = str.ToUpper().IndexOf('D'); if (dIndex >= 0) { str = str.Substring(0, dIndex) + str.Substring(dIndex + 1); type = PathItemType.Deflection; } } // Try to parse an angular value into radians. double radval; if (RadianValue.TryParse(str, out radval)) { PathItem item = new PathItem(type, null, radval); AddItem(item); return; } // Bad angle. string msg = String.Format("Malformed angle '{0}'", str); throw new ApplicationException(msg); } else { // Get the current distance units. DistanceUnit unit = GetUnits(null, false); // Grab characters that look like a floating point number string num = GetDoubleDigits(str); double val; if (!Double.TryParse(num, out val)) { string msg = String.Format("Malformed value '{0}'", str); throw new ApplicationException(msg); } // If we didn't get right to the end, we may have distance units, // or the ")" character indicating an EC. if (num.Length < str.Length && str[num.Length] != ')') { unit = GetUnits(str.Substring(num.Length), false); if (unit == null) { string msg = String.Format("Malformed value '{0}'", str); throw new ApplicationException(msg); } } PathItem item = new PathItem(PathItemType.Value, unit, val); AddItem(item); if (str.Length > num.Length && str[num.Length] == ')') { ParseWord(str.Substring(num.Length)); } return; } }