Skip to content

Commit d2d2106

Browse files
authored
Closes #163: Show raster and snap icons (#550)
* proof of concept * add grid size to forms and individual options
1 parent 9072faf commit d2d2106

File tree

9 files changed

+206
-24
lines changed

9 files changed

+206
-24
lines changed

netbox_topology_views/api/serializers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,4 @@ class Meta:
5050
class IndividualOptionsSerializer(NetBoxModelSerializer):
5151
class Meta:
5252
model = IndividualOptions
53-
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "group_virtualchassis", "draw_default_layout", "straight_cables")
53+
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size")

netbox_topology_views/api/views.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def list(self, request):
109109

110110
if request.GET:
111111

112-
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables = get_query_settings(request)
112+
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = get_query_settings(request)
113113

114114
# Read options from saved filters as NetBox does not handle custom plugin filters
115115
if "filter_id" in request.GET and request.GET["filter_id"] != '':
@@ -131,6 +131,7 @@ def list(self, request):
131131
if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
132132
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
133133
if straight_cables == False and 'straight_cables' in saved_filter_params: show_neighbors = saved_filter_params['straight_cables']
134+
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
134135
except SavedFilter.DoesNotExist: # filter_id not found
135136
pass
136137
except Exception as inst:
@@ -162,6 +163,7 @@ def list(self, request):
162163
group_virtualchassis=group_virtualchassis,
163164
group_id=group_id,
164165
straight_cables=straight_cables,
166+
grid_size=grid_size,
165167
)
166168
xml_data = export_data_to_xml(topo_data).decode('utf-8')
167169

netbox_topology_views/forms.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class DeviceFilterForm(
3636
FieldSet(
3737
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
3838
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
39-
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', name=_("Options")
39+
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options")
4040
),
4141
FieldSet('id', name=_("Device")),
4242
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
@@ -327,6 +327,14 @@ class DeviceFilterForm(
327327
choices=BOOLEAN_WITH_BLANK_CHOICES
328328
)
329329
)
330+
grid_size = forms.IntegerField(
331+
label=_('Grid Size'),
332+
required=False,
333+
initial=0,
334+
min_value=0,
335+
max_value=1000,
336+
help_text=_('Show grid and snap dragged icons to grid. Set to 0 to disable grid and snapping.')
337+
)
330338

331339
class CoordinateGroupsForm(NetBoxModelForm):
332340
fieldsets = (
@@ -521,6 +529,7 @@ class IndividualOptionsForm(NetBoxModelForm):
521529
'group_virtualchassis',
522530
'draw_default_layout',
523531
'straight_cables',
532+
'grid_size',
524533
),
525534
)
526535

@@ -662,6 +671,16 @@ class IndividualOptionsForm(NetBoxModelForm):
662671
help_text=_('Enable this option if you want to draw cables as straight lines '
663672
'instead of curves.')
664673
)
674+
grid_size = forms.IntegerField(
675+
label=_('Grid Size'),
676+
required=True,
677+
initial=0,
678+
min_value=0,
679+
max_value=1000,
680+
help_text=_('Default grid value. Set to 0 to disable grid. '
681+
'Integers between 0 and 1000 are allowed. Snap to grid will be '
682+
'automatically enabled for values > 0.')
683+
)
665684

666685
class Meta:
667686
model = IndividualOptions
@@ -670,5 +689,5 @@ class Meta:
670689
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
671690
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
672691
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'draw_default_layout',
673-
'straight_cables'
692+
'straight_cables', 'grid_size'
674693
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.0.6 on 2024-08-14 09:46
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('netbox_topology_views', '0009_individualoptions_group_virtualchassis'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='individualoptions',
15+
name='grid_size',
16+
field=models.PositiveSmallIntegerField(default=0),
17+
),
18+
]

netbox_topology_views/models.py

+3
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ class IndividualOptions(NetBoxModel):
404404
straight_cables = models.BooleanField(
405405
default=False
406406
)
407+
grid_size = models.PositiveSmallIntegerField(
408+
default=0
409+
)
407410

408411
_netbox_private = True
409412

netbox_topology_views/static/netbox_topology_views/js/app.js

+18-18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

netbox_topology_views/static_dev/js/home.js

+127
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,129 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
7575
const group_racks = topologyData.options.group_racks
7676
const group_virtualchassis = topologyData.options.group_virtualchassis
7777

78+
const gridSize = parseInt(topologyData.options.grid_size[0]);
79+
var dragMode = false;
80+
7881
graph = new Network(container, { nodes, edges }, options)
7982
graph.fit()
8083

84+
function getGridPosition(nodeId, gridSize) {
85+
x = graph.getPosition(nodeId).x;
86+
y = graph.getPosition(nodeId).y;
87+
88+
if(x >= 0) {
89+
if((x % gridSize) > (gridSize / 2)) {
90+
x += gridSize;
91+
}
92+
}
93+
else {
94+
if((-x % gridSize) > (gridSize / 2)) {
95+
x -= gridSize;
96+
}
97+
}
98+
x = x - x % gridSize;
99+
100+
if(y >= 0) {
101+
if((y % gridSize) > (gridSize / 2)) {
102+
y += gridSize;
103+
}
104+
}
105+
else {
106+
if((-y % gridSize) > (gridSize / 2)) {
107+
y -= gridSize;
108+
}
109+
}
110+
y = y - y % gridSize;
111+
112+
return {
113+
x: x,
114+
y: y
115+
};
116+
}
117+
118+
function drawGrid(canvascontext) {
119+
// Canvas can be zoomed. It then contains more or less virtual pixels than the real number of pixels
120+
const zoomFactor = graph.getScale() * window.devicePixelRatio;
121+
const virtualWidth = canvascontext.canvas.width / zoomFactor;
122+
const virtualHeight = canvascontext.canvas.height / zoomFactor;
123+
124+
// Canvas can be moved. Get the center of the virtual canvas. Take the grid into account
125+
const virtualCenter = graph.getViewPosition();
126+
const rasterizedCenterX = virtualCenter.x - virtualCenter.x % gridSize;
127+
const rasterizedCenterY = virtualCenter.y - virtualCenter.y % gridSize;
128+
129+
// Calculate virtual space for the grid
130+
const hSpace = (virtualWidth / 2) - (virtualWidth / 2) % gridSize + gridSize;
131+
const vSpace = (virtualHeight / 2) - (virtualHeight / 2) % gridSize + gridSize;
132+
133+
// Calculate virtual position for the grid
134+
const left = rasterizedCenterX - gridSize - hSpace;
135+
const right = rasterizedCenterX + gridSize + hSpace;
136+
const top = rasterizedCenterY - gridSize - vSpace;
137+
const bottom = rasterizedCenterY + gridSize + vSpace;
138+
139+
// Draw grid
140+
canvascontext.beginPath();
141+
142+
for (let x = left; x < right; x += gridSize) {
143+
canvascontext.moveTo(x, top);
144+
canvascontext.lineTo(x, bottom);
145+
}
146+
147+
for (let y = top; y < bottom; y += gridSize) {
148+
canvascontext.moveTo(left, y);
149+
canvascontext.lineTo(right, y);
150+
}
151+
152+
canvascontext.strokeStyle = '#777777';
153+
canvascontext.stroke();
154+
}
155+
156+
function drawGridSnapHint(canvascontext) {
157+
// Draw grid hinting line and circle
158+
if(gridSize > 0 && dragMode == true && graph.getSelectedNodes().length > 0) {
159+
for(i = 0; i < graph.getSelectedNodes().length; i++) {
160+
id = graph.getSelectedNodes()[i];
161+
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
162+
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
163+
164+
canvascontext.beginPath();
165+
canvascontext.arc(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y, 5, 0, 2 * Math.PI);
166+
canvascontext.fillStyle = '#FF3D3D';
167+
canvascontext.fill();
168+
169+
canvascontext.beginPath();
170+
canvascontext.moveTo(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y);
171+
canvascontext.lineTo(pos.x, pos.y);
172+
canvascontext.strokeStyle = '#FF3D3D';
173+
canvascontext.stroke();
174+
175+
canvascontext.beginPath();
176+
canvascontext.arc(pos.x, pos.y, 10, 0, 2 * Math.PI);
177+
canvascontext.fillStyle = '#9C0000';
178+
canvascontext.fill();
179+
}
180+
}
181+
}
182+
}
183+
184+
graph.on('dragStart', (params) => {
185+
dragMode = true;
186+
})
187+
81188
graph.on('dragEnd', (params) => {
189+
dragMode = false;
190+
// Place icon on the grid
191+
if(gridSize > 0 && graph.getSelectedNodes().length > 0) {
192+
for(i = 0; i < graph.getSelectedNodes().length; i++) {
193+
id = graph.getSelectedNodes()[i];
194+
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
195+
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
196+
window.nodes.update({id: graph.getSelectedNodes()[i], x: pos.x, y: pos.y});
197+
}
198+
}
199+
}
200+
82201
if (coordSaveCheckbox.options[coordSaveCheckbox.selectedIndex].text != "Yes") return
83202

84203
Promise.allSettled(
@@ -139,12 +258,20 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
139258
}
140259
})
141260

261+
graph.on('beforeDrawing', (canvascontext) => {
262+
if (gridSize > 0) {
263+
drawGrid(canvascontext);
264+
}
265+
})
266+
142267
graph.on('afterDrawing', (canvascontext) => {
143268
allRectangles = [];
144269
if(group_sites != null && group_sites == 'on') { drawGroupRectangles(canvascontext, groupedNodeSites, siteRectParams); }
145270
if(group_locations != null && group_locations == 'on') { drawGroupRectangles(canvascontext, groupedNodeLocations, locationRectParams); }
146271
if(group_racks != null && group_racks == 'on') { drawGroupRectangles(canvascontext, groupedNodeRacks, rackRectParams); }
147272
if(group_virtualchassis != null && group_virtualchassis == 'on') { drawGroupRectangles(canvascontext, groupedNodeVirtualchassis, virtualchassisRectParams); }
273+
274+
drawGridSnapHint(canvascontext);
148275
})
149276

150277
graph.on('click', (canvascontext) => {

netbox_topology_views/utils.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,11 @@ def get_query_settings(request):
196196
if request.GET["straight_cables"] == "True":
197197
straight_cables = True
198198

199-
return filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables
199+
grid_size = 0
200+
if "grid_size" in request.GET:
201+
grid_size = request.GET.getlist('grid_size')
202+
203+
return filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size
200204

201205
class LinePattern():
202206
wireless = [2, 10, 2, 10]

netbox_topology_views/views.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def get_topology_data(
349349
group_virtualchassis: bool,
350350
group_id,
351351
straight_cables: bool,
352+
grid_size: list,
352353
):
353354

354355
supported_termination_types = []
@@ -685,6 +686,10 @@ def get_topology_data(
685686
options['group_sites'] = 'on'
686687
if group_virtualchassis:
687688
options['group_virtualchassis'] = 'on'
689+
if grid_size:
690+
options['grid_size'] = grid_size
691+
else:
692+
options['grid_size'] = list('0')
688693

689694
for qs_device in queryset:
690695
if qs_device.pk not in nodes_devices and show_unconnected:
@@ -724,7 +729,7 @@ def get(self, request):
724729

725730
if request.GET:
726731

727-
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables = get_query_settings(request)
732+
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = get_query_settings(request)
728733

729734
# Read options from saved filters as NetBox does not handle custom plugin filters
730735
if "filter_id" in request.GET and request.GET["filter_id"] != '':
@@ -746,6 +751,7 @@ def get(self, request):
746751
if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
747752
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
748753
if straight_cables == False and 'straight_cables' in saved_filter_params: straight_cables = saved_filter_params['straight_cables']
754+
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
749755
except SavedFilter.DoesNotExist: # filter_id not found
750756
pass
751757
except Exception as inst:
@@ -779,6 +785,7 @@ def get(self, request):
779785
group_virtualchassis=group_virtualchassis,
780786
group_id=group_id,
781787
straight_cables=straight_cables,
788+
grid_size=grid_size,
782789
)
783790

784791
else:
@@ -807,6 +814,7 @@ def get(self, request):
807814
if individualOptions.group_racks: q['group_racks'] = "True"
808815
if individualOptions.group_virtualchassis: q['group_virtualchassis'] = "True"
809816
if individualOptions.straight_cables: q['straight_cables'] = "True"
817+
if individualOptions.grid_size: q['grid_size'] = individualOptions.grid_size
810818
if individualOptions.draw_default_layout:
811819
q['draw_init'] = "True"
812820
else:
@@ -1167,6 +1175,7 @@ def get(self, request):
11671175
'group_virtualchassis': queryset.group_virtualchassis,
11681176
'draw_default_layout': queryset.draw_default_layout,
11691177
'straight_cables': queryset.straight_cables,
1178+
'grid_size': queryset.grid_size,
11701179
},
11711180
)
11721181

0 commit comments

Comments
 (0)