def extent_handler(node: xml.Element, ctx: Context) -> svgwrite.shapes.Rect: """ handler to transform Proteus Extent object to SVG Circle object :param node: XML node with Proteus extent definition :param ctx: Proteus model context :return: SVG Rect object """ ensure_type(node, 'Extent') stroke_width = 0.1 x_min_str, y_min_str = itemgetter('X', 'Y')(node.find('Min').attrib) x_max_str, y_max_str = itemgetter('X', 'Y')(node.find('Max').attrib) x_min_f, y_min_f, x_max_f, y_max_f = map( float, (x_min_str, y_min_str, x_max_str, y_max_str)) y_min_f = ctx.y_max - y_min_f y_max_f = ctx.y_max - y_max_f rect_width = x_max_f - x_min_f rect_height = y_min_f - y_max_f return ctx.drawing.rect( (x_min_f, y_min_f - rect_height), (rect_width, rect_height), stroke='red', fill='none', style=f'stroke-width:{stroke_width}', onmouseover='evt.target.setAttribute("fill", "blue")', onmouseout='evt.target.setAttribute("fill", "none")')
def circle_handler(node: xml.Element, ctx: Context) -> svgwrite.shapes.Circle: """ handler to transform Proteus Circle object to SVG Circle object :param node: XML node with Proteus circle definition :param ctx: Proteus model context :return: SVG Circle object """ ensure_type(node, 'Circle') presentation_obj = node.find("Presentation") if presentation_obj is None: raise AssertionError( f'"Presentation" node expected but not found in Line node') stroke_color = fetch_color_from_presentation(presentation_obj) stroke_width = float( presentation_obj.attrib.get('LineWeight')) * ctx.units.value radius = float(node.attrib.get('Radius')) * ctx.units.value is_filled = True if node.attrib.get('Filled') else False coordinates = node.find('Position').find('Location') x_pos, y_pos = map(lambda x: float(x) * ctx.units.value, itemgetter('X', 'Y')(coordinates.attrib)) circle = svgwrite.shapes.Circle( (x_pos, ctx.y_max - y_pos), radius, fill='none' if not is_filled else stroke_color, stroke=stroke_color, style=f'stroke-width:{stroke_width}') return circle
def pipe_connector_symbol_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus PipeConnectorSymbol node. This is a complex node which contains other nodes :param node: :param ctx: :return: """ ensure_type(node, 'PipeConnectorSymbol') pipe_conn_group = create_group(ctx, node) cross_page_conn = node.find('CrossPageConnection') if cross_page_conn is not None: pipe_conn_group.attribs[ 'data-drawing-name'] = cross_page_conn.attrib.get('DrawingName') pipe_conn_group.attribs[ 'data-drawing-link-label'] = cross_page_conn.attrib.get( 'LinkLabel') # todo we could also need support for CrossPageConnection with linkedPersistentId if node.attrib.get(ATTR_COMP_NAME) is not None: shape_reference = ctx.get_from_shape_catalog( 'PipeConnectorSymbol', node.attrib[ATTR_COMP_NAME]) process_shape_reference(node, shape_reference, ctx) return pipe_conn_group
def fetch_color_from_presentation(pr_obj: XMLParse.Element) -> str: """ function to fetch color from Presentation object :param pr_obj: Presentation object with 'R', 'G','B' objects defined :return: color as hex string """ ensure_type(pr_obj, 'Presentation') red, green, blue = map(float, itemgetter('R', 'G', 'B')(pr_obj.attrib)) return rgb_to_hex_string(*compute_rgb(red, green, blue))
def drawing_handler(node: xml.Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus Drawing node. This is a complex node which contains other nodes such as Label, Text, etc. :param node: Drawing to process :param ctx: drawing context :return: SVG group object where other drawing parts will be added later """ ensure_type(node, 'Drawing') return create_group(ctx, node)
def piping_network_segment_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus PipingNetworkSegment node. This is a complex node which contains other nodes :param node: :param ctx: :return: """ ensure_type(node, 'PipingNetworkSegment') pn_sys_group = create_group(ctx, node) return pn_sys_group
def process_circle(circle_node: xml.Element): ensure_type(circle_node, 'Circle') _presentation_obj = circle_node.find('Presentation') _radius = float(circle_node.attrib.get('Radius')) * ctx.units.value _x, _y = map( lambda val: float(val) * ctx.units.value, itemgetter('X', 'Y')( circle_node.find('Position').find('Location').attrib)) _stroke_color = fetch_color_from_presentation(_presentation_obj) _stroke_width = float( _presentation_obj.attrib.get('LineWeight')) * ctx.units.value return _x, _y, _radius, _stroke_color, _stroke_width
def instrument_component_handler(node: xml._Element, ctx: Context): """ Handler to process Proteus InstrumentComponent node. This is a complex node which contains other nodes and can be referenced in ShapeCatalogue :param node: :param ctx: :return: """ ensure_type(node, 'InstrumentComponent') pipe_comp_group = create_group(ctx, node) shape_reference = ctx.get_from_shape_catalog( 'InstrumentComponent', node.attrib.get(ATTR_COMP_NAME)) process_shape_reference(node, shape_reference, ctx) return pipe_comp_group
def component_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus Component node. This is a complex node which contains other nodes :param node: :param ctx: :return: """ ensure_type(node, 'Component') pn_sys_group = create_group(ctx, node) shape_reference = ctx.get_from_shape_catalog( 'Component', node.attrib.get(ATTR_COMP_NAME)) process_shape_reference(node, shape_reference, ctx) return pn_sys_group
def process_instrumentation_function_handler(node: xml._Element, ctx: Context): """ Handler to process Proteus ProcessInstrumentationFunction node. This is a complex node which contains other nodes and can be referenced in ShapeCatalogue :param node: :param ctx: :return: """ ensure_type(node, 'ProcessInstrumentationFunction') shape_reference = ctx.get_from_shape_catalog( 'ProcessInstrumentationFunction', node.attrib.get(ATTR_COMP_NAME)) process_shape_reference(node, shape_reference, ctx) return create_group(ctx, node)
def piping_component_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus PipingComponent node. This is a complex node which contains other nodes and can be referenced in ShapeCatalogue :param node: node to process :param ctx: drawing context :return: svgwrite.container.Group object """ ensure_type(node, 'PipingComponent') pipe_comp_group = create_group(ctx, node) shape_reference = ctx.get_from_shape_catalog( 'PipingComponent', node.attrib.get(ATTR_COMP_NAME)) process_shape_reference(node, shape_reference, ctx) return pipe_comp_group
def label_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Proteus Label node. This is a complex node which contains other nodes and can be referenced in ShapeCatalogue :param node: :param ctx: :return: """ ensure_type(node, 'Label') shape_reference = ctx.get_from_shape_catalog( 'Label', node.attrib.get(ATTR_COMP_NAME)) process_shape_reference(node, shape_reference, ctx) return create_group(ctx, node)
def nozzle_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Nozzle object :param node: object to process :param ctx: drawing context :return: SVG group object """ ensure_type(node, 'Nozzle') nozzle_group = create_group(ctx, node) shape_reference = ctx.get_from_shape_catalog( 'Nozzle', nozzle_group.attribs[DATA_COMPONENT_NAME]) process_shape_reference(node, shape_reference, ctx) return nozzle_group
def shape_handler(node: xml.Element, ctx: Context) -> svgwrite.path.Path: """ handler to transform Proteus PolyLine geometric curve primitive to SVG path object :param node: XML node with Proteus CenterLine definition :param ctx: drawing context :return: XML object with SVG path definition """ ensure_type(node, 'Shape') presentation_obj = node.find("Presentation") # as of P&ID profile file specification - The drawing behaviour is undefined if the Presentation element is missing. if presentation_obj is None: parent_presentation_obj = node.getparent().find('Presentation') stroke_color = fetch_color_from_presentation(parent_presentation_obj) stroke_width = float( parent_presentation_obj.attrib.get('LineWeight')) * ctx.units.value else: stroke_color = fetch_color_from_presentation(presentation_obj) stroke_width = float( presentation_obj.attrib.get('LineWeight')) * ctx.units.value coordinates = node.findall('Coordinate') is_filled = True if node.attrib.get('Filled') else False path = svgwrite.path.Path(None, stroke=stroke_color, fill='none' if not is_filled else stroke_color, style=f'stroke-width:{stroke_width}') start_point = True for coordinate in coordinates: operation = 'M' if start_point else 'L' start_point = False x_, y_ = map(lambda x: float(x) * ctx.units.value, itemgetter('X', 'Y')(coordinate.attrib)) path.push(operation, x_, ctx.y_max - y_) line_type = fetch_line_type_from_presentation(presentation_obj) if line_type: path.dasharray(line_type) return path.push('Z')
def line_handler(node: xml.Element, ctx: Context) -> svgwrite.shapes.Line: """ handler to transform Proteus line object to SVG line object :param node: XML node with Proteus line definition :param ctx: Proteus model context :return: SVG line object """ ensure_type(node, 'Line') coordinates = node.findall('Coordinate') if len(coordinates) != 2: raise AssertionError( f'a line must have a pair of coordinates, {len(coordinates)} found' ) presentation_obj = node.find("Presentation") if presentation_obj is None: raise AssertionError( f'"Presentation" node expected but not found in Line node') stroke_color = fetch_color_from_presentation(presentation_obj) stroke_width = float( presentation_obj.attrib.get('LineWeight')) * ctx.units.value x_min_str, y_min_str = itemgetter('X', 'Y')(coordinates[0].attrib) x_max_str, y_max_str = itemgetter('X', 'Y')(coordinates[1].attrib) x_min_f, y_min_f, x_max_f, y_max_f = map( lambda x: float(x) * ctx.units.value, (x_min_str, y_min_str, x_max_str, y_max_str)) line = svgwrite.shapes.Line(start=(x_min_f, ctx.y_max - y_min_f), end=(x_max_f, ctx.y_max - y_max_f), stroke=stroke_color, stroke_width=stroke_width) line_type = fetch_line_type_from_presentation(presentation_obj) if line_type: line.dasharray(line_type) return line
def fetch_line_type_from_presentation(pr_obj: xml._Element, fallback='Solid') -> list: """ function to fetch and convert Proteus line type to SVG stroke-dasharray One of the numbers or names from the following (Object Model document v2.2) :- 0 Solid 1 Dotted 2 Dashed 3 Long Dash 4 Long Dash + Short Dash, CenterLine 5 Short Dash 6 Long Dash + Short Dash + Short Dash 7 Dash + Short Dash :param fallback: default line type :param pr_obj: Presentation object :return: list values for stroke-dasharray SVG property """ ensure_type(pr_obj, 'Presentation') line_type = pr_obj.attrib.get('LineType', fallback).lower() if '0' == line_type or 'solid' == line_type: return [] if '1' == line_type or 'dotted' == line_type: return [1] if '2' == line_type or 'dashed' == line_type: return [3] if '3' == line_type or 'long dash' == line_type: return [4, 1] if '4' == line_type or 'long dash + short dash, centerline' == line_type: return [4, 1, 2, 1] if '5' == line_type or 'short dash' == line_type: return [2, 1] if '6' == line_type or 'long dash + short dash + short dash' == line_type: return [4, 1, 2, 1, 2, 1] if '7' == line_type or 'dash + short dash' == line_type: return [3, 1, 2, 1] raise ValueError(f'unknown line type ({line_type}) found')
def equipment_handler(node: xml._Element, ctx: Context) -> svgwrite.container.Group: """ Handler to process Equipment object :param node: node to process :param ctx: model context :return: SVG group object where Equipment parts will be added later """ ensure_type(node, 'Equipment') eq_group = create_group(ctx, node) # fixme this one will work with Comos but what if we get drawing from any other systems? # eq_group.attribs[DATA_LABEL] = get_gen_attr_val(node, 'ComosProperties', 'Label') # eq_group.attribs[DATA_FULL_LABEL] = get_gen_attr_val(node, 'ComosProperties', 'FullLabel') # this it temporary fix for that # attributes_to_add_from_origin method should be updated if we get new Proteus data sources # probably we could move it into external config file instead of code # probably we also need common data-* name's which do not depend on originating system for attr in ctx.attributes_to_add_from_origin(): for attr_value in attr.get('values'): key = f'data-{ctx.origin.lower()}-{attr_value.lower()}' value = get_gen_attr_val(node, attr['set'], attr_value) if value is not None: eq_group.attribs[key] = value descr_obj = node.find('Description') if descr_obj is not None: eq_group.attribs['data-description'] = descr_obj.text if eq_group.attribs.get(DATA_COMPONENT_NAME) is not None: shape_reference = ctx.get_from_shape_catalog( 'Equipment', eq_group.attribs[DATA_COMPONENT_NAME]) process_shape_reference(node, shape_reference, ctx) return eq_group
def text_handler(node: xml.Element, ctx: Context) -> svgwrite.text.Text: """ handler to transform Proteus Text object to SVG String object :param node: XML node with Proteus line definition :param ctx: Proteus model context :return: SVG line object """ ensure_type(node, 'Text') if node.attrib.get('String'): text_arr = re.split(LINEBREAK_PATTERN, node.attrib.get('String')) else: # todo add support for displaying values stored in generic attribute # see 4.2.3.1 of DEXPI 1.2 spec return text_font = node.attrib.get('Font') text_size = round(float(node.attrib.get('Height')) * ctx.units.value) text_color = DEFAULT_STROKE_COLOR if node.find('Presentation') is not None: text_color = fetch_color_from_presentation(node.find('Presentation')) style = f'font-size:{text_size}px; font-family:{text_font}; fill:{text_color}' text_angle = float(node.attrib.get('TextAngle')) if node.attrib.get( 'TextAngle') is not None else 0 text_justification = node.attrib.get('Justification') if not text_justification: text_justification = 'LeftBottom' if text_justification.startswith('Left'): svg_jst = 'start' elif text_justification.startswith('Right'): svg_jst = 'end' elif text_justification.startswith('Center'): svg_jst = 'middle' else: raise ValueError(f'Unknown justification {text_justification}') if text_justification.endswith('Top'): alignment_baseline = 'baseline' elif text_justification.endswith('Center'): alignment_baseline = 'middle' elif text_justification.endswith('Bottom'): alignment_baseline = 'hanging' else: raise ValueError(f'Unknown justification {text_justification}') text_pos = node.find('Position') if text_pos is None: text_pos = node.getparent().find('Position') text_pos_x, text_pos_y = map( lambda x: float(x) * ctx.units.value, itemgetter('X', 'Y')(text_pos.find('Location').attrib)) text_obj = svgwrite.text.Text(text_arr[0], x=[text_pos_x], y=[ctx.y_max - text_pos_y - text_size / 2], style=style, text_anchor=svg_jst, alignment_baseline=alignment_baseline) text_obj.rotate(-text_angle, (text_pos_x, -(text_pos_y - ctx.y_max))) for span in text_arr[1:]: t_span = svgwrite.text.TSpan(span, x=[float(text_pos_x)], dy=[text_size], text_anchor=svg_jst, alignment_baseline=alignment_baseline) text_obj.add(t_span) return text_obj