blob: 55067d832a42b4b43498de2717299a96395e1f85 [file] [log] [blame]
Steve McIntyre2454bf02015-09-23 18:33:02 +01001#! /usr/bin/python
2
3# Copyright 2015 Linaro Limited
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
18# MA 02110-1301, USA.
19#
20# Visualisation graphics module for VLANd
21#
22# This code uses python-gd to generate graphics ready for insertion
23# into our web interface. Example code in the self-test at the
24# bottom.
25
26import gd, os, sys
27
28if __name__ == '__main__':
29 vlandpath = os.path.abspath(os.path.normpath(os.path.dirname(sys.argv[0])))
30 sys.path.insert(0, vlandpath)
31 sys.path.insert(0, "%s/.." % vlandpath)
32
33from errors import InputError
34
35class Graphics:
36 """ Code and config for the visualisation graphics module """
37
38 font = None
39
40 # Default font size for the small labels
41 small_font_size = 12
42
43 # And the size for the top-level label
44 label_font_size = 24
45
46 # Size in pixels of that font, calculated later
47 twocharwidth = 0
48 charheight = 0
49
50 # How big a gap to leave between trunk connections
51 trunk_gap = 8
52
53 # Details of the legend
54 legend_width = 0
55 legend_height = 0
56 legend_text_width = 0
57 legend_text_height = 0
58 legend_total_width = 0
Steve McIntyre9ff96bf2015-09-23 18:54:53 +010059 legend_box_width = 0
60 legend_box_height = 0
Steve McIntyre2454bf02015-09-23 18:33:02 +010061
62 # Basic colour definitions used later
63 colour_defs = {}
64 colour_defs['black'] = (0, 0, 0)
65 colour_defs['white'] = (255, 255, 255)
66 colour_defs['purple'] = (255, 0, 255)
67 colour_defs['blue'] = (0, 0, 255)
68 colour_defs['darkgrey'] = (60, 60, 60)
69 colour_defs['yellow'] = (255, 255, 0)
70 colour_defs['red'] = (255, 0, 0)
71 colour_defs['aqua'] = (0, 255, 255)
72
73 pallette = {}
74
75 # colours for the background
76 pallette['bg_colour'] = 'purple'
77 pallette['transparent_colour'] = 'purple'
78 pallette['graphic_label_colour'] = 'black'
79
80 # switch colours
81 pallette['switch_outline_colour'] = 'black'
82 pallette['switch_fill_colour'] = 'darkgrey'
83 pallette['switch_label_colour'] = 'white'
84
85 # verious sets of port colours, matching the 'highlight' options in
86 # draw_port()
87 port_pallette = {}
88 port_pallette['normal'] = {}
89 port_pallette['normal']['port_box'] = 'white'
90 port_pallette['normal']['port_bg'] = 'black'
91 port_pallette['normal']['port_label'] = 'white'
92 port_pallette['normal']['trace'] = 'black'
93
94 port_pallette['trunk'] = {}
95 port_pallette['trunk']['port_box'] = 'white'
96 port_pallette['trunk']['port_bg'] = 'blue'
97 port_pallette['trunk']['port_label'] = 'yellow'
98 port_pallette['trunk']['trace'] = 'blue'
99
100 port_pallette['locked'] = {}
101 port_pallette['locked']['port_box'] = 'white'
102 port_pallette['locked']['port_bg'] = 'red'
103 port_pallette['locked']['port_label'] = 'yellow'
104 port_pallette['locked']['trace'] = 'red'
105
106 port_pallette['VLAN'] = {}
107 port_pallette['VLAN']['port_box'] = 'white'
108 port_pallette['VLAN']['port_bg'] = 'aqua'
109 port_pallette['VLAN']['port_label'] = 'black'
110 port_pallette['VLAN']['trace'] = 'aqua'
111
112 im = None
113
114 # TODO: make colours configurable, add maybe parsing for
115 # /etc/X11/rgb.txt to allow people to use arbitrary names?
116
117 # Choose a font for our graphics to use. Pass in a list of fonts
118 # to be tried, in priority order.
119 def set_font(self, fontlist):
120 for font in fontlist:
121 if os.path.exists(font):
122 self.font = os.path.abspath(font)
123 break
124
125 # Work out how big we need to be for the biggest possible text
126 # in a 2-digit number. Grotty, but we need to know this later.
127 for value in range (0, 100):
128 (width, height) = self.get_label_size(repr(value), self.small_font_size)
129 self.twocharwidth = max(self.twocharwidth, width)
130 self.charheight = max(self.charheight, height)
131
132 # Now we can also calulate other stuff
133 self._calc_legend_size()
134
135 # Create a canvas and set things up ready for use
136 def create_canvas(self, x, y):
137 im = gd.image((x, y))
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100138
Steve McIntyre2454bf02015-09-23 18:33:02 +0100139 # Allocate our colours in the image's colour map
140 for key in self.colour_defs.iterkeys():
141 im.colorAllocate((self.colour_defs[key][0],
142 self.colour_defs[key][1],
143 self.colour_defs[key][2]))
144
145 im.fill((0,0), im.colorExact(self.colour_defs[self.pallette['bg_colour']]))
146 im.colorTransparent(im.colorExact(self.colour_defs[self.pallette['transparent_colour']]))
147 im.interlace(0)
148 self.im = im
149
150 # Using our selected font, what dimensions will a particular piece
151 # of text take?
152 def get_label_size(self, label, font_size):
153 tmp_im = gd.image((200, 200))
154 (llx, lly, lrx, lry, urx, ury, ulx, uly) = tmp_im.get_bounding_rect(self.font,
155 font_size,
156 0.0,
157 (10, 100), label)
158 width = max(lrx, urx) - min(llx, ulx)
159 height = max(lly, lry) - min(uly, ury)
160 return (width, height)
161
162 # Draw a trunk connection between two ports
163 #
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000164 # Ports are defined as (ulx,uly),(lrx,lry), top): x, y
165 # co-ordinates of UL and LR corners, and whether the port is on
166 # the top or bottom row of a switch, i.e. does the wire come up or
167 # down when it leaves the port.
Steve McIntyre2454bf02015-09-23 18:33:02 +0100168 def draw_trunk(self, trunknum, node1, node2, colour):
169 for node in (node1, node2):
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000170 ((ulx,uly),(lrx,lry),top) = node
171
172 # Work out the co-ordinates for a line vertically up or
173 # down from the edge of the port
174 x1 = int((ulx + lrx) / 2)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100175 x2 = x1
176 if (top):
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000177 y1 = uly
Steve McIntyre2454bf02015-09-23 18:33:02 +0100178 y2 = y1 - (self.trunk_gap * (trunknum + 1))
179 else:
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000180 y1 = lry
Steve McIntyre2454bf02015-09-23 18:33:02 +0100181 y2 = y1 + (self.trunk_gap * (trunknum + 1))
182 # Quick hack - use 2-pixel wide rectangles as thick lines :-)
183 # First line, vertically up/down from the port
184 self.im.rectangle((x1-1,y1), (x2,y2), self.im.colorExact(self.colour_defs[colour]))
185 # Now draw horizontally across to the left margin space
186 x3 = self.trunk_gap * (trunknum + 1)
187 self.im.rectangle((x3, y2), (x2,y2+1), self.im.colorExact(self.colour_defs[colour]))
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000188
Steve McIntyre2454bf02015-09-23 18:33:02 +0100189 # Now join up the trunks vertically
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000190 ((ulx1,uly1),(lrx1,lry1),top1) = node1
Steve McIntyre2454bf02015-09-23 18:33:02 +0100191 if (top1):
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000192 y1 = uly1 - self.trunk_gap * (trunknum + 1)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100193 else:
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000194 y1 = lry1 + self.trunk_gap * (trunknum + 1)
195 ((ulx2,uly2),(lrx2,lry2),top2) = node2
Steve McIntyre2454bf02015-09-23 18:33:02 +0100196 if (top2):
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000197 y2 = uly2 - self.trunk_gap * (trunknum + 1)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100198 else:
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000199 y2 = lry2 + self.trunk_gap * (trunknum + 1)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100200 x1 = self.trunk_gap * (trunknum + 1)
201 self.im.rectangle((x1, y1), (x1+1,y2), self.im.colorExact(self.colour_defs[colour]))
202
203 # How big is the legend?
204 def _calc_legend_size(self):
205 max_width = 0
206 max_height = 0
207
208 for value in self.port_pallette.iterkeys():
209 (width, height) = self.get_label_size(value, self.small_font_size)
210 max_width = max(max_width, width)
211 max_height = max(max_height, height)
212
213 (width, height) = self.get_label_size('##', self.small_font_size)
214 self.legend_box_width = width + 6
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100215 self.legend_box_height = height + 6
Steve McIntyre2454bf02015-09-23 18:33:02 +0100216 self.legend_width = max_width + self.legend_box_width + 10
217 self.legend_height = 3 + self.legend_box_height + 3
218 self.legend_text_width = max_width
219 self.legend_text_height = max_height
220 self.legend_total_width = 6 + (len(self.port_pallette) * self.legend_width)
221
222 # Return the legend dimensions
223 def get_legend_dimensions(self):
224 return (self.legend_total_width, self.legend_height)
225
226 # Draw the legend using (left, top) as the top left corner
227 def draw_legend(self, left, top):
228 lrx = left + self.legend_total_width - 1
229 lry = top + self.legend_height - 1
230 self.im.rectangle((left, top), (lrx, lry),
231 self.im.colorExact(self.colour_defs[self.pallette['switch_outline_colour']]),
232 self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']]))
233 curr_x = left + 3
234 curr_y = top + 3
235
236 for value in sorted(self.port_pallette):
237 box_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_box']])
238 box_bg_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_bg']])
239 text_colour = self.im.colorExact(self.colour_defs[self.port_pallette[value]['port_label']])
240 lrx = curr_x + self.legend_box_width - 1
241 lry = curr_y + self.legend_box_height - 1
242 self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour)
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100243
Steve McIntyre2454bf02015-09-23 18:33:02 +0100244 llx = curr_x + 4
245 lly = curr_y + self.legend_box_height - 4
246 self.im.string_ttf(self.font, self.small_font_size, 0.0, (llx, lly), '##', text_colour)
247 curr_x += self.legend_box_width
248 self.im.string_ttf(self.font, self.small_font_size, 0.0, (curr_x + 3, lly), value,
249 self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']]))
250 curr_x += self.legend_text_width + 10
251
252 # Draw the graphic's label using (left, top) as the top left
253 # corner with a box around
254 def draw_label(self, left, top, label, gap):
255 box_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])
256 box_bg_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_fill_colour']])
257 text_colour = self.im.colorExact(self.colour_defs[self.pallette['switch_label_colour']])
258 (width, height) = self.get_label_size(label, self.label_font_size)
259 curr_x = left
260 curr_y = top
261 lrx = curr_x + width + gap
262 lry = curr_y + height + 20
263 self.im.rectangle((curr_x,curr_y), (lrx,lry), box_colour, box_bg_colour)
264 curr_x = left + 10
265 curr_y = top + height + 6
266 self.im.string_ttf(self.font, self.label_font_size, 0.0, (curr_x, curr_y), label, text_colour)
267
268
269class Switch:
270 """ Code and config for dealing with a switch """
271 port_width = 0
272 port_height = 0
273 text_width = 0
274 text_height = 0
275 label_left = 0
276 label_bot = 0
277 total_width = 0
278 total_height = 0
279 num_ports = 0
280 left = None
281 top = None
282 name = None
283
284 # Set up a new switch instance; calculate all the sizes so we can
285 # size our canvas
286 def __init__(self, g, num_ports, name):
287 self.num_ports = num_ports
288 self.name = name
289 self._calc_port_size(g)
290 self._calc_switch_size(g)
291
292 # How big is a port and the text within it?
293 def _calc_port_size(self, g):
Steve McIntyre2454bf02015-09-23 18:33:02 +0100294 self.text_width = g.twocharwidth
295 self.text_height = g.charheight
296 # Leave enough space around the text for a nice clear box
297 self.port_width = self.text_width + 6
298 self.port_height = self.text_height + 6
299
300 # How big is the full switch, including all the ports and the
301 # switch name label?
302 def _calc_switch_size(self, g):
303 (label_width, label_height) = g.get_label_size(self.name, g.small_font_size)
304 num_ports = self.num_ports
305 # Make sure we have an even number for 2 rows
306 if (self.num_ports & 1):
307 num_ports += 1
308 self.label_left = 3 + (num_ports * self.port_width / 2) + 3
309 self.label_bot = self.port_height - 2
310 self.total_width = self.label_left + label_width + 3
311 self.total_height = 3 + max(label_height, (2 * self.port_height)) + 3
312
313 # Return the switch dimensions
314 def get_dimensions(self):
315 return (self.total_width, self.total_height)
316
317 # Draw the basic switch outline and label using (left, top) as the
318 # top left corner. The switch object will remember this origin for
319 # later use when drawing ports.
320 def draw_switch(self, g, left, top):
321 self.left = left
322 self.top = top
Steve McIntyre2454bf02015-09-23 18:33:02 +0100323 lrx = left + self.total_width -1
324 lry = top + self.total_height - 1
325 g.im.rectangle((left, top), (lrx, lry),
326 g.im.colorExact(g.colour_defs[g.pallette['switch_outline_colour']]),
327 g.im.colorExact(g.colour_defs[g.pallette['switch_fill_colour']]))
328 llx = left + self.label_left
329 lly = top + self.label_bot
330 g.im.string_ttf(g.font, g.small_font_size, 0.0, (llx, lly), self.name,
331 g.im.colorExact(g.colour_defs[g.pallette['switch_label_colour']]))
332
333 # Draw a port inside the switch, using a specified colour scheme
334 # to denote its type. The switch outline must have been drawn
335 # first, for its origin to be set.
336 def draw_port(self, g, portnum, highlight):
337 if portnum < 1 or portnum > self.num_ports:
338 raise InputError('port number out of range')
339 if not self.left or not self.top:
340 raise InputError('cannot draw ports before switch is drawn')
341 if highlight not in g.port_pallette.iterkeys():
342 raise InputError('unknown highlight type \"%s\"' % highlight)
343
344 box_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_box']])
345 box_bg_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_bg']])
346 text_colour = g.im.colorExact(g.colour_defs[g.port_pallette[highlight]['port_label']])
347
348 if (portnum & 1): # odd port number, so top row
349 ulx = self.left + 3 + ((portnum-1) * self.port_width / 2)
350 uly = self.top + 3
351 else: # bottom row
352 ulx = self.left + 3 + ((portnum-2) * self.port_width / 2)
353 uly = self.top + 3 + self.port_height
354 lrx = ulx + self.port_width - 1
355 lry = uly + self.port_height - 1
356 g.im.rectangle((ulx,uly), (lrx,lry), box_colour, box_bg_colour)
357
358 # centre the text
359 (width, height) = g.get_label_size(repr(portnum), g.small_font_size)
360 llx = ulx + 3 + (self.text_width - width) / 2
361 lly = uly + max(height, self.text_height) + 1
362 g.im.string_ttf(g.font, g.small_font_size,
363 0.0, (llx, lly), repr(portnum), text_colour)
364
365 # Quick helper: draw all the ports for a switch in the default
366 # colour scheme.
367 def draw_default_ports(self, g):
368 for portnum in range(1, self.num_ports + 1):
369 self.draw_port(g, portnum, 'normal')
370
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000371 # Get the (x,y) co-ordinates of the UL and LR edges of the port
372 # box, and if it's upper row. This lets us so useful things such
373 # as draw a connection to that point for a trunk.
Steve McIntyre2454bf02015-09-23 18:33:02 +0100374 def get_port_location(self, portnum):
375 if portnum > self.num_ports:
376 raise InputError('port number out of range')
377
378 if (portnum & 1): # odd port number, so top row
379 ulx = self.left + 3 + ((portnum-1) * self.port_width / 2)
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000380 uly = self.top
381 lrx = ulx + self.port_width
382 lry = uly + self.port_height
383 return ((ulx,uly), (lrx,lry), True)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100384 else: # bottom row
385 ulx = self.left + 3 + ((portnum-2) * self.port_width / 2)
386 uly = self.top + 3 + self.port_height
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000387 lrx = ulx + self.port_width
Steve McIntyre2454bf02015-09-23 18:33:02 +0100388 lry = uly + self.port_height
Steve McIntyrec7d3f852015-10-26 16:37:26 +0000389 return ((ulx,uly), (lrx,lry), False)
Steve McIntyre2454bf02015-09-23 18:33:02 +0100390
391 # Debug: print some of the state of the switch object
392 def dump_state(self):
393 print 'port_width %d' % self.port_width
394 print 'port_height %d' % self.port_height
395 print 'text_width %d' % self.text_width
396 print 'text_height %d' % self.text_height
397 print 'label_left %d' % self.label_left
398 print 'label_bot %d' % self.label_bot
399 print 'total_width %d' % self.total_width
400 print 'total_height %d' % self.total_height
401
402# Test harness - generate a PNG using fake data
403if __name__ == '__main__':
404 gim = Graphics()
405 gim.set_font(['/usr/share/fonts/truetype/inconsolata/Inconsolata.otf',
406 '/usr/share/fonts/truetype/freefont/FreeMono.ttf'])
407 try:
408 gim.font
409 except NameError:
410 print 'no fonts found'
411 sys.exit(1)
412
413 switch = {}
414 size_x = {}
415 size_y = {}
416 switch[0] = Switch(gim, 48, 'lngswitch01')
417 switch[1] = Switch(gim, 24, 'lngswitch02')
418 switch[2] = Switch(gim, 52, 'lngswitch03')
419 label = "VLAN 4jj"
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100420
Steve McIntyre2454bf02015-09-23 18:33:02 +0100421 # Need to set gaps big enough for the number of trunks, at least.
422 num_trunks = 3
423 y_gap = max(20, 15 * num_trunks)
424 x_gap = max(20, 15 * num_trunks)
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100425
Steve McIntyre2454bf02015-09-23 18:33:02 +0100426 x = 0
427 y = y_gap
428
429 for i in range (0, 3):
430 (size_x[i], size_y[i]) = switch[i].get_dimensions()
431 x = max(x, size_x[i])
432 y += size_y[i] + y_gap
433
434 # Add space for the legend and the label
435 (legend_width, legend_height) = gim.get_legend_dimensions()
436 (label_width, label_height) = gim.get_label_size(label, gim.label_font_size)
437
438 x = max(x, legend_width + 2*x_gap + label_width)
439 x = x_gap + x + x_gap
Steve McIntyre9ff96bf2015-09-23 18:54:53 +0100440 y = y + max(legend_height + y_gap, label_height)
441
Steve McIntyre2454bf02015-09-23 18:33:02 +0100442 gim.create_canvas(x, y)
443
444 curr_y = y_gap
445 switch[0].draw_switch(gim, x_gap, curr_y)
446 switch[0].draw_default_ports(gim)
447 switch[0].draw_port(gim, 2, 'VLAN')
448 switch[0].draw_port(gim, 5, 'locked')
449 switch[0].draw_port(gim, 11, 'trunk')
450 switch[0].draw_port(gim, 44, 'trunk')
451 curr_y += size_y[0] + y_gap
452
453 switch[1].draw_switch(gim, x_gap, curr_y)
454 switch[1].draw_default_ports(gim)
455 switch[1].draw_port(gim, 5, 'VLAN')
456 switch[1].draw_port(gim, 8, 'locked')
457 switch[1].draw_port(gim, 13, 'trunk')
458 switch[1].draw_port(gim, 16, 'trunk')
459 curr_y += size_y[2] + y_gap
460
461 switch[2].draw_switch(gim, x_gap, curr_y)
462 switch[2].draw_default_ports(gim)
463 switch[2].draw_port(gim, 1, 'trunk')
464 switch[2].draw_port(gim, 2, 'locked')
465 switch[2].draw_port(gim, 14, 'trunk')
466 switch[2].draw_port(gim, 19, 'VLAN')
467 curr_y += size_y[2] + y_gap
468
469 # Now let's try and draw some trunks!
470 gim.draw_trunk(0,
471 switch[0].get_port_location(11),
472 switch[1].get_port_location(16),
473 gim.port_pallette['trunk']['trace'])
474 gim.draw_trunk(1,
475 switch[1].get_port_location(13),
476 switch[2].get_port_location(1),
477 gim.port_pallette['trunk']['trace'])
478 gim.draw_trunk(2,
479 switch[0].get_port_location(44),
480 switch[2].get_port_location(14),
481 gim.port_pallette['trunk']['trace'])
482
483 gim.draw_legend(x_gap, curr_y)
484 gim.draw_label(x - label_width - 2*x_gap, curr_y, label, int(x_gap / 2))
485
486 f=open('xx.png','w')
487 gim.im.writePng(f)
488 f.close()
489 print 'Test graphic written to xx.png'