Source code for svg_model.connections

# coding: utf-8
# Copyright 2015
# Jerry Zhou <jerryzhou@hotmail.ca> and Christian Fobel <christian@fobel.net>
import re
import warnings

import pandas as pd
import numpy as np

from . import INKSCAPE_NSMAP
from .draw import draw_lines_svg_layer as _draw_lines_svg_layer


[docs]def extend_shapes(df_shapes, axis, distance): ''' Extend shape/polygon outline away from polygon center point by absolute distance. ''' df_shapes_i = df_shapes.copy() offsets = df_shapes_i[axis + '_center_offset'].copy() offsets[offsets < 0] -= distance offsets[offsets >= 0] += distance df_shapes_i[axis] = df_shapes_i[axis + '_center'] + offsets return df_shapes_i
[docs]def extract_adjacent_shapes(df_shapes, shape_i_column, extend=.5): ''' Generate list of connections between "adjacent" polygon shapes based on geometrical "closeness". Parameters ---------- df_shapes : pandas.DataFrame Table of polygon shape vertices (one row per vertex). Table rows with the same value in the :data:`shape_i_column` column are grouped together as a polygon. shape_i_column : str or list[str] Column name(s) that identify the polygon each row belongs to. extend : float, optional Extend ``x``/``y`` coords by the specified number of absolute units from the center point of each polygon. Each polygon is stretched independently in the ``x`` and ``y`` direction. In each direction, a polygon considered adjacent to all polygons that are overlapped by the extended shape. Returns ------- pandas.DataFrame Adjacency list as a frame containing the columns ``source`` and ``target``. The ``source`` and ``target`` of each adjacency connection is ordered such that the ``source`` is less than the ``target``. ''' # Find corners of each solid shape outline. # Extend x coords by abs units df_scaled_x = extend_shapes(df_shapes, 'x', extend) # Extend y coords by abs units df_scaled_y = extend_shapes(df_shapes, 'y', extend) df_corners = df_shapes.groupby(shape_i_column).agg({'x': ['min', 'max'], 'y': ['min', 'max']}) # Find adjacent electrodes row_list = [] for shapeNumber in df_shapes[shape_i_column].drop_duplicates(): df_stretched = df_scaled_x[df_scaled_x[shape_i_column] .isin([shapeNumber])] xmin_x, xmax_x, ymin_x, ymax_x = (df_stretched.x.min(), df_stretched.x.max(), df_stretched.y.min(), df_stretched.y.max()) df_stretched = df_scaled_y[df_scaled_y[shape_i_column] .isin([shapeNumber])] xmin_y, xmax_y, ymin_y, ymax_y = (df_stretched.x.min(), df_stretched.x.max(), df_stretched.y.min(), df_stretched.y.max()) #Some conditions unnecessary if it is assumed that electrodes don't overlap adjacent = df_corners[ ((df_corners.x['min'] < xmax_x) & (df_corners.x['max'] >= xmax_x) # Check in x stretched direction |(df_corners.x['min'] < xmin_x) & (df_corners.x['max'] >= xmin_x)) # Check if y is within bounds & (df_corners.y['min'] < ymax_x) & (df_corners.y['max'] > ymin_x) | #maybe do ymax_x - df_corners.y['min'] > threshold & # df_corners.y['max'] - ymin_x > threshold ((df_corners.y['min'] < ymax_y) & (df_corners.y['max'] >= ymax_y) # Checks in y stretched direction |(df_corners.y['min'] < ymin_y) & (df_corners.y['max'] >= ymin_y)) # Check if x in within bounds & ((df_corners.x['min'] < xmax_y) & (df_corners.x['max'] > xmin_y)) ].index.values for shape in adjacent: temp_dict = {} reverse_dict = {} temp_dict ['source'] = shapeNumber reverse_dict['source'] = shape temp_dict ['target'] = shape reverse_dict['target'] = shapeNumber if(reverse_dict not in row_list): row_list.append(temp_dict) df_connected = (pd.DataFrame(row_list)[['source', 'target']] .sort_index(axis=1, ascending=True) .sort_values(['source', 'target'])) return df_connected
[docs]def get_adjacency_matrix(df_connected): ''' Return matrix where $a_{i,j} = 1$ indicates polygon $i$ is connected to polygon $j$. Also, return mapping (and reverse mapping) from original keys in `df_connected` to zero-based integer index used for matrix rows and columns. ''' sorted_path_keys = np.sort(np.unique(df_connected[['source', 'target']] .values.ravel())) indexed_paths = pd.Series(sorted_path_keys) path_indexes = pd.Series(indexed_paths.index, index=sorted_path_keys) adjacency_matrix = np.zeros((path_indexes.shape[0], ) * 2, dtype=int) for i_key, j_key in df_connected[['source', 'target']].values: i, j = path_indexes.loc[[i_key, j_key]] adjacency_matrix[i, j] = 1 adjacency_matrix[j, i] = 1 return adjacency_matrix, indexed_paths, path_indexes
[docs]def extract_connections(svg_source, shapes_canvas, line_layer='Connections', line_xpath=None, path_xpath=None, namespaces=None): ''' Load all ``<svg:line>`` elements and ``<svg:path>`` elements from a layer of an SVG source. For each element, if endpoints overlap distinct shapes in :data:`shapes_canvas`, add connection between overlapped shapes. .. versionchanged:: 0.6.post1 Allow both ``<svg:line>`` *and* ``<svg:path>`` instances to denote connected/adjacent shapes. .. versionadded:: 0.6.post1 :data:`path_xpath` Parameters ---------- svg_source : filepath Input SVG file containing connection lines. shapes_canvas : shapes_canvas.ShapesCanvas Shapes canvas containing shapes to compare against connection endpoints. line_layer : str Name of layer in SVG containing connection lines. line_xpath : str XPath string to iterate through connection lines. path_xpath : str XPath string to iterate through connection paths. namespaces : dict SVG namespaces (compatible with :func:`etree.parse`). Returns ------- pandas.DataFrame Each row corresponds to connection between two shapes in :data:`shapes_canvas`, denoted ``source`` and ``target``. ''' from lxml import etree if namespaces is None: # Inkscape namespace is required to match SVG elements as well as # Inkscape-specific SVG tags and attributes (e.g., `inkscape:label`). namespaces = INKSCAPE_NSMAP # Parse SVG source. e_root = etree.parse(svg_source) # List to hold records of form: `[<id>, <x1>, <y1>, <x2>, <y2>]`. frames = [] if line_xpath is None: # Define query to look for `svg:line` elements in top level of layer of # SVG specified to contain connections. line_xpath = ("//svg:g[@inkscape:label='%s']/svg:line" % line_layer) coords_columns = ['x1', 'y1', 'x2', 'y2'] for line_i in e_root.xpath(line_xpath, namespaces=namespaces): # Extract start and end coordinate from `svg:line` element. line_i_dict = dict(line_i.items()) values = ([line_i_dict.get('id', None)] + [float(line_i_dict[k]) for k in coords_columns]) # Append record for end points of current line. frames.append(values) # Regular expression pattern to match start and end coordinates of # connection `svg:path` element. cre_path_ends = re.compile(r'^\s*M\s*(?P<start_x>\d+(\.\d+)?),\s*' r'(?P<start_y>\d+(\.\d+)?).*' # Diagonal line... r'((L\s*(?P<end_x>\d+(\.\d+)?),\s*' r'(?P<end_y>\d+(\.\d+)?))|' # or Vertical line... r'(V\s*(?P<end_vy>\d+(\.\d+)?))|' # or Horizontal line r'(H\s*(?P<end_hx>\d+(\.\d+)?))' r')\D*' r'$') if path_xpath is None: # Define query to look for `svg:path` elements in top level of layer of # SVG specified to contain connections. path_xpath = ("//svg:g[@inkscape:label='%s']/svg:path" % line_layer) for path_i in e_root.xpath(path_xpath, namespaces=namespaces): path_i_dict = dict(path_i.items()) match_i = cre_path_ends.match(path_i_dict['d']) if match_i: # Connection `svg:path` matched required format. Extract start and # end coordinates. match_dict_i = match_i.groupdict() if match_dict_i['end_vy']: # Path ended with vertical line match_dict_i['end_x'] = match_dict_i['start_x'] match_dict_i['end_y'] = match_dict_i['end_vy'] if match_dict_i['end_hx']: # Path ended with horizontal line match_dict_i['end_x'] = match_dict_i['end_hx'] match_dict_i['end_y'] = match_dict_i['start_y'] # Append record for end points of current path. frames.append([path_i_dict['id']] + map(float, (match_dict_i['start_x'], match_dict_i['start_y'], match_dict_i['end_x'], match_dict_i['end_y']))) if not frames: return pd.DataFrame(None, columns=['source', 'target']) df_connection_lines = pd.DataFrame(frames, columns=['id'] + coords_columns) # Use `shapes_canvas.find_shape` to determine shapes overlapped by end # points of each `svg:path` or `svg:line`. df_shape_connections_i = pd.DataFrame([[shapes_canvas.find_shape(x1, y1), shapes_canvas.find_shape(x2, y2)] for i, (x1, y1, x2, y2) in df_connection_lines[coords_columns] .iterrows()], columns=['source', 'target']) # Order the source and target of each row so the source shape identifier is # always the lowest. df_shape_connections_i.sort_index(axis=1, inplace=True) # Tag each shape connection with the corresponding `svg:line`/`svg:path` # identifier. May be useful, e.g., in debugging. df_shape_connections_i['line_id'] = df_connection_lines['id'] # Remove connections where source or target shape was not matched (e.g., if # one or more end points does not overlap with a shape). return df_shape_connections_i.dropna()
[docs]def draw_lines_svg_layer(df_endpoints, layer_name='Connections'): warnings.warn('`draw_lines_svg_layer` has been moved to `svg_model.draw`') return _draw_lines_svg_layer(df_endpoints, layer_name=layer_name)