def validate_coordinates(self): ''' Iterate the geometry, testing for incomplete / incorrect station / coordinate values Fix them where possible, error if values cannot be resolved ''' #calculate distance bewteen curve start and end using #internal station and coordinate vectors _datum = self.geometry['meta'] _geo_data = self.geometry['geometry'] _prev_geo = { 'End': _datum['Start'], 'InternalStation': (0.0, 0.0), 'StartStation': _datum['StartStation'], 'Length': 0.0 } for _geo in _geo_data: if not _geo: continue #get the vector between the two gemetries and the station distance _vector = _geo['Start'].sub(_prev_geo['End']) _sta_len = abs(_geo['InternalStation'][0] - _prev_geo['InternalStation'][1]) #calculate the difference between the vector length and station distance #in document units _delta = (_vector.Length - _sta_len) / Units.scale_factor() #if the stationing / coordinates are out of tolerance, #determine if the error is with the coordinate vector or station if not Support.within_tolerance(_delta): bearing_angle = Support.get_bearing(_vector) #if the coordinate vector bearing matches, fix the station if Support.within_tolerance(bearing_angle, _geo['BearingIn']): _geo['InternalStation'] = ( _prev_geo['InternalStation'][1] + _vector.Length, _geo['InternalStation'][0]) _geo['StartStation'] = _prev_geo['StartStation'] + \ _prev_geo['Length'] / Units.scale_factor() + \ _vector.Length / Units.scale_factor() #otherwise, fix the coordinate else: _bearing_vector = Support.vector_from_angle( _geo['BearingIn']) _bearing_vector.multiply(_sta_len) _geo['Start'] = _prev_geo['End'].add(_bearing_vector) _prev_geo = _geo
def get_points(arc_dict, interval, interval_type='Segment'): ''' Discretize an arc into the specified segments. Resulting list of coordinates omits provided starting point and concludes with end point arc_dict - A dictionary containing key elemnts: Direction - non-zero. <0 = ccw, >0 = cw Radius - in document units (non-zero, positive) Delta - in radians (non-zero, positive) BearingIn - true north starting bearing in radians (0 to 2*pi) BearingOut - true north ending bearing in radians (0 to 2*pi) interval - value for the interval type (non-zero, positive) interval_type: (defaults to segment for invalid values) 'Segment' - subdivide into n equal segments 'Interval' - subdivide into fixed length segments 'Tolerance' - limit error between segment and curve Points are returned references to start_coord ''' angle = arc_dict['Delta'] direction = arc_dict['Direction'] bearing_in = arc_dict['BearingIn'] radius = arc_dict['Radius'] start_coord = arc_dict['Start'] _forward = App.Vector(math.sin(bearing_in), math.cos(bearing_in), 0.0) _right = App.Vector(_forward.y, -_forward.x, 0.0) result = [start_coord] #define the incremental angle for segment calculations, defaulting to 'Segment' _delta = angle / interval _ratio = (interval * Units.scale_factor()) / radius if interval_type == 'Interval': _delta = _ratio elif interval_type == 'Tolerance': _delta = 2.0 * math.acos(1 - _ratio) #pre-calculate the segment deltas, increasing from zero to the central angle segment_deltas = [ float(_i + 1) * _delta for _i in range(0, int(angle / _delta) + 1) ] segment_deltas[-1] = angle for delta in segment_deltas: _dfw = App.Vector(_forward).multiply(math.sin(delta)) _drt = App.Vector(_right).multiply(direction * (1 - math.cos(delta))) result.append(start_coord.add(_dfw.add(_drt).multiply(radius))) return result
def convert_units(arc, to_document=False): ''' Cnvert the units of the arc parameters to or from document units to_document = True - convert to document units False - convert to system units (mm / radians) ''' angle_keys = ['Delta', 'BearingIn', 'BearingOut'] result = {} angle_fn = math.radians scale_factor = Units.scale_factor() if to_document: angle_fn = math.degrees scale_factor = 1.0 / scale_factor for _k, _v in arc.items(): result[_k] = _v if _v is None: continue if _k in angle_keys: result[_k] = angle_fn(_v) continue if _k != 'Direction': result[_k] = _v * scale_factor return result
def _get_internal_station(self, station): ''' Using the station equations, determine the internal station (position) along the alingment, scaled to the document units ''' start_sta = self.Object.Station_Equations[0].y eqs = self.Object.Station_Equations #default to distance from starting station position = 0.0 for _eq in eqs[1:]: #if station falls within equation, quit if start_sta < station < _eq.x: break #increment the position by the equaion length and #set the starting station to the next equation position += _eq.x - start_sta start_sta = _eq.y #add final distance to position position += station - start_sta return position * Units.scale_factor()
def _parse_data(self, align_name, tags, attrib): ''' Build a dictionary keyed to the internal attribute names from the XML ''' result = {} #test to ensure all required tags are in the imrpoted XML data missing_tags = set(tags[0]).difference(set(list(attrib.keys()))) #report error and skip the alignment if required tags are missing if missing_tags: self.errors.append( 'The required XML tags were not found in alignment %s:\n%s' % (align_name, missing_tags)) return None #merge the required / optional tag dictionaries and iterate the items for _tag in tags[0] + tags[1]: attr_val = LandXml.convert_token(_tag, attrib.get(_tag)) if attr_val is None: if _tag in tags[0]: self.errors.append( 'Missing or invalid %s attribute in alignment %s' % (_tag, align_name)) #test for angles and convert to radians elif _tag in maps.XML_TAGS['angle']: attr_val = Utils.to_float(attrib.get(_tag)) if attr_val: attr_val = math.radians(attr_val) #test for lengths to convert to mm elif _tag in maps.XML_TAGS['length']: attr_val = Utils.to_float(attrib.get(_tag)) if attr_val: attr_val = attr_val * Units.scale_factor() #convert rotation from string to number elif _tag == 'rot': attr_val = 0.0 if attrib.get(_tag) == 'cw': attr_val = 1.0 elif attrib.get(_tag) == 'ccw': attr_val = -1.0 if not attr_val: attr_val = LandXml.get_tag_default(_tag) result[maps.XML_MAP[_tag]] = attr_val return result
def _validate_units(self, units): ''' Validate the units of the alignment, ensuring it matches the document ''' if units is None: print('Missing units') return '' xml_units = units[0].attrib['linearUnit'] if xml_units != Units.get_doc_units()[1]: self.errors.append('Document units of ' + Units.get_doc_units()[1] + ' expected, units of ' + xml_units + 'found') return '' return xml_units
def validate_stationing(self): ''' Iterate the geometry, calculating the internal start station based on the actual station and storing it in an 'InternalStation' parameter tuple for the start and end of the curve ''' prev_station = self.geometry['meta'].get('StartStation') prev_coord = self.geometry['meta'].get('Start') if not prev_coord or not prev_station: print('Unable to validate alignment stationing') return for _geo in self.geometry['geometry']: if not _geo: continue _geo['InternalStation'] = None geo_station = _geo.get('StartStation') geo_coord = _geo['Start'] #if no station is provided, try to infer it from the start coordinate #and the previous station if geo_station is None: geo_station = prev_station if not geo_coord: geo_coord = prev_coord delta = geo_coord.sub(prev_coord).Length if not Support.within_tolerance(delta): geo_station += delta / Units.scale_factor() _geo['StartStation'] = geo_station prev_coord = _geo['End'] prev_station = _geo[ 'StartStation'] + _geo['Length'] / Units.scale_factor() int_sta = self._get_internal_station(geo_station) _geo['InternalStation'] = (int_sta, int_sta + _geo['Length'])
def _parse_coord_geo_data(self, align_name, alignment): ''' Parse the alignment coorinate geometry data to get curve information and return as a dictionary ''' coord_geo = LandXml.get_child(alignment, 'CoordGeom') if not coord_geo: print('Missing coordinate geometry for ', align_name) return None result = [] for geo_node in coord_geo: node_tag = geo_node.tag.split('}')[1] if not node_tag in ['Curve', 'Spiral', 'Line']: continue points = [] for _tag in ['Start', 'End', 'Center', 'PI']: _pt = LandXml.get_child_as_vector(geo_node, _tag) points.append(None) if _pt: points[-1] = (_pt.multiply(Units.scale_factor())) continue if not (node_tag == 'Line' and _tag in ['Center', 'PI']): self.errors.append( 'Missing %s %s coordinate in alignment %s' % (node_tag, _tag, align_name)) coords = { 'Type': node_tag, 'Start': points[0], 'End': points[1], 'Center': points[2], 'PI': points[3] } result.append({ **coords, **self._parse_data(align_name, maps.XML_ATTRIBS[node_tag], geo_node.attrib) }) print('\n<---- Import result ---->\n', result) return result
def _update_alignment(self, value): subset = self.data['Alignments'][value] if subset['meta'].get('StartStation'): self.panel.startStationValueLabel.setText('{0:.2f}'.format(subset['meta']['StartStation'])) if subset['meta'].get('Length'): self.panel.lengthTitleLabel.setText('Length ({0:s}):'.format(Units.get_doc_units()[0])) self.panel.lengthValueLabel.setText('{0:.2f}'.format(subset['meta']['Length'])) sta_model = [] if subset['station']: for st_eq in subset['station']: sta_model.append([st_eq['Back'], st_eq['Ahead']]) widget_model = WidgetModel.create(sta_model, ['Back'], ['Ahead']) self.panel.staEqTableView.setModel(widget_model) curve_model = [] for curve in subset['geometry']: _vals = [curve[_k] if curve.get(_k) else 0.0 for _k in [ 'Direction', 'StartStation', 'BearingIn', 'BearingOut', 'Radius'] ] row = '{0:s}, {1:f}, {2:.2f}, {3:.2f}, {4:.2f}, {5:.2f}'.format( curve['Type'], _vals[0], _vals[1], _vals[2], _vals[3], _vals[4] ) #curve['Type'], curve['Direction'], curve['StartStation'], #curve['BearingIn'], curve['BearingOut'], curve['Radius'] #) curve_model.append(row.split(',')) widget_model_2 = WidgetModel.create(curve_model, ['Type', 'Dir', 'Start', 'In', 'Out', 'Radius']) self.panel.curveTableView.setModel(widget_model_2) self.panel.curveTableView.resizeColumnsToContents()
def _write_tree_data(self, data, node, attribs): ''' Write data to the tree using the passed parameters ''' for _tag in attribs[0] + attribs[1]: #find the xml tag for the current dictionary key _key = maps.XML_MAP.get(_tag) _value = None if _key: value = data.get(_key) #assign default if no value in the dictionary if value is None: value = LandXml.get_tag_default(_tag) if _tag in maps.XML_TAGS['angle']: value = math.degrees(value) elif _tag in maps.XML_TAGS['length']: value /= Units.scale_factor() elif _tag == 'rot': if value < 0.0: value = 'ccw' elif value > 0.0: value = 'cw' else: value = '' result = str(value) if isinstance(value, float): result = '{:.8f}'.format(value) node.set(_tag, result)
def _parse_geo_data(self, align_name, geometry, curve_type): ''' Parse curve data and return as a dictionary ''' result = [] for curve in geometry: #add the curve / line start / center / end coordinates, skipping if any are missing _points = [] for _tag in ['Start', 'End', 'Center', 'PI']: _pt = LandXml.get_child_as_vector(curve, _tag) if _pt: _pt.multiply(Units.scale_factor()) else: #report missing coordinates if not (curve_type == 'line' and _tag in ['Center', 'PI']): self.errors.append( 'Missing %s %s coordinate in alignment %s' % (curve_type, _tag, align_name)) _points.append(_pt) coords = { 'Type': curve_type, 'Start': _points[0], 'End': _points[1], 'Center': _points[2], 'PI': _points[3] } result.append({ **coords, **self._parse_data(align_name, maps.XML_ATTRIBS[curve_type], curve.attrib) }) return result
def _write_coordinates(self, data, parent): ''' Write coordinate children to parent geometry ''' _sf = 1.0 / Units.scale_factor() for _key in maps.XML_TAGS['coordinate']: if not _key in data: continue #scale the coordinates to the document units _vec = App.Vector(data[_key]) _vec.multiply(_sf) _child = LandXml.add_child(parent, _key) _vec_string = LandXml.get_vector_string(_vec) LandXml.set_text(_child, _vec_string)
def write_xml(self): ''' Serialize the object data and it's children to xml files ''' _list = [] #iterate the list of children, acquiring their data sets #and creating a total data set for alignments. for _obj in self.Object.OutList: _list.append(_obj.Proxy.get_geometry()) exporter = AlignmentExporter() template_path = App.getUserAppDataDir() + 'Mod/freecad-transportation-wb/Resources/data/' template_file = 'landXML-' + Units.get_doc_units()[1] + '.xml' xml_path = App.ActiveDocument.TransientDir + '/alignment.xml' print('writing xml...') exporter.write(_list, template_path + template_file, xml_path) self.Object.Xml_Path = xml_path
def assign_geometry_data(self, datum, data): """ Iterate the dataset, extracting geometric data Validate data Create separate items for each curve / tangent Assign Northing / Easting datums """ _points = [datum] _geometry = [] for item in data: _ne = [item['Northing'], item['Easting']] _db = [item['Distance'], item['Bearing']] _rd = [item['Radius'], item['Degree']] curve = [0.0, 0.0, 0.0, 0.0] try: curve[3] = float(item['Spiral']) except: pass point_vector = App.Vector(0.0, 0.0, 0.0) #parse degree of curve / radius values if _rd[0]: try: curve[2] = float(_rd[0]) except: self.errors.append('Invalid radius: %s' % _rd[0]) elif _rd[1]: try: curve[2] = Utils.doc_to_radius(float(_rd[1])) except: self.errors.append('Invalid degree of curve: %s' % _rd[1]) else: self.errors.append('Missing Radius / Degree of Curvature') #parse bearing / distance values if any(_db) and not all(_db): self.errors.append('(Distance, Bearing) Incomplete: (%s, %s)' % tuple(_db)) elif all(_db): #get the last point in the geometry for the datum, unless empty datum = self.Object.Datum try: _db[0] = float(_db[0]) _db[1] = float(_db[1]) #zero distance means coincident PI's. Skip that. if _db[0] > 0.0: #set values to geo_vector, adding the previous position to the new one point_vector = Utils.distance_bearing_to_coordinates( _db[0], _db[1]) curve[0:2] = _db if not Units.is_metric_doc(): point_vector.multiply(304.8) point_vector = _points[-1].add(point_vector) except: self.errors.append( '(Distance, Bearing) Invalid: (%s, %s)' % tuple(_db)) #parse northing / easting values if any(_ne) and not all(_ne): self.errors.append( '(Easting, Northing) Incomplete: ( %s, %s)' % tuple(_ne)) elif all(_ne): try: point_vector.y = float(_ne[0]) point_vector.x = float(_ne[1]) except: self.errors.append( '(Easting, Northing) Invalid: (%s, %s)' % tuple(_ne)) curve[0:2] = Utils.coordinates_to_distance_bearing( _points[-1], point_vector) #skip coincident PI's #save the point as a vector #save the geometry as a string if curve[0] > 0.0: _points.append(point_vector) print('saving ', str(curve)) _geometry.append(str(curve).replace('[', '').replace(']', '')) self.Object.PIs = _points self.Object.Geometry = _geometry
def parameter_test(excludes=None): ''' ''' scale_factor = 1.0 / Units.scale_factor() radius = 670.00 delta = 50.3161 half_delta = math.radians(delta) / 2.0 arc = { 'Type': 'arc', 'Direction': -1, 'Delta': delta, 'Radius': radius, 'Length': radius * math.radians(delta), 'Tangent': radius * math.tan(half_delta), 'Chord': 2 * radius * math.sin(half_delta), 'External': radius * ((1 / math.cos(half_delta) - 1)), 'MiddleOrd': radius * (1 - math.cos(half_delta)), 'BearingIn': 139.3986, 'BearingOut': 89.0825, 'Start': App.Vector(122056.0603640062, -142398.20717496306, 0.0).multiply(scale_factor), 'Center': App.Vector(277108.1622932797, -9495.910944558627, 0.0).multiply(scale_factor), 'End': App.Vector(280378.2141876281, -213685.7280672748, 0.0).multiply(scale_factor), 'PI': App.Vector(184476.32163324804, -215221.57431973785, 0.0).multiply(scale_factor) } #convert the arc to system units before processing, and back to document units on return comp = { 'Type': 'arc', 'Radius': 670.0, 'Tangent': 314.67910063712156, 'Chord': 569.6563702820052, 'Delta': 50.31609999999997, 'Direction': -1.0, 'BearingIn': 139.3986, 'BearingOut': 89.0825, 'Length': 588.3816798810212, 'External': 70.21816809491217, 'Middle': 63.5571709144523, 'Start': App.Vector(400.4463922703616, -467.1857190779628, 0.0), 'Center': App.Vector(909.147514086, -31.1545634664, 0.0), 'End': App.Vector(919.8760307993049, -701.0686616380407, 0.0), 'PI': App.Vector(605.2372756996326, -706.1075272957279, 0.0) } if excludes: return run_test(arc, comp, excludes) keys = ['Start', 'End', 'Center', 'PI'] run_test(arc, comp, None) for i in range(0, 4): run_test(arc, comp, [keys[i]]) for j in range(i + 1, 4): run_test(arc, comp, [keys[i], keys[j]]) for k in range(j + 1, 4): run_test(arc, comp, [keys[i], keys[j], keys[k]]) run_test(arc, comp, keys) return arc
def validate_datum(self): ''' Ensure the datum is valid, assuming 0+00 / (0,0,0) for station and coordinate where none is suplpied and it cannot be inferred fromt the starting geometry ''' _datum = self.geometry['meta'] _geo = self.geometry['geometry'][0] if not _geo or not _datum: print('Unable to validate alignment datum') return _datum_truth = [ not _datum.get('StartStation') is None, not _datum.get('Start') is None ] _geo_truth = [ not _geo.get('StartStation') is None, not _geo.get('Start') is None ] #---------------------------- #CASE 0 #---------------------------- #both defined? nothing to do if all(_datum_truth): return _geo_station = 0 _geo_start = App.Vector() if _geo_truth[0]: _geo_station = _geo['StartStation'] if _geo_truth[1]: _geo_start = _geo['Start'] #--------------------- #CASE 1 #--------------------- #no datum defined? use initial geometry or zero defaults if not any(_datum_truth): _datum['StartStation'] = _geo_station _datum['Start'] = _geo_start return #-------------------- #CASE 2 #-------------------- #station defined? #if the geometry has a station and coordinate, project the start coordinate if _datum['StartStation']: _datum['Start'] = _geo_start #assume geometry start if no geometry station if not _geo_truth[0]: return #scale the distance to the system units delta = _geo_station - _datum['StartStation'] #cutoff if error is below tolerance if not Support.within_tolerance(delta): delta *= Units.scale_factor() else: delta = 0.0 #assume geometry start if station delta is zero if delta: #calcualte the start based on station delta _datum['Start'] = _datum['Start'].sub( Support.vector_from_angle( _geo['BearingIn']).multiply(delta)) return #--------------------- #CASE 3 #--------------------- #datum start coordinate is defined #if the geometry has station and coordinate, project the start station _datum['StartStation'] = _geo_station #assume geometry station if no geometry start if _geo_truth[1]: #scale the length to the document units delta = _geo_start.sub( _datum['Start']).Length / Units.scale_factor() _datum['StartStation'] -= delta