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