Update SVG trace rendering to support multiple terminations per cable end

This commit is contained in:
jeremystretch 2022-06-02 11:07:26 -04:00
parent cf7a091319
commit bab6fb0de2

View File

@ -17,6 +17,8 @@ OFFSET = 0.5
PADDING = 10 PADDING = 10
LINE_HEIGHT = 20 LINE_HEIGHT = 20
TERMINATION_WIDTH = 80
class CableTraceSVG: class CableTraceSVG:
""" """
@ -35,6 +37,11 @@ class CableTraceSVG:
# Center edges on pixels to render sharp borders # Center edges on pixels to render sharp borders
self.cursor = OFFSET self.cursor = OFFSET
# Prep elements lists
self.parent_objects = []
self.terminations = []
self.connectors = []
@property @property
def center(self): def center(self):
return self.width / 2 return self.width / 2
@ -79,49 +86,75 @@ class CableTraceSVG:
# Other parent object # Other parent object
return 'e0e0e0' return 'e0e0e0'
def _draw_box(self, width, color, url, labels, y_indent=0, padding_multiplier=1, radius=10): def _draw_box(
self,
x,
width,
color,
url,
labels,
reset_cursor=False,
radius=10
):
""" """
Return an SVG Link element containing a Rect and one or more text labels representing a Return an SVG Link element containing a Rect and one or more text labels representing a
parent object or cable termination point. parent object or cable termination point.
:param x: X axis position
:param width: Box width :param width: Box width
:param color: Box fill color :param color: Box fill color
:param url: Hyperlink URL :param url: Hyperlink URL
:param labels: Iterable of text labels :param labels: Iterable of text labels
:param y_indent: Vertical indent (for overlapping other boxes) (default: 0)
:param padding_multiplier: Add extra vertical padding (default: 1)
:param radius: Box corner radius (default: 10) :param radius: Box corner radius (default: 10)
""" """
self.cursor -= y_indent _orig_cursor = self.cursor
# Create a hyperlink # Create a hyperlink
link = Hyperlink(href=f'{self.base_url}{url}', target='_blank') link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
# Add the box # Add the box
position = ( position = (x + OFFSET, self.cursor)
OFFSET + (self.width - width) / 2, height = PADDING \
self.cursor
)
height = PADDING * padding_multiplier \
+ LINE_HEIGHT * len(labels) \ + LINE_HEIGHT * len(labels) \
+ PADDING * padding_multiplier + PADDING
box = Rect(position, (width - 2, height), rx=radius, class_='parent-object', style=f'fill: #{color}') box = Rect(position, (width - 2, height), rx=radius, class_='parent-object', style=f'fill: #{color}')
link.add(box) link.add(box)
self.cursor += PADDING * padding_multiplier self.cursor += PADDING
# Add text label(s) # Add text label(s)
for i, label in enumerate(labels): for i, label in enumerate(labels):
self.cursor += LINE_HEIGHT self.cursor += LINE_HEIGHT
text_coords = (self.center, self.cursor - LINE_HEIGHT / 2) text_coords = (x + width / 2, self.cursor - LINE_HEIGHT / 2)
text_color = f'#{foreground_color(color, dark="303030")}' text_color = f'#{foreground_color(color, dark="303030")}'
text = Text(label, insert=text_coords, fill=text_color, class_='bold' if not i else []) text = Text(label, insert=text_coords, fill=text_color, class_='bold' if not i else [])
link.add(text) link.add(text)
self.cursor += PADDING * padding_multiplier if reset_cursor:
self.cursor = _orig_cursor
else:
self.cursor += PADDING
return link return link
def _draw_cable(self, color, url, labels): def draw_terminations(self, terminations):
"""
Draw a row of terminating objects (e.g. interfaces) belonging to the same parent object, all of which
are attached to the same end of a cable.
"""
x = self.width / 2 - len(terminations) * TERMINATION_WIDTH / 2
for i, term in enumerate(terminations):
t = self._draw_box(
x=x + i * TERMINATION_WIDTH,
width=TERMINATION_WIDTH,
color=self._get_color(term),
url=term.get_absolute_url(),
labels=self._get_labels(term),
radius=5,
reset_cursor=bool(i + 1 != len(terminations))
)
self.terminations.append(t)
def draw_cable(self, color, url, labels):
""" """
Return an SVG group containing a line element and text labels representing a Cable. Return an SVG group containing a line element and text labels representing a Cable.
@ -159,7 +192,7 @@ class CableTraceSVG:
return group return group
def _draw_wirelesslink(self, url, labels): def draw_wirelesslink(self, url, labels):
""" """
Draw a line with labels representing a WirelessLink. Draw a line with labels representing a WirelessLink.
@ -192,7 +225,7 @@ class CableTraceSVG:
return group return group
def _draw_attachment(self): def draw_attachment(self):
""" """
Return an SVG group containing a line element and "Attachment" label. Return an SVG group containing a line element and "Attachment" label.
""" """
@ -217,40 +250,27 @@ class CableTraceSVG:
traced_path = self.origin.trace() traced_path = self.origin.trace()
# Prep elements list # Iterate through each (terms, cable, terms) segment in the path
parent_objects = []
terminations = []
connectors = []
# Iterate through each (term, cable, term) segment in the path
for i, segment in enumerate(traced_path): for i, segment in enumerate(traced_path):
near_end, connector, far_end = segment near_ends, connector, far_ends = segment
# Near end parent # Near end parent
if i == 0: if i == 0:
# If this is the first segment, draw the originating termination's parent object # If this is the first segment, draw the originating termination's parent object
parent_object = self._draw_box( parent_object = self._draw_box(
x=0,
width=self.width, width=self.width,
color=self._get_color(near_end.parent_object), color=self._get_color(near_ends[0].parent_object),
url=near_end.parent_object.get_absolute_url(), url=near_ends[0].parent_object.get_absolute_url(),
labels=self._get_labels(near_end.parent_object), labels=self._get_labels(near_ends[0].parent_object)
padding_multiplier=2
) )
parent_objects.append(parent_object) self.parent_objects.append(parent_object)
# Near end termination # Near end termination
if near_end is not None: self.draw_terminations(near_ends)
termination = self._draw_box(
width=self.width * .8,
color=self._get_color(near_end),
url=near_end.get_absolute_url(),
labels=self._get_labels(near_end),
y_indent=PADDING,
radius=5
)
terminations.append(termination)
# Connector (a Cable or WirelessLink) # Connector (a Cable or WirelessLink)
connector = connector[0] # Remove Cable from list
if connector is not None: if connector is not None:
# Cable # Cable
@ -263,12 +283,12 @@ class CableTraceSVG:
connector_labels.append(connector.get_type_display()) connector_labels.append(connector.get_type_display())
if connector.length and connector.length_unit: if connector.length and connector.length_unit:
connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}') connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
cable = self._draw_cable( cable = self.draw_cable(
color=connector.color or '000000', color=connector.color or '000000',
url=connector.get_absolute_url(), url=connector.get_absolute_url(),
labels=connector_labels labels=connector_labels
) )
connectors.append(cable) self.connectors.append(cable)
# WirelessLink # WirelessLink
elif type(connector) is WirelessLink: elif type(connector) is WirelessLink:
@ -278,48 +298,40 @@ class CableTraceSVG:
] ]
if connector.ssid: if connector.ssid:
connector_labels.append(connector.ssid) connector_labels.append(connector.ssid)
wirelesslink = self._draw_wirelesslink( wirelesslink = self.draw_wirelesslink(
url=connector.get_absolute_url(), url=connector.get_absolute_url(),
labels=connector_labels labels=connector_labels
) )
connectors.append(wirelesslink) self.connectors.append(wirelesslink)
# Far end termination # Far end termination
termination = self._draw_box( self.draw_terminations(far_ends)
width=self.width * .8,
color=self._get_color(far_end),
url=far_end.get_absolute_url(),
labels=self._get_labels(far_end),
radius=5
)
terminations.append(termination)
# Far end parent # Far end parent
parent_object = self._draw_box( parent_object = self._draw_box(
x=0,
width=self.width, width=self.width,
color=self._get_color(far_end.parent_object), color=self._get_color(far_ends[0].parent_object),
url=far_end.parent_object.get_absolute_url(), url=far_ends[0].parent_object.get_absolute_url(),
labels=self._get_labels(far_end.parent_object), labels=self._get_labels(far_ends[0].parent_object),
y_indent=PADDING,
padding_multiplier=2
) )
parent_objects.append(parent_object) self.parent_objects.append(parent_object)
elif far_end: elif far_ends:
# Attachment # Attachment
attachment = self._draw_attachment() attachment = self.draw_attachment()
connectors.append(attachment) self.connectors.append(attachment)
# ProviderNetwork # ProviderNetwork
parent_object = self._draw_box( parent_object = self._draw_box(
x=0,
width=self.width, width=self.width,
color=self._get_color(far_end), color=self._get_color(far_ends[0]),
url=far_end.get_absolute_url(), url=far_ends[0].get_absolute_url(),
labels=self._get_labels(far_end), labels=self._get_labels(far_ends[0])
padding_multiplier=2
) )
parent_objects.append(parent_object) self.parent_objects.append(parent_object)
# Determine drawing size # Determine drawing size
self.drawing = svgwrite.Drawing( self.drawing = svgwrite.Drawing(
@ -331,7 +343,7 @@ class CableTraceSVG:
self.drawing.defs.add(self.drawing.style(css_file.read())) self.drawing.defs.add(self.drawing.style(css_file.read()))
# Add elements to the drawing in order of depth (Z axis) # Add elements to the drawing in order of depth (Z axis)
for element in connectors + parent_objects + terminations: for element in self.connectors + self.parent_objects + self.terminations:
self.drawing.add(element) self.drawing.add(element)
return self.drawing return self.drawing