You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

442 lines
18 KiB

# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved.
class LayoutConverter(object):
def __init__(self):
self.gt = None
# an object containing min and max x,y coords of the original canvas
self.boundaries = None
# margins for the output canvas
self.MARGIN_LEFT = 10
self.MARGIN_TOP = 10
self.MARGIN_RIGHT = 10
self.MARGIN_BOTTOM = 10
# max width to downscale existing canvas to
# and at the same time min canvas width
self.DEFAULT_CANVAS_WIDTH = 1920
self.DEFAULT_CANVAS_HEIGHT = 1080
self.MIN_WIDGET_SIZES = {
'Sparkline': {
'w': 84,
'h': 35
},
'SingleValue': {
'w': 35,
'h': 35
},
'SingleValueDelta': {
'w': 35,
'h': 35
},
'PolyIcon': {
'w': 35,
'h': 35
}
}
# used to scale down all items in the GT to wanted max resolution
self.scale_factor = 0.0
self.DEFAULT_CANVAS_BG_COLOR = '#FFFFFF'
def convert(self, gt):
"""
Calculates canvas size by going through each item's position and size
:param gt: original GT
:return: layout options for updating UDF's layout section
"""
self.gt = gt
canvas_color = self.gt.get('bgColor')
content = self.gt.get('content')
if not len(content):
canvas_width = self.DEFAULT_CANVAS_WIDTH
canvas_height = self.DEFAULT_CANVAS_HEIGHT
else:
self.boundaries = self.calculate_canvas_boundaries()
canvas_width = (self.boundaries['max_x'] - self.boundaries['min_x']) + self.MARGIN_LEFT + self.MARGIN_RIGHT
canvas_height = (self.boundaries['max_y'] - self.boundaries['min_y']) + self.MARGIN_TOP + self.MARGIN_BOTTOM
if canvas_width > self.DEFAULT_CANVAS_WIDTH:
default_scale_factor = (self.DEFAULT_CANVAS_WIDTH * 1.0) / canvas_width
if default_scale_factor > self.scale_factor:
# at this point self.scale_factor has the smallest value we can use without losing visibility
# and the default canvas size is bigger than the smallest we calculated
self.scale_factor = default_scale_factor
canvas_width = self.DEFAULT_CANVAS_WIDTH
canvas_height = self.DEFAULT_CANVAS_HEIGHT
else:
# we can't fit into default canvas - adjust canvas size
canvas_width = int(canvas_width * self.scale_factor)
canvas_height = int(canvas_height * self.scale_factor)
else:
if self.scale_factor <= 1.0:
# we have no widgets that are too small and canvas size is enough - don't change scale
# otherwise widget sizes will adjust to scale_factor, but not the canvas size
self.scale_factor = 1.0
canvas_width = self.DEFAULT_CANVAS_WIDTH
canvas_height = self.DEFAULT_CANVAS_HEIGHT
return {
'width': canvas_width,
'height': canvas_height,
'backgroundColor': canvas_color if canvas_color else self.DEFAULT_CANVAS_BG_COLOR
}
def get_scale_factor(self):
"""
Returns the calculated scale factor which is a value between 0 and 1.
:return: Integer scale factor
"""
return self.scale_factor
def calculate_canvas_boundaries(self):
"""
Goes through each element on the canvas and finds min and max x,y coords
:return: Object with min and max x,y coords
"""
min_x = None
min_y = None
max_x = 0
max_y = 0
content = self.gt.get('content')
for item in content:
layout = self.get_item_layout(item)
max_x = max_x if layout['x'] + layout['w'] < max_x else layout['x'] + layout['w']
max_y = max_y if layout['y'] + layout['h'] < max_y else layout['y'] + layout['h']
min_x = min_x if (min_x is not None and layout['x'] > min_x) else layout['x']
min_y = min_y if (min_y is not None and layout['y'] > min_y) else layout['y']
self.adjust_scale_to_min_render_size(item.get('name'), layout)
return {
'max_x': max_x,
'max_y': max_y,
'min_x': min_x,
'min_y': min_y
}
def get_item_layout(self, item):
"""
Converts original size/coords to UDF format
:param item: original widget
:return: object with UDF item layout options
"""
height = item.get('height')
width = item.get('width')
name = item.get('name')
return {
'x': int(round(float(item.get('x')))) if item.get('x') else 0,
'y': int(round(float(item.get('y')))) if item.get('y') else 0,
'h': int(round(float(height))) if height and height != 0
else (self.MIN_WIDGET_SIZES[name]['h']
if name in self.MIN_WIDGET_SIZES.keys() else 100),
'w': int(round(float(width))) if width and width != 0
else (self.MIN_WIDGET_SIZES[name]['w']
if name in self.MIN_WIDGET_SIZES.keys() else 100)
}
def adjust_scale_to_min_render_size(self, widget_type, layout):
"""
Look for the max scale factor we can afford (how much we can reduce the size) without breaking
visibility of each widget.
Updates self.scale_factor
:param widget_type: classic widget type
:param layout: widget layout object with x,y,w,h params
"""
if widget_type in list(self.MIN_WIDGET_SIZES.keys()):
for dimension in ['w', 'h']:
temp_scale = float(self.MIN_WIDGET_SIZES[widget_type][dimension]) / float(layout[dimension])
if temp_scale > self.scale_factor:
self.scale_factor = temp_scale
def get_connection_position(self, item):
"""
Connection widget has a special layout format
:param item: connection widget item config
:return: UDF layout item
"""
# item ports/directions for connection
item_ports = {
'east': 'e',
'west': 'w',
'south': 's',
'north': 'n'
}
source_port = item_ports.get('east')
target_port = item_ports.get('west')
source_id = item.get('sourceId')
target_id = item.get('targetId')
if self.gt and self.gt.get('content', None):
content = self.gt.get('content')
source_item = [item for item in content if item.get('id') == source_id]
target_item = [item for item in content if item.get('id') == target_id]
if source_item and target_item:
item_vertices = item.get('vertices')
if item_vertices:
item_start_vertex = item_vertices[0]
item_end_vertex = item_vertices[-1]
source_item_x_pos = int(round(float(source_item[0].get('x', 0))))
source_item_y_pos = int(round(float(source_item[0].get('y', 0))))
source_item_height = int(round(float(source_item[0].get('height', 0))))
source_item_width = int(round(float(source_item[0].get('width', 0))))
target_item_x_pos = int(round(float(target_item[0].get('x', 0))))
target_item_y_pos = int(round(float(target_item[0].get('y', 0))))
target_item_height = int(round(float(target_item[0].get('height', 0))))
target_item_width = int(round(float(target_item[0].get('width', 0))))
connection_start_x = item_start_vertex.get('x', 0)
connection_end_x = item_end_vertex.get('x', 0)
connection_start_y = item_start_vertex.get('y', 0)
connection_end_y = item_end_vertex.get('y', 0)
east_and_west_diff = abs(connection_start_x - connection_end_x)
south_and_north_diff = abs(connection_start_y - connection_end_y)
# for connection pointing east to west or west to east
if east_and_west_diff > south_and_north_diff:
# for east to west connection
if connection_start_x < connection_end_x:
source_item_end_x_pos = source_item_x_pos + source_item_width
target_item_start_x_pos = target_item_x_pos
if connection_start_x == source_item_end_x_pos:
source_port = item_ports.get('east')
if connection_end_x == target_item_start_x_pos:
target_port = item_ports.get('west')
# for west to east connection
else:
source_item_start_x_pos = source_item_x_pos
target_item_end_x_pos = target_item_x_pos + target_item_width
if connection_start_x == source_item_start_x_pos:
source_port = item_ports.get('west')
if connection_end_x == target_item_end_x_pos:
target_port = item_ports.get('east')
# for connection pointing south to north or north to south
else:
# defaults for this case
source_port = item_ports.get('south')
target_port = item_ports.get('north')
# for north to south connection
if connection_start_y < connection_end_y:
source_item_end_y_pos = source_item_y_pos + source_item_height
target_item_start_y_pos = target_item_y_pos
if connection_start_y == source_item_end_y_pos:
source_port = item_ports.get('south')
if connection_end_y == target_item_start_y_pos:
target_port = item_ports.get('north')
# for south to north connection
else:
source_item_start_y_pos = source_item_y_pos
target_item_end_y_pos = target_item_y_pos + target_item_height
if connection_start_y == source_item_start_y_pos:
source_port = item_ports.get('north')
if connection_end_y == target_item_end_y_pos:
target_port = item_ports.get('south')
# adjust viz id format for UDF standards
source_id = 'viz_%s' % source_id
target_id = 'viz_%s' % target_id
return {
'from': {
'item': source_id,
'port': source_port
},
'to': {
'item': target_id,
'port': target_port
}
}
def get_line_position(self, item):
"""
Line widget has a special layout format
:param item: line widget item config
:return: UDF layout item
"""
return {
'from': {
'x': int(item.get('startX')),
'y': int(item.get('startY'))
},
'to': {
'x': int(item.get('endX')),
'y': int(item.get('endY'))
}
}
def adjust_line_position(self, position):
"""
Same as adjust_item_position, but for Line widget
:param position: item's original position
:return: new UDF position object
"""
new_position = position
new_position['from']['x'] = (position['from']['x'] - self.boundaries['min_x']) + self.MARGIN_LEFT
new_position['to']['x'] = (position['to']['x'] - self.boundaries['min_x']) + self.MARGIN_LEFT
new_position['from']['y'] = (position['from']['y'] - self.boundaries['min_y']) + self.MARGIN_TOP
new_position['to']['y'] = (position['to']['y'] - self.boundaries['min_y']) + self.MARGIN_TOP
new_position['from']['x'] = int(new_position['from']['x'] * self.scale_factor)
new_position['to']['x'] = int(new_position['to']['x'] * self.scale_factor)
new_position['from']['y'] = int(new_position['from']['y'] * self.scale_factor)
new_position['to']['y'] = int(new_position['to']['y'] * self.scale_factor)
return new_position
def adjust_item_position(self, position):
"""
Modify coordinates of an original element according to found boundaries so that the canvas begins
at the left corner of the screen. Original GT depended on SVG coords that could be negative.
Also apply margins.
:param position: item's original position
:return: new UDF position object
"""
new_position = position
new_position['x'] = (position['x'] - self.boundaries['min_x']) + self.MARGIN_LEFT
new_position['y'] = (position['y'] - self.boundaries['min_y']) + self.MARGIN_TOP
new_position['x'] = int(new_position['x'] * self.scale_factor)
new_position['y'] = int(new_position['y'] * self.scale_factor)
new_position['w'] = int(new_position['w'] * self.scale_factor)
new_position['h'] = int(new_position['h'] * self.scale_factor)
return new_position
def adjust_text_position(self, position):
"""
UDF text widgets have a 12px vertical offset that we need to get rid of
"""
new_position = position
new_position['y'] -= 12
return new_position
def convert_item(self, item):
"""
Produces a layout item object for UDF json
:param item: original widget
:return: converted layout item object
"""
item_id = 'viz_%s' % str(item.get('id'))
if item.get('name') == 'Connection':
position = self.get_connection_position(item)
elif item.get('name') == 'Line':
position = self.get_line_position(item)
position = self.adjust_line_position(position)
else:
position = self.get_item_layout(item)
position = self.adjust_item_position(position)
if item.get('name') == 'Text':
position = self.adjust_text_position(position)
layout_item = {
'item': item_id,
'position': position
}
layout_item['type'] = 'line' if item.get('name') in ['Line', 'Connection'] else 'block'
return layout_item
def find_label_position(self, location, position):
"""
Finds position for label relative to the item
:param location: label's location relative to item (top/bottom/left/right)
:param position: item's position
:return: UDF position object
"""
label_width = position['w']
label_height = position['h']
if location == 'top':
return {
'x': position['x'],
'y': position['y'] - label_height,
'w': position['w'],
'h': label_height
}
elif location == 'bottom':
return {
'x': position['x'],
'y': position['y'] + position['h'],
'w': position['w'],
'h': label_height
}
elif location == 'left':
return {
'x': position['x'] - label_width,
'y': position['y'],
'w': label_width,
'h': position['h']
}
elif location == 'right':
return {
'x': position['x'] + position['w'],
'y': position['y'],
'w': label_width,
'h': position['h']
}
else:
return None
def find_label_position_line(self, label_location, position, item):
"""
Finds position for label relative to the item
:param location: label's location relative to item (top/bottom/left/right)
:param position: item's position
:param item: original widget
:return: UDF position object
"""
label_width = int(round(float(item.get('width', 80))) * self.scale_factor)
label_height = int(round(float(item.get('height', 80))) * self.scale_factor)
label_x = int(round(float(((position['from']['x'] + position['to']['x']) / 2))))
label_y = int(round(float(((position['from']['y'] + position['to']['y']) / 2))))
if label_location == 'top':
return {
'x': label_x,
'y': label_y - (label_height / 2),
'w': label_width,
'h': label_height
}
elif label_location == 'bottom':
return {
'x': label_x,
'y': label_y + (label_height / 2),
'w': label_width,
'h': label_height
}
elif label_location == 'left':
return {
'x': label_x - (label_width / 2),
'y': label_y,
'w': label_width,
'h': label_height
}
elif label_location == 'right':
return {
'x': label_x + (label_width / 2),
'y': label_y,
'w': label_width,
'h': label_height
}
else:
return None