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 dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from utilities.utils import foreground_color
|
from utilities.utils import foreground_color
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableTraceSVG',
|
'CableTraceSVG',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
OFFSET = 0.5
|
OFFSET = 0.5
|
||||||
PADDING = 10
|
PADDING = 10
|
||||||
LINE_HEIGHT = 20
|
LINE_HEIGHT = 20
|
||||||
FANOUT_HEIGHT = 35
|
FANOUT_HEIGHT = 35
|
||||||
FANOUT_LEG_HEIGHT = 15
|
FANOUT_LEG_HEIGHT = 15
|
||||||
|
CABLE_HEIGHT = 4 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
class Node(Hyperlink):
|
class Node(Hyperlink):
|
||||||
|
@ -84,31 +83,38 @@ class Connector(Group):
|
||||||
labels: Iterable of text labels
|
labels: Iterable of text labels
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, start, url, color, labels=[], description=[], **extra):
|
def __init__(self, start, url, color, wireless, labels=[], description=[], end=None, text_offset=0, **extra):
|
||||||
super().__init__(class_='connector', **extra)
|
super().__init__(class_="connector", **extra)
|
||||||
|
|
||||||
self.start = start
|
self.start = start
|
||||||
self.height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
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'
|
self.color = color or '000000'
|
||||||
|
|
||||||
# Draw a "shadow" line to give the cable a border
|
if wireless:
|
||||||
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
# Draw the cable
|
||||||
self.add(cable_shadow)
|
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
|
# Draw the cable
|
||||||
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
||||||
self.add(cable)
|
self.add(cable)
|
||||||
|
|
||||||
# Add link
|
# Add link
|
||||||
link = Hyperlink(href=url, target='_parent')
|
link = Hyperlink(href=url, target='_parent')
|
||||||
|
|
||||||
# Add text label(s)
|
# Add text label(s)
|
||||||
cursor = start[1]
|
cursor = start[1] + text_offset
|
||||||
cursor += PADDING * 2
|
cursor += PADDING * 2 + LINE_HEIGHT * 2
|
||||||
|
x_coord = (start[0] + end[0]) / 2 + PADDING
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
cursor += LINE_HEIGHT
|
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 [])
|
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||||
link.add(text)
|
link.add(text)
|
||||||
if len(description) > 0:
|
if len(description) > 0:
|
||||||
|
@ -190,8 +196,9 @@ class CableTraceSVG:
|
||||||
|
|
||||||
def draw_parent_objects(self, obj_list):
|
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)
|
width = self.width / len(obj_list)
|
||||||
for i, obj in enumerate(obj_list):
|
for i, obj in enumerate(obj_list):
|
||||||
node = Node(
|
node = Node(
|
||||||
|
@ -199,23 +206,26 @@ class CableTraceSVG:
|
||||||
width=width,
|
width=width,
|
||||||
url=f'{self.base_url}{obj.get_absolute_url()}',
|
url=f'{self.base_url}{obj.get_absolute_url()}',
|
||||||
color=self._get_color(obj),
|
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)
|
self.parent_objects.append(node)
|
||||||
if i + 1 == len(obj_list):
|
if i + 1 == len(obj_list):
|
||||||
self.cursor += node.box['height']
|
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
|
nodes_height = 0
|
||||||
width = self.width / len(terminations)
|
nodes = []
|
||||||
|
# Sort them by name to make renders more readable
|
||||||
for i, term in enumerate(terminations):
|
for i, term in enumerate(sorted(terminations, key=lambda x: x.name)):
|
||||||
node = Node(
|
node = Node(
|
||||||
position=(i * width, self.cursor),
|
position=(offset_x + i * width, self.cursor),
|
||||||
width=width,
|
width=width,
|
||||||
url=f'{self.base_url}{term.get_absolute_url()}',
|
url=f'{self.base_url}{term.get_absolute_url()}',
|
||||||
color=self._get_color(term),
|
color=self._get_color(term),
|
||||||
|
@ -225,133 +235,89 @@ class CableTraceSVG:
|
||||||
)
|
)
|
||||||
nodes_height = max(nodes_height, node.box['height'])
|
nodes_height = max(nodes_height, node.box['height'])
|
||||||
nodes.append(node)
|
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.cursor += nodes_height
|
||||||
self.terminations.extend(nodes)
|
self.terminations.extend(nodes)
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
def draw_fanin(self, node, connector):
|
def draw_far_objects(self, obj_list, terminations):
|
||||||
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):
|
|
||||||
"""
|
"""
|
||||||
Draw a single cable. Terminations and cable count are passed for determining position and padding
|
Draw the far-end objects and its terminations and return all created nodes
|
||||||
|
|
||||||
: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.
|
|
||||||
"""
|
"""
|
||||||
|
# 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
|
# Max-height of created terminations
|
||||||
if cable_count > 2:
|
terms_height = 0
|
||||||
# Use the cable __str__ function to denote the cable
|
term_nodes = []
|
||||||
labels = [f'{cable}']
|
|
||||||
|
|
||||||
# Include the label and the status description in the tooltip
|
# Draw the terminations by per object first
|
||||||
description = [
|
for i, obj in enumerate(objects):
|
||||||
f'Cable {cable}',
|
obj_terms = [term for term in terminations if term.parent_object == obj]
|
||||||
cable.get_status_display()
|
obj_pos = i * width
|
||||||
]
|
result, result_nodes_height = self.draw_object_terminations(obj_terms, obj_pos, width / len(obj_terms))
|
||||||
|
|
||||||
if cable.type:
|
terms_height = max(terms_height, result_nodes_height)
|
||||||
# Include the cable type in the tooltip
|
term_nodes.extend(result)
|
||||||
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()}')
|
|
||||||
|
|
||||||
# If there is only one termination, center on that termination
|
# Update cursor and draw the objects
|
||||||
# Otherwise average the center across the terminations
|
self.cursor += terms_height
|
||||||
if len(terminations) == 1:
|
self.terminations.extend(term_nodes)
|
||||||
center = terminations[0].bottom_center[0]
|
object_nodes = self.draw_parent_objects(objects)
|
||||||
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)
|
|
||||||
|
|
||||||
# Create the connector
|
return object_nodes, term_nodes
|
||||||
connector = Connector(
|
|
||||||
start=(center, self.cursor),
|
|
||||||
color=cable.color or '000000',
|
|
||||||
url=f'{self.base_url}{cable.get_absolute_url()}',
|
|
||||||
labels=labels,
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the cursor position
|
def draw_fanin(self, target, terminations, color):
|
||||||
self.cursor += connector.height
|
|
||||||
|
|
||||||
return connector
|
|
||||||
|
|
||||||
def draw_wirelesslink(self, wirelesslink):
|
|
||||||
"""
|
"""
|
||||||
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 = [
|
def draw_fanout(self, start, terminations, color):
|
||||||
f'Wireless link {wirelesslink}',
|
"""
|
||||||
wirelesslink.get_status_display()
|
Draw the fan-out-lines from the startpoint to each of the terminations
|
||||||
]
|
"""
|
||||||
if wirelesslink.ssid:
|
for term in terminations:
|
||||||
labels.append(wirelesslink.ssid)
|
points = (
|
||||||
|
term.top_center,
|
||||||
# Draw the wireless link
|
(term.top_center[0], term.top_center[1] - FANOUT_LEG_HEIGHT),
|
||||||
start = (OFFSET + self.center, self.cursor)
|
start,
|
||||||
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
)
|
||||||
end = (start[0], start[1] + height)
|
self.connectors.extend((
|
||||||
line = Line(start=start, end=end, class_='wireless-link')
|
Polyline(points=points, class_='cable-shadow'),
|
||||||
group.add(line)
|
Polyline(points=points, style=f'stroke: #{color}'),
|
||||||
|
))
|
||||||
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_attachment(self):
|
def draw_attachment(self):
|
||||||
"""
|
"""
|
||||||
|
@ -378,86 +344,99 @@ class CableTraceSVG:
|
||||||
|
|
||||||
traced_path = self.origin.trace()
|
traced_path = self.origin.trace()
|
||||||
|
|
||||||
|
parent_object_nodes = []
|
||||||
# Iterate through each (terms, cable, terms) segment in the path
|
# Iterate through each (terms, cable, terms) segment in the path
|
||||||
for i, segment in enumerate(traced_path):
|
for i, segment in enumerate(traced_path):
|
||||||
near_ends, links, far_ends = segment
|
near_ends, links, far_ends = segment
|
||||||
|
|
||||||
# Near end parent
|
# This is segment number one.
|
||||||
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
|
||||||
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)
|
near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
|
||||||
terminations = self.draw_terminations(near_ends)
|
self.cursor += CABLE_HEIGHT
|
||||||
|
|
||||||
# Connector (a Cable or WirelessLink)
|
# Connector (a Cable or WirelessLink)
|
||||||
if links:
|
if links:
|
||||||
link_cables = {}
|
|
||||||
fanin = False
|
|
||||||
fanout = False
|
|
||||||
|
|
||||||
# Determine if we have fanins or fanouts
|
parent_object_nodes, far_terminations = self.draw_far_objects(set(end.parent_object for end in far_ends), far_ends)
|
||||||
if len(near_ends) > len(set(links)):
|
for cable in links:
|
||||||
self.cursor += FANOUT_HEIGHT
|
# Fill in labels and description with all available data
|
||||||
fanin = True
|
description = [
|
||||||
if len(far_ends) > len(set(links)):
|
f"Link {cable}",
|
||||||
fanout = True
|
cable.get_status_display()
|
||||||
cursor = self.cursor
|
]
|
||||||
for link in links:
|
near = []
|
||||||
# Cable
|
far = []
|
||||||
if type(link) is Cable and not link_cables.get(link.pk):
|
color = '000000'
|
||||||
# Reset cursor
|
if cable.description:
|
||||||
self.cursor = cursor
|
description.append(f"{cable.description}")
|
||||||
# Generate a list of terminations connected to this cable
|
if isinstance(cable, Cable):
|
||||||
near_end_link_terminations = [term for term in terminations if term.object.cable == link]
|
labels = [f"{cable}"] if len(links) > 2 else [f"Cable {cable}", cable.get_status_display()]
|
||||||
# Draw the cable
|
if cable.type:
|
||||||
cable = self.draw_cable(link, near_end_link_terminations, cable_count=len(links))
|
description.append(cable.get_type_display())
|
||||||
# Add cable to the list of cables
|
if cable.length and cable.length_unit:
|
||||||
link_cables.update({link.pk: cable})
|
description.append(f"{cable.length} {cable.get_length_unit_display()}")
|
||||||
# Add cable to drawing
|
color = cable.color or '000000'
|
||||||
self.connectors.append(cable)
|
|
||||||
|
|
||||||
# Draw fan-ins
|
# Collect all connected nodes to this cable
|
||||||
if len(near_ends) > 1 and fanin:
|
near = [term for term in near_terminations if term.object in cable.a_terminations]
|
||||||
for term in terminations:
|
far = [term for term in far_terminations if term.object in cable.b_terminations]
|
||||||
if term.object.cable == link:
|
if not (near and far):
|
||||||
self.draw_fanin(term, cable)
|
# 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
|
# Select most-probable start and end position
|
||||||
elif type(link) is WirelessLink:
|
start = near[0].bottom_center
|
||||||
wirelesslink = self.draw_wirelesslink(link)
|
end = far[0].top_center
|
||||||
self.connectors.append(wirelesslink)
|
text_offset = 0
|
||||||
|
|
||||||
# Far end termination(s)
|
if len(near) > 1:
|
||||||
if len(far_ends) > 1:
|
# Handle Fan-In - change start position to be directly below start
|
||||||
if fanout:
|
start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
|
||||||
self.cursor += FANOUT_HEIGHT
|
self.draw_fanin(start, near, color)
|
||||||
terminations = self.draw_terminations(far_ends)
|
text_offset -= FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||||
for term in terminations:
|
elif len(far) > 1:
|
||||||
if hasattr(term.object, 'cable') and link_cables.get(term.object.cable.pk):
|
# Handle Fan-Out - change end position to be directly above end
|
||||||
self.draw_fanout(term, link_cables.get(term.object.cable.pk))
|
end = (start[0], end[1] - FANOUT_HEIGHT - FANOUT_LEG_HEIGHT)
|
||||||
else:
|
self.draw_fanout(end, far, color)
|
||||||
self.draw_terminations(far_ends)
|
text_offset -= FANOUT_HEIGHT
|
||||||
elif far_ends:
|
|
||||||
self.draw_terminations(far_ends)
|
|
||||||
else:
|
|
||||||
# Link is not connected to anything
|
|
||||||
break
|
|
||||||
|
|
||||||
# Far end parent
|
# Create the connector
|
||||||
parent_objects = set(end.parent_object for end in far_ends)
|
connector = Connector(
|
||||||
self.draw_parent_objects(parent_objects)
|
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
|
# Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with
|
||||||
# a CircuitTermination)
|
# a CircuitTermination)
|
||||||
elif far_ends:
|
elif far_ends:
|
||||||
|
|
||||||
# Attachment
|
# Attachment
|
||||||
attachment = self.draw_attachment()
|
attachment = self.draw_attachment()
|
||||||
self.connectors.append(attachment)
|
self.connectors.append(attachment)
|
||||||
|
|
||||||
# Object
|
# Object
|
||||||
self.draw_parent_objects(far_ends)
|
parent_object_nodes = self.draw_parent_objects(far_ends)
|
||||||
|
|
||||||
# Determine drawing size
|
# Determine drawing size
|
||||||
self.drawing = svgwrite.Drawing(
|
self.drawing = svgwrite.Drawing(
|
||||||
|
|
Loading…
Reference in New Issue