Merge 2c93dd03e1
into da6a1ef03e
This commit is contained in:
commit
256f6f8753
|
@ -8,17 +8,16 @@ from django.conf import settings
|
|||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||
from utilities.utils import foreground_color
|
||||
|
||||
|
||||
__all__ = (
|
||||
'CableTraceSVG',
|
||||
)
|
||||
|
||||
|
||||
OFFSET = 0.5
|
||||
PADDING = 10
|
||||
LINE_HEIGHT = 20
|
||||
FANOUT_HEIGHT = 35
|
||||
FANOUT_LEG_HEIGHT = 15
|
||||
CABLE_HEIGHT = 4 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||
|
||||
|
||||
class Node(Hyperlink):
|
||||
|
@ -84,31 +83,38 @@ class Connector(Group):
|
|||
labels: Iterable of text labels
|
||||
"""
|
||||
|
||||
def __init__(self, start, url, color, labels=[], description=[], **extra):
|
||||
super().__init__(class_='connector', **extra)
|
||||
def __init__(self, start, url, color, wireless, labels=[], description=[], end=None, text_offset=0, **extra):
|
||||
super().__init__(class_="connector", **extra)
|
||||
|
||||
self.start = start
|
||||
self.height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||
self.end = (start[0], start[1] + self.height)
|
||||
# Allow to specify end-position or auto-calculate
|
||||
self.end = end if end else (start[0], start[1] + self.height)
|
||||
self.color = color or '000000'
|
||||
|
||||
# Draw a "shadow" line to give the cable a border
|
||||
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
||||
self.add(cable_shadow)
|
||||
if wireless:
|
||||
# Draw the cable
|
||||
cable = Line(start=self.start, end=self.end, class_="wireless-link")
|
||||
self.add(cable)
|
||||
else:
|
||||
# Draw a "shadow" line to give the cable a border
|
||||
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
||||
self.add(cable_shadow)
|
||||
|
||||
# Draw the cable
|
||||
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
||||
self.add(cable)
|
||||
# Draw the cable
|
||||
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
||||
self.add(cable)
|
||||
|
||||
# Add link
|
||||
link = Hyperlink(href=url, target='_parent')
|
||||
|
||||
# Add text label(s)
|
||||
cursor = start[1]
|
||||
cursor += PADDING * 2
|
||||
cursor = start[1] + text_offset
|
||||
cursor += PADDING * 2 + LINE_HEIGHT * 2
|
||||
x_coord = (start[0] + end[0]) / 2 + PADDING
|
||||
for i, label in enumerate(labels):
|
||||
cursor += LINE_HEIGHT
|
||||
text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2)
|
||||
text_coords = (x_coord, cursor - LINE_HEIGHT / 2)
|
||||
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||
link.add(text)
|
||||
if len(description) > 0:
|
||||
|
@ -190,8 +196,9 @@ class CableTraceSVG:
|
|||
|
||||
def draw_parent_objects(self, obj_list):
|
||||
"""
|
||||
Draw a set of parent objects.
|
||||
Draw a set of parent objects (eg hosts, switched, patchpanels) and return all created nodes
|
||||
"""
|
||||
objects = []
|
||||
width = self.width / len(obj_list)
|
||||
for i, obj in enumerate(obj_list):
|
||||
node = Node(
|
||||
|
@ -199,23 +206,26 @@ class CableTraceSVG:
|
|||
width=width,
|
||||
url=f'{self.base_url}{obj.get_absolute_url()}',
|
||||
color=self._get_color(obj),
|
||||
labels=self._get_labels(obj)
|
||||
labels=self._get_labels(obj),
|
||||
object=obj
|
||||
)
|
||||
objects.append(node)
|
||||
self.parent_objects.append(node)
|
||||
if i + 1 == len(obj_list):
|
||||
self.cursor += node.box['height']
|
||||
return objects
|
||||
|
||||
def draw_terminations(self, terminations):
|
||||
def draw_object_terminations(self, terminations, offset_x, width):
|
||||
"""
|
||||
Draw a row of terminating objects (e.g. interfaces), all of which are attached to the same end of a cable.
|
||||
Draw all terminations belonging to an object with specified offset and width
|
||||
Return all created nodes and their maximum height
|
||||
"""
|
||||
nodes = []
|
||||
nodes_height = 0
|
||||
width = self.width / len(terminations)
|
||||
|
||||
for i, term in enumerate(terminations):
|
||||
nodes = []
|
||||
# Sort them by name to make renders more readable
|
||||
for i, term in enumerate(sorted(terminations, key=lambda x: x.name)):
|
||||
node = Node(
|
||||
position=(i * width, self.cursor),
|
||||
position=(offset_x + i * width, self.cursor),
|
||||
width=width,
|
||||
url=f'{self.base_url}{term.get_absolute_url()}',
|
||||
color=self._get_color(term),
|
||||
|
@ -225,133 +235,89 @@ class CableTraceSVG:
|
|||
)
|
||||
nodes_height = max(nodes_height, node.box['height'])
|
||||
nodes.append(node)
|
||||
return nodes, nodes_height
|
||||
|
||||
def draw_terminations(self, terminations, parent_object_nodes):
|
||||
"""
|
||||
Draw a row of terminating objects (e.g. interfaces) and return all created nodes
|
||||
Attach them to previously created parent objects
|
||||
"""
|
||||
nodes = []
|
||||
nodes_height = 0
|
||||
|
||||
# Draw terminations for each parent object
|
||||
for parent in parent_object_nodes:
|
||||
parent_terms = [term for term in terminations if term.parent_object == parent.object]
|
||||
|
||||
# Width and offset(position) for each termination box
|
||||
width = parent.box['width'] / len(parent_terms)
|
||||
offset_x = parent.box['x']
|
||||
|
||||
result, nodes_height = self.draw_object_terminations(parent_terms, offset_x, width)
|
||||
nodes.extend(result)
|
||||
|
||||
self.cursor += nodes_height
|
||||
self.terminations.extend(nodes)
|
||||
|
||||
return nodes
|
||||
|
||||
def draw_fanin(self, node, connector):
|
||||
points = (
|
||||
node.bottom_center,
|
||||
(node.bottom_center[0], node.bottom_center[1] + FANOUT_LEG_HEIGHT),
|
||||
connector.start,
|
||||
)
|
||||
self.connectors.extend((
|
||||
Polyline(points=points, class_='cable-shadow'),
|
||||
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
||||
))
|
||||
|
||||
def draw_fanout(self, node, connector):
|
||||
points = (
|
||||
connector.end,
|
||||
(node.top_center[0], node.top_center[1] - FANOUT_LEG_HEIGHT),
|
||||
node.top_center,
|
||||
)
|
||||
self.connectors.extend((
|
||||
Polyline(points=points, class_='cable-shadow'),
|
||||
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
||||
))
|
||||
|
||||
def draw_cable(self, cable, terminations, cable_count=0):
|
||||
def draw_far_objects(self, obj_list, terminations):
|
||||
"""
|
||||
Draw a single cable. Terminations and cable count are passed for determining position and padding
|
||||
|
||||
:param cable: The cable to draw
|
||||
:param terminations: List of terminations to build positioning data off of
|
||||
:param cable_count: Count of all cables on this layer for determining whether to collapse description into a
|
||||
tooltip.
|
||||
Draw the far-end objects and its terminations and return all created nodes
|
||||
"""
|
||||
# Make sure elements are sorted by name for readability
|
||||
objects = sorted(obj_list, key=lambda x: x.name)
|
||||
width = self.width / len(objects)
|
||||
|
||||
# If the cable count is higher than 2, collapse the description into a tooltip
|
||||
if cable_count > 2:
|
||||
# Use the cable __str__ function to denote the cable
|
||||
labels = [f'{cable}']
|
||||
# Max-height of created terminations
|
||||
terms_height = 0
|
||||
term_nodes = []
|
||||
|
||||
# Include the label and the status description in the tooltip
|
||||
description = [
|
||||
f'Cable {cable}',
|
||||
cable.get_status_display()
|
||||
]
|
||||
# Draw the terminations by per object first
|
||||
for i, obj in enumerate(objects):
|
||||
obj_terms = [term for term in terminations if term.parent_object == obj]
|
||||
obj_pos = i * width
|
||||
result, result_nodes_height = self.draw_object_terminations(obj_terms, obj_pos, width / len(obj_terms))
|
||||
|
||||
if cable.type:
|
||||
# Include the cable type in the tooltip
|
||||
description.append(cable.get_type_display())
|
||||
if cable.length is not None and cable.length_unit:
|
||||
# Include the cable length in the tooltip
|
||||
description.append(f'{cable.length} {cable.get_length_unit_display()}')
|
||||
else:
|
||||
labels = [
|
||||
f'Cable {cable}',
|
||||
cable.get_status_display()
|
||||
]
|
||||
description = []
|
||||
if cable.type:
|
||||
labels.append(cable.get_type_display())
|
||||
if cable.length is not None and cable.length_unit:
|
||||
# Include the cable length in the tooltip
|
||||
labels.append(f'{cable.length} {cable.get_length_unit_display()}')
|
||||
terms_height = max(terms_height, result_nodes_height)
|
||||
term_nodes.extend(result)
|
||||
|
||||
# If there is only one termination, center on that termination
|
||||
# Otherwise average the center across the terminations
|
||||
if len(terminations) == 1:
|
||||
center = terminations[0].bottom_center[0]
|
||||
else:
|
||||
# Get a list of termination centers
|
||||
termination_centers = [term.bottom_center[0] for term in terminations]
|
||||
# Average the centers
|
||||
center = sum(termination_centers) / len(termination_centers)
|
||||
# Update cursor and draw the objects
|
||||
self.cursor += terms_height
|
||||
self.terminations.extend(term_nodes)
|
||||
object_nodes = self.draw_parent_objects(objects)
|
||||
|
||||
# Create the connector
|
||||
connector = Connector(
|
||||
start=(center, self.cursor),
|
||||
color=cable.color or '000000',
|
||||
url=f'{self.base_url}{cable.get_absolute_url()}',
|
||||
labels=labels,
|
||||
description=description
|
||||
)
|
||||
return object_nodes, term_nodes
|
||||
|
||||
# Set the cursor position
|
||||
self.cursor += connector.height
|
||||
|
||||
return connector
|
||||
|
||||
def draw_wirelesslink(self, wirelesslink):
|
||||
def draw_fanin(self, target, terminations, color):
|
||||
"""
|
||||
Draw a line with labels representing a WirelessLink.
|
||||
Draw the fan-in-lines from each of the terminations to the targetpoint
|
||||
"""
|
||||
group = Group(class_='connector')
|
||||
for term in terminations:
|
||||
points = (
|
||||
term.bottom_center,
|
||||
(term.bottom_center[0], term.bottom_center[1] + FANOUT_LEG_HEIGHT),
|
||||
target,
|
||||
)
|
||||
self.connectors.extend((
|
||||
Polyline(points=points, class_='cable-shadow'),
|
||||
Polyline(points=points, style=f'stroke: #{color}'),
|
||||
))
|
||||
|
||||
labels = [
|
||||
f'Wireless link {wirelesslink}',
|
||||
wirelesslink.get_status_display()
|
||||
]
|
||||
if wirelesslink.ssid:
|
||||
labels.append(wirelesslink.ssid)
|
||||
|
||||
# Draw the wireless link
|
||||
start = (OFFSET + self.center, self.cursor)
|
||||
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||
end = (start[0], start[1] + height)
|
||||
line = Line(start=start, end=end, class_='wireless-link')
|
||||
group.add(line)
|
||||
|
||||
self.cursor += PADDING * 2
|
||||
|
||||
# Add link
|
||||
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_parent')
|
||||
|
||||
# Add text label(s)
|
||||
for i, label in enumerate(labels):
|
||||
self.cursor += LINE_HEIGHT
|
||||
text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
|
||||
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||
link.add(text)
|
||||
|
||||
group.add(link)
|
||||
self.cursor += PADDING * 2
|
||||
|
||||
return group
|
||||
def draw_fanout(self, start, terminations, color):
|
||||
"""
|
||||
Draw the fan-out-lines from the startpoint to each of the terminations
|
||||
"""
|
||||
for term in terminations:
|
||||
points = (
|
||||
term.top_center,
|
||||
(term.top_center[0], term.top_center[1] - FANOUT_LEG_HEIGHT),
|
||||
start,
|
||||
)
|
||||
self.connectors.extend((
|
||||
Polyline(points=points, class_='cable-shadow'),
|
||||
Polyline(points=points, style=f'stroke: #{color}'),
|
||||
))
|
||||
|
||||
def draw_attachment(self):
|
||||
"""
|
||||
|
@ -378,86 +344,99 @@ class CableTraceSVG:
|
|||
|
||||
traced_path = self.origin.trace()
|
||||
|
||||
parent_object_nodes = []
|
||||
# Iterate through each (terms, cable, terms) segment in the path
|
||||
for i, segment in enumerate(traced_path):
|
||||
near_ends, links, far_ends = segment
|
||||
|
||||
# Near end parent
|
||||
# This is segment number one.
|
||||
if i == 0:
|
||||
# If this is the first segment, draw the originating termination's parent object
|
||||
self.draw_parent_objects(set(end.parent_object for end in near_ends))
|
||||
parent_object_nodes = self.draw_parent_objects(set(end.parent_object for end in near_ends))
|
||||
# Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!)
|
||||
|
||||
# Near end termination(s)
|
||||
terminations = self.draw_terminations(near_ends)
|
||||
near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
|
||||
self.cursor += CABLE_HEIGHT
|
||||
|
||||
# Connector (a Cable or WirelessLink)
|
||||
if links:
|
||||
link_cables = {}
|
||||
fanin = False
|
||||
fanout = False
|
||||
|
||||
# Determine if we have fanins or fanouts
|
||||
if len(near_ends) > len(set(links)):
|
||||
self.cursor += FANOUT_HEIGHT
|
||||
fanin = True
|
||||
if len(far_ends) > len(set(links)):
|
||||
fanout = True
|
||||
cursor = self.cursor
|
||||
for link in links:
|
||||
# Cable
|
||||
if type(link) is Cable and not link_cables.get(link.pk):
|
||||
# Reset cursor
|
||||
self.cursor = cursor
|
||||
# Generate a list of terminations connected to this cable
|
||||
near_end_link_terminations = [term for term in terminations if term.object.cable == link]
|
||||
# Draw the cable
|
||||
cable = self.draw_cable(link, near_end_link_terminations, cable_count=len(links))
|
||||
# Add cable to the list of cables
|
||||
link_cables.update({link.pk: cable})
|
||||
# Add cable to drawing
|
||||
self.connectors.append(cable)
|
||||
parent_object_nodes, far_terminations = self.draw_far_objects(set(end.parent_object for end in far_ends), far_ends)
|
||||
for cable in links:
|
||||
# Fill in labels and description with all available data
|
||||
description = [
|
||||
f"Link {cable}",
|
||||
cable.get_status_display()
|
||||
]
|
||||
near = []
|
||||
far = []
|
||||
color = '000000'
|
||||
if cable.description:
|
||||
description.append(f"{cable.description}")
|
||||
if isinstance(cable, Cable):
|
||||
labels = [f"{cable}"] if len(links) > 2 else [f"Cable {cable}", cable.get_status_display()]
|
||||
if cable.type:
|
||||
description.append(cable.get_type_display())
|
||||
if cable.length and cable.length_unit:
|
||||
description.append(f"{cable.length} {cable.get_length_unit_display()}")
|
||||
color = cable.color or '000000'
|
||||
|
||||
# Draw fan-ins
|
||||
if len(near_ends) > 1 and fanin:
|
||||
for term in terminations:
|
||||
if term.object.cable == link:
|
||||
self.draw_fanin(term, cable)
|
||||
# Collect all connected nodes to this cable
|
||||
near = [term for term in near_terminations if term.object in cable.a_terminations]
|
||||
far = [term for term in far_terminations if term.object in cable.b_terminations]
|
||||
if not (near and far):
|
||||
# a and b terminations may be swapped
|
||||
near = [term for term in near_terminations if term.object in cable.b_terminations]
|
||||
far = [term for term in far_terminations if term.object in cable.a_terminations]
|
||||
elif isinstance(cable, WirelessLink):
|
||||
labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()]
|
||||
if cable.ssid:
|
||||
description.append(f"{cable.ssid}")
|
||||
near = [term for term in near_terminations if term.object == cable.interface_a]
|
||||
far = [term for term in far_terminations if term.object == cable.interface_b]
|
||||
if not (near and far):
|
||||
# a and b terminations may be swapped
|
||||
near = [term for term in near_terminations if term.object == cable.interface_b]
|
||||
far = [term for term in far_terminations if term.object == cable.interface_a]
|
||||
|
||||
# WirelessLink
|
||||
elif type(link) is WirelessLink:
|
||||
wirelesslink = self.draw_wirelesslink(link)
|
||||
self.connectors.append(wirelesslink)
|
||||
# Select most-probable start and end position
|
||||
start = near[0].bottom_center
|
||||
end = far[0].top_center
|
||||
text_offset = 0
|
||||
|
||||
# Far end termination(s)
|
||||
if len(far_ends) > 1:
|
||||
if fanout:
|
||||
self.cursor += FANOUT_HEIGHT
|
||||
terminations = self.draw_terminations(far_ends)
|
||||
for term in terminations:
|
||||
if hasattr(term.object, 'cable') and link_cables.get(term.object.cable.pk):
|
||||
self.draw_fanout(term, link_cables.get(term.object.cable.pk))
|
||||
else:
|
||||
self.draw_terminations(far_ends)
|
||||
elif far_ends:
|
||||
self.draw_terminations(far_ends)
|
||||
else:
|
||||
# Link is not connected to anything
|
||||
break
|
||||
if len(near) > 1:
|
||||
# Handle Fan-In - change start position to be directly below start
|
||||
start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
|
||||
self.draw_fanin(start, near, color)
|
||||
text_offset -= FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||
elif len(far) > 1:
|
||||
# Handle Fan-Out - change end position to be directly above end
|
||||
end = (start[0], end[1] - FANOUT_HEIGHT - FANOUT_LEG_HEIGHT)
|
||||
self.draw_fanout(end, far, color)
|
||||
text_offset -= FANOUT_HEIGHT
|
||||
|
||||
# Far end parent
|
||||
parent_objects = set(end.parent_object for end in far_ends)
|
||||
self.draw_parent_objects(parent_objects)
|
||||
# Create the connector
|
||||
connector = Connector(
|
||||
start=start,
|
||||
end=end,
|
||||
color=color,
|
||||
wireless=isinstance(cable, WirelessLink),
|
||||
url=f'{self.base_url}{cable.get_absolute_url()}',
|
||||
text_offset=text_offset,
|
||||
labels=labels,
|
||||
description=description
|
||||
)
|
||||
self.connectors.append(connector)
|
||||
|
||||
# Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with
|
||||
# a CircuitTermination)
|
||||
elif far_ends:
|
||||
|
||||
# Attachment
|
||||
attachment = self.draw_attachment()
|
||||
self.connectors.append(attachment)
|
||||
|
||||
# Object
|
||||
self.draw_parent_objects(far_ends)
|
||||
parent_object_nodes = self.draw_parent_objects(far_ends)
|
||||
|
||||
# Determine drawing size
|
||||
self.drawing = svgwrite.Drawing(
|
||||
|
|
Loading…
Reference in New Issue