Просмотр исходного кода

Add support for cable fan-outs

jeremystretch 3 лет назад
Родитель
Сommit
8d92ec2007
1 измененных файлов с 112 добавлено и 76 удалено
  1. 112 76
      netbox/dcim/svg/cables.py

+ 112 - 76
netbox/dcim/svg/cables.py

@@ -16,8 +16,8 @@ __all__ = (
 OFFSET = 0.5
 PADDING = 10
 LINE_HEIGHT = 20
-
-TERMINATION_WIDTH = 80
+FANOUT_HEIGHT = 25
+TERMINATION_WIDTH = 100
 
 
 class Node(Hyperlink):
@@ -66,6 +66,47 @@ class Node(Hyperlink):
         return self.box['x'] + self.box['width'] / 2, self.box['y'] + self.box['height']
 
 
+class Connector(Group):
+    """
+    Return an SVG group containing a line element and text labels representing a Cable.
+
+    Arguments:
+        color: Cable (line) color
+        url: Hyperlink URL
+        labels: Iterable of text labels
+    """
+
+    def __init__(self, start, url, color, labels=[], **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)
+        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)
+
+        # 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='_blank')
+
+        # Add text label(s)
+        cursor = start[1]
+        cursor += PADDING * 2
+        for i, label in enumerate(labels):
+            cursor += LINE_HEIGHT
+            text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2)
+            text = Text(label, insert=text_coords, class_='bold' if not i else [])
+            link.add(text)
+
+        self.add(link)
+
+
 class CableTraceSVG:
     """
     Generate a graphical representation of a CablePath in SVG format.
@@ -136,13 +177,15 @@ class CableTraceSVG:
         node = Node(
             position=(0, self.cursor),
             width=self.width,
-            url=obj.get_absolute_url(),
+            url=f'{self.base_url}{obj.get_absolute_url()}',
             color=self._get_color(obj),
             labels=self._get_labels(obj)
         )
         self.parent_objects.append(node)
         self.cursor += node.box['height']
 
+        return node
+
     def draw_terminations(self, terminations):
         """
         Draw a row of terminating objects (e.g. interfaces) belonging to the same parent object, all of which
@@ -156,7 +199,7 @@ class CableTraceSVG:
             node = Node(
                 position=(x + i * TERMINATION_WIDTH, self.cursor),
                 width=TERMINATION_WIDTH,
-                url=term.get_absolute_url(),
+                url=f'{self.base_url}{term.get_absolute_url()}',
                 color=self._get_color(term),
                 labels=self._get_labels(term),
                 radius=5
@@ -169,53 +212,51 @@ class CableTraceSVG:
 
         return nodes
 
-    def draw_cable(self, color, url, labels):
-        """
-        Return an SVG group containing a line element and text labels representing a Cable.
-
-        :param color: Cable (line) color
-        :param url: Hyperlink URL
-        :param labels: Iterable of text labels
-        """
-        group = Group(class_='connector')
-
-        # Draw a "shadow" line to give the cable a border
-        start = (OFFSET + self.center, self.cursor)
-        height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
-        end = (start[0], start[1] + height)
-        cable_shadow = Line(start=start, end=end, class_='cable-shadow')
-        group.add(cable_shadow)
-
-        # Draw the cable
-        cable = Line(start=start, end=end, style=f'stroke: #{color}')
-        group.add(cable)
-
-        self.cursor += PADDING * 2
-
-        # Add link
-        link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
-
-        # 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)
+    def draw_fanin(self, node, connector):
+        self.connectors.extend((
+            Line(start=node.bottom_center, end=connector.start, class_='cable-shadow'),
+            Line(start=node.bottom_center, end=connector.start, style=f'stroke: #{connector.color}'),
+        ))
+
+    def draw_fanout(self, node, connector):
+        self.connectors.extend((
+            Line(start=connector.end, end=node.top_center, class_='cable-shadow'),
+            Line(start=connector.end, end=node.top_center, style=f'stroke: #{connector.color}')
+        ))
+
+    def draw_cable(self, cable):
+        labels = [
+            f'Cable {cable}',
+            cable.get_status_display()
+        ]
+        if cable.type:
+            labels.append(cable.get_type_display())
+        if cable.length and cable.length_unit:
+            labels.append(f'{cable.length} {cable.get_length_unit_display()}')
+        connector = Connector(
+            start=(self.center + OFFSET, self.cursor),
+            color=cable.color or '000000',
+            url=f'{self.base_url}{cable.get_absolute_url()}',
+            labels=labels
+        )
 
-        group.add(link)
-        self.cursor += PADDING * 2
+        self.cursor += connector.height
 
-        return group
+        return connector
 
-    def draw_wirelesslink(self, url, labels):
+    def draw_wirelesslink(self, wirelesslink):
         """
         Draw a line with labels representing a WirelessLink.
-
-        :param url: Hyperlink URL
-        :param labels: Iterable of text labels
         """
         group = Group(class_='connector')
 
+        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
@@ -226,7 +267,7 @@ class CableTraceSVG:
         self.cursor += PADDING * 2
 
         # Add link
-        link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
+        link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_blank')
 
         # Add text label(s)
         for i, label in enumerate(labels):
@@ -267,53 +308,48 @@ class CableTraceSVG:
 
         # Iterate through each (terms, cable, terms) segment in the path
         for i, segment in enumerate(traced_path):
-            near_ends, connector, far_ends = segment
+            near_ends, link, far_ends = segment
 
             # Near end parent
             if i == 0:
                 # If this is the first segment, draw the originating termination's parent object
                 self.draw_parent_object(near_ends[0].parent_object)
 
-            # Near end termination
-            self.draw_terminations(near_ends)
+            # Near end termination(s)
+            terminations = self.draw_terminations(near_ends)
 
             # Connector (a Cable or WirelessLink)
-            connector = connector[0]  # Remove Cable from list
-            if connector is not None:
+            link = link[0]  # Remove Cable from list
+            if link is not None:
 
                 # Cable
-                if type(connector) is Cable:
-                    connector_labels = [
-                        f'Cable {connector}',
-                        connector.get_status_display()
-                    ]
-                    if connector.type:
-                        connector_labels.append(connector.get_type_display())
-                    if connector.length and connector.length_unit:
-                        connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
-                    cable = self.draw_cable(
-                        color=connector.color or '000000',
-                        url=connector.get_absolute_url(),
-                        labels=connector_labels
-                    )
+                if type(link) is Cable:
+
+                    # Account for fan-ins height
+                    if len(near_ends) > 1:
+                        self.cursor += FANOUT_HEIGHT
+
+                    cable = self.draw_cable(link)
                     self.connectors.append(cable)
 
+                    # Draw fan-ins
+                    if len(near_ends) > 1:
+                        for term in terminations:
+                            self.draw_fanin(term, cable)
+
                 # WirelessLink
-                elif type(connector) is WirelessLink:
-                    connector_labels = [
-                        f'Wireless link {connector}',
-                        connector.get_status_display()
-                    ]
-                    if connector.ssid:
-                        connector_labels.append(connector.ssid)
-                    wirelesslink = self.draw_wirelesslink(
-                        url=connector.get_absolute_url(),
-                        labels=connector_labels
-                    )
+                elif type(link) is WirelessLink:
+                    wirelesslink = self.draw_wirelesslink(link)
                     self.connectors.append(wirelesslink)
 
-                # Far end termination
-                self.draw_terminations(far_ends)
+                # Far end termination(s)
+                if len(far_ends) > 1:
+                    self.cursor += FANOUT_HEIGHT
+                    terminations = self.draw_terminations(far_ends)
+                    for term in terminations:
+                        self.draw_fanout(term, cable)
+                else:
+                    self.draw_terminations(far_ends)
 
                 # Far end parent
                 self.draw_parent_object(far_ends[0].parent_object)