Compare commits

...

5 Commits

Author SHA1 Message Date
Moehritz 256f6f8753
Merge 2c93dd03e1 into da6a1ef03e 2024-03-28 08:10:44 +08:00
Jeremy Stretch da6a1ef03e Clean up the Markdown reference guide 2024-03-26 16:26:47 -04:00
Moritz Geist 2c93dd03e1 account for swapped terminations in cable object
also remove out-of-scope changes to tooltips
2024-01-10 14:29:46 +01:00
Moritz Geist ced44832f7 Remove dangling logging message used during development 2024-01-09 14:22:36 +01:00
Moritz Geist 6af3aad362 Fixes #14722, Fixes #13922: Update the CableRender
This commit updates the cable rendering logic to fix
both issue #14722 and #13922. Before, objects, terminations
and cables where drawn in the svg without context of each
other.
Now the following changes are applied:
- Hosts and Terminations are where possible sorted alphabetically
- Terminations and Cables are visually connected, and if necessary not in a vertical line
- Terminations and Hosts are visually aligning
- Cable Tooltips contain more information
2024-01-09 13:51:09 +01:00
2 changed files with 389 additions and 509 deletions

View File

@ -1,353 +1,254 @@
---
hide:
- toc
---
# Markdown # Markdown
NetBox supports markdown rendering for certain text fields. NetBox supports Markdown rendering for certain text fields. Some common examples are provided below. For a complete Markdown reference, please see [Markdownguide.org](https://www.markdownguide.org/basic-syntax/).
## Syntax ## Headings
##### Table of Contents
[Headers](#headers)
[Emphasis](#emphasis)
[Lists](#lists)
[Links](#links)
[Images](#images)
[Code Blocks](#code)
[Tables](#tables)
[Blockquotes](#blockquotes)
[Inline HTML](#html)
[Horizontal Rule](#hr)
[Line Breaks](#lines)
<a name="headers"></a>
## Headers
```no-highlight ```no-highlight
# H1 # Heading 1
## H2 ## Heading 2
### H3 ### Heading 3
#### H4 #### Heading 4
##### H5 ##### Heading 5
###### H6 ###### Heading 6
```
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
Alternatively, for H1 and H2, an underline-ish style: Alternatively, for H1 and H2, an underline-ish style:
Alt-H1 ```no-highlight
====== Heading 1
=========
Alt-H2 Heading 2
------ ---------
``` ```
# H1 <h1>Heading 1</h1>
## H2 <h2>Heading 2</h2>
### H3
#### H4
##### H5
###### H6
<a name="emphasis"></a> ## Text
## Emphasis
```no-highlight ```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_. Italicize text with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
``` ```
Emphasis, aka italics, with *asterisks* or _underscores_. Italicize text with *asterisks* or _underscores_.
Strong emphasis, aka bold, with **asterisks** or __underscores__.
Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
<a name="lists"></a>
## Lists
(In this example, leading and trailing spaces are shown with with dots: ⋅)
```no-highlight ```no-highlight
1. First ordered list item Bold text with two **asterisks** or __underscores__.
2. Another item
⋅⋅* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
⋅⋅1. Ordered sub-list
4. And another item.
⋅⋅⋅You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
⋅⋅⋅To have a line break without a paragraph, you will need to use two trailing spaces.⋅⋅
⋅⋅⋅Note that this line is separate, but within the same paragraph.⋅⋅
⋅⋅⋅(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
``` ```
1. First ordered list item Bold text with two **asterisks** or __underscores__.
2. Another item
* Unordered sub-list.
1. Actual numbers don't matter, just that it's a number
1. Ordered sub-list
4. And another item.
You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).
To have a line break without a paragraph, you will need to use two trailing spaces.
Note that this line is separate, but within the same paragraph.
(This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)
* Unordered list can use asterisks
- Or minuses
+ Or pluses
<a name="links"></a>
## Links
There are two ways to create links.
```no-highlight ```no-highlight
[I'm an inline-style link](https://www.google.com) Strike text with two tildes. ~~Deleted text.~~
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
``` ```
[I'm an inline-style link](https://www.google.com) Strike text with two tildes. ~~Deleted text.~~
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself].
URLs and URLs in angle brackets will automatically get turned into links.
http://www.example.com or <http://www.example.com> and sometimes
example.com (but not on Github, for example).
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[link text itself]: http://www.reddit.com
<a name="images"></a>
## Images
```
Here's the NetBox logo (hover to see the title text):
Inline-style:
![alt text](/media/misc/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: /media/misc/netbox_logo.png "Logo Title Text 2"
```
Here's the NetBox logo (hover to see the title text):
Inline-style:
![alt text](../media/misc/netbox_logo.png "Logo Title Text 1")
Reference-style:
![alt text][logo]
[logo]: ../media/misc/netbox_logo.png "Logo Title Text 2"
<a name="code"></a>
## Code blocks
```
Inline `code` has `back-ticks around` it.
```
Inline `code` has `back-ticks around` it.
Blocks of code are fenced by lines with three back-ticks <code>```</code>
````
```
var s = "Code block";
alert(s);
```
````
```
var s = "Code block";
alert(s);
```
<a name="tables"></a>
## Tables
```no-highlight
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell.
The outer pipes (|) are optional, and you don't need to make the
raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
```
Colons can be used to align columns.
| Tables | Are | Cool |
| ------------- |:-------------:| -----:|
| col 3 is | right-aligned | $1600 |
| col 2 is | centered | $12 |
| zebra stripes | are neat | $1 |
There must be at least 3 dashes separating each header cell. The outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.
Markdown | Less | Pretty
--- | --- | ---
*Still* | `renders` | **nicely**
1 | 2 | 3
<a name="blockquotes"></a>
## Blockquotes
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
```
> Blockquotes are very handy in email to emulate reply text.
> This line is part of the same quote.
Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
<a name="html"></a>
## Inline HTML
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
```no-highlight
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
```
<dl>
<dt>Definition list</dt>
<dd>Is something people use sometimes.</dd>
<dt>Markdown in HTML</dt>
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
<a name="hr"></a>
## Horizontal Rule
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
```
Three or more...
---
Hyphens
***
Asterisks
___
Underscores
<a name="lines"></a>
## Line Breaks ## Line Breaks
By default, Markdown will remove line breaks between successive lines of text. For example:
``` ```no-highlight
Here's a line for us to start with. This is one line.
And this is another line.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*. One more line here.
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
``` ```
Here's a line for us to start with. This is one line.
And this is another line.
One more line here.
This line is separated from the one above by two newlines, so it will be a *separate paragraph*. To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
This line is also begins a separate paragraph, but... ```no-highlight
This line is only separated by a single newline, so it's a separate line in the *same paragraph*. This is one line.⋅⋅
And this is another line.⋅⋅
One more line here.
```
Based on [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) by [adam-p](https://github.com/adam-p) licensed under [CC-BY](https://creativecommons.org/licenses/by/3.0/) This is one line.
And this is another line.
One more line here.
## Lists
Use asterisks or hyphens for unordered lists. Indent items by four spaces to start a child list.
```no-highlight
* Alpha
* Bravo
* Charlie
* Child item 1
* Child item 2
* Delta
```
* Alpha
* Bravo
* Charlie
* Child item 1
* Child item 2
* Delta
Use digits followed by periods for ordered (numbered) lists.
```no-highlight
1. Red
2. Green
3. Blue
1. Light blue
2. Dark blue
4. Orange
```
1. Red
2. Green
3. Blue
1. Light blue
2. Dark blue
4. Orange
## Links
Text can be rendered as a hyperlink by encasing it in square brackets, followed by a URL in parentheses. A title (text displayed on hover) may optionally be included as well.
```no-highlight
Here's an [example](https://www.example.com) of a link.
And here's [another link](https://www.example.com "Click me!"), this time with a title.
```
Here's an [example](https://www.example.com) of a link.
And here's [another link](https://www.example.com "Click me!"), with a title.
## Images
The syntax for embedding an image is very similar to that used for a hyperlink. Alternate text should always be provided; this will be displayed if the image fails to load. As with hyperlinks, title text is optional.
```no-highlight
![Alternate text](/path/to/image.png "Image title text")
```
## Code Blocks
Single backticks can be used to annotate code inline. Text enclosed by lines of three backticks will be displayed as a code block.
```no-highlight
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
```
Paragraphs are rendered in HTML using `<p>` and `</p>` tags.
````
```
def my_func(foo, bar):
# Do something
return foo * bar
```
````
```no-highlight
def my_func(foo, bar):
# Do something
return foo * bar
```
## Tables
Simple tables can be constructed using the pipe character (`|`) to denote columns, and hyphens (`-`) to denote the heading. Inline Markdown can be used to style text within columns.
```no-highlight
| Heading 1 | Heading 2 | Heading 3 |
|-----------|-----------|-----------|
| Row 1 | Alpha | Red |
| Row 2 | **Bravo** | Green |
| Row 3 | Charlie | ~~Blue~~ |
```
| Heading 1 | Heading 2 | Heading 3 |
|-----------|-----------|-----------|
| _Row 1_ | Alpha | Red |
| Row 2 | **Bravo** | Green |
| Row 3 | Charlie | ~~Blue~~ |
Colons can be used to align text to the left or right side of a column.
```no-highlight
| Left-aligned | Centered | Right-aligned |
|:-------------|:--------:|--------------:|
| Text | Text | Text |
| Text | Text | Text |
| Text | Text | Text |
```
| Left-aligned | Centered | Right-aligned |
|:-------------|:--------:|--------------:|
| Text | Text | Text |
| Text | Text | Text |
| Text | Text | Text |
## Blockquotes
Text can be wrapped in a blockquote by prepending a right angle bracket (`>`) before each line.
```no-highlight
> I think that I shall never see
> a graph more lovely than a tree.
> A tree whose crucial property
> is loop-free connectivity.
```
> I think that I shall never see
> a graph more lovely than a tree.
> A tree whose crucial property
> is loop-free connectivity.
Markdown removes line breaks by default. To preserve line breaks, append two spaces to each line (represented below with the `⋅` character).
```no-highlight
> I think that I shall never see⋅⋅
> a graph more lovely than a tree.⋅⋅
> A tree whose crucial property⋅⋅
> is loop-free connectivity.
```
> I think that I shall never see
> a graph more lovely than a tree.
> A tree whose crucial property
> is loop-free connectivity.
## Horizontal Rule
A horizontal rule is a single line rendered across the width of the page using a series of three or more hyphens or asterisks. It can be useful for separating sections of content.
```no-highlight
Content
---
More content
***
Final content
```
Content
---
More content
***
Final content

View File

@ -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(