# 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