Skip to content

Commit

Permalink
Merge pull request #223 from iriusrisk/feature/OPT-780
Browse files Browse the repository at this point in the history
[feature/OPT-780] to dev
  • Loading branch information
PacoCid committed May 9, 2023
2 parents 6566140 + bbeaed2 commit 0dfa56d
Show file tree
Hide file tree
Showing 31 changed files with 888 additions and 183 deletions.
2 changes: 2 additions & 0 deletions slp_visio/slp_visio/load/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .strategies.connector.impl import connector_identifier_by_connects, create_connector_by_connects, \
create_connector_by_line_coordinates
14 changes: 14 additions & 0 deletions slp_visio/slp_visio/load/connector_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from vsdx import Shape

from slp_visio.slp_visio.load.strategies.connector.connector_identifier_strategy import ConnectorIdentifierStrategy


class ConnectorIdentifier:

@staticmethod
def is_connector(shape: Shape) -> bool:
for strategy in ConnectorIdentifierStrategy.get_strategies():
if strategy.is_connector(shape):
return True

return False
50 changes: 7 additions & 43 deletions slp_visio/slp_visio/load/objects/visio_diagram_factories.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,10 @@
from typing import Optional

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramComponent, DiagramConnector
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy
from slp_visio.slp_visio.util.visio import get_shape_text, get_master_shape_text, normalize_label, get_unique_id_text


# if it has two shapes connected and is not pointing itself
def is_valid_connector(connected_shapes) -> bool:
if len(connected_shapes) < 2:
return False
if connected_shapes[0].shape_id == connected_shapes[1].shape_id:
return False
return True


# if its master name includes 'Double Arrow' or has arrows defined in both ends
def is_bidirectional_connector(shape) -> bool:
if shape.master_page.name is not None and 'Double Arrow' in shape.master_page.name:
return True
for arrow_value in [shape.cell_value(att) for att in ['BeginArrow', 'EndArrow']]:
if arrow_value is None or not str(arrow_value).isnumeric() or arrow_value == '0':
return False
return True


def is_created_from(connector) -> bool:
return connector.from_rel == 'BeginX'


def connector_has_arrow_in_origin(shape) -> bool:
begin_arrow_value = shape.cell_value('BeginArrow')
return begin_arrow_value is not None and str(begin_arrow_value).isnumeric() and begin_arrow_value != '0'


class VisioComponentFactory:

def create_component(self, shape, origin, representer) -> DiagramComponent:
Expand All @@ -46,18 +19,9 @@ def create_component(self, shape, origin, representer) -> DiagramComponent:

class VisioConnectorFactory:

def create_connector(self, shape) -> Optional[DiagramConnector]:
connected_shapes = shape.connects
if not is_valid_connector(connected_shapes):
return None

if is_bidirectional_connector(shape):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id, True)

has_arrow_in_origin = connector_has_arrow_in_origin(shape)

if (not has_arrow_in_origin and is_created_from(connected_shapes[0])) \
or (has_arrow_in_origin and is_created_from(connected_shapes[1])):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id)
else:
return DiagramConnector(shape.ID, connected_shapes[1].shape_id, connected_shapes[0].shape_id)
@staticmethod
def create_connector(shape) -> Optional[DiagramConnector]:
for strategy in CreateConnectorStrategy.get_strategies():
connector = strategy.create_connector(shape)
if connector:
return connector
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import abc

from vsdx import Shape

from slp_visio.slp_visio.load.strategies.strategy import Strategy


class ConnectorIdentifierStrategy(Strategy):
"""
Formal Interface to check if a shape is a connector
"""

@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'is_connector') and callable(subclass.process)
or NotImplemented)

@abc.abstractmethod
def is_connector(self, shape: Shape) -> bool:
"""return True if the Shape is a connector"""
raise NotImplementedError
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import abc
from typing import Optional

from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.strategies.strategy import Strategy


class CreateConnectorStrategy(Strategy):
"""
Formal Interface to create an OTM Dataflow from a vsdx shape
"""

@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'create_connector') and callable(subclass.process)
or NotImplemented)

@abc.abstractmethod
def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
"""creates the OTM Dataflow from the vsdx shape"""
raise NotImplementedError
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from vsdx import Shape

from slp_visio.slp_visio.load.strategies.connector.connector_identifier_strategy import ConnectorIdentifierStrategy


class ConnectorIdentifierByConnects(ConnectorIdentifierStrategy):
"""
Strategy to know if a shape is a connector
The shape must have connects and each connector_shape_id must match with the shape id
"""

def is_connector(self, shape: Shape) -> bool:
for connect in shape.connects:
if shape.ID == connect.connector_shape_id:
return True
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Optional

from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy


class CreateConnectorByConnects(CreateConnectorStrategy):
"""
Strategy to create a connector from the shape connects
"""

def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
connected_shapes = shape.connects
if not self.are_two_different_shapes(connected_shapes):
return None

if self.is_bidirectional_connector(shape):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id, True)

has_arrow_in_origin = self.connector_has_arrow_in_origin(shape)

if (not has_arrow_in_origin and self.is_created_from(connected_shapes[0])) \
or (has_arrow_in_origin and self.is_created_from(connected_shapes[1])):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id)
else:
return DiagramConnector(shape.ID, connected_shapes[1].shape_id, connected_shapes[0].shape_id)

# if it has two shapes connected and is not pointing itself
@staticmethod
def are_two_different_shapes(connected_shapes) -> bool:
if len(connected_shapes) < 2:
return False
if connected_shapes[0].shape_id == connected_shapes[1].shape_id:
return False
return True

# if its master name includes 'Double Arrow' or has arrows defined in both ends
@staticmethod
def is_bidirectional_connector(shape) -> bool:
if shape.master_page.name is not None and 'Double Arrow' in shape.master_page.name:
return True
for arrow_value in [shape.cell_value(att) for att in ['BeginArrow', 'EndArrow']]:
if arrow_value is None or not str(arrow_value).isnumeric() or arrow_value == '0':
return False
return True

@staticmethod
def connector_has_arrow_in_origin(shape) -> bool:
begin_arrow_value = shape.cell_value('BeginArrow')
return begin_arrow_value is not None and str(begin_arrow_value).isnumeric() and begin_arrow_value != '0'

@staticmethod
def is_created_from(connector) -> bool:
return connector.from_rel == 'BeginX'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Optional

from shapely.geometry import Point
from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.representation.simple_component_representer import SimpleComponentRepresenter
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy


class CreateConnectorByLineCoordinates(CreateConnectorStrategy):
"""
Strategy to create a connector from the shape begin and end coordinates
We search a component shape that touch the beginning line coordinates of the given shape connector
We search a component shape that touch the ending line coordinates of the given shape connector
If we find both shapes at the end and at the beginning of the line, we create the connector
"""

def __init__(self):
self.tolerance = 0.09
self.representer: SimpleComponentRepresenter() = SimpleComponentRepresenter()

def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
if not shape.begin_x or not shape.begin_y or not shape.end_x or not shape.end_y:
return None
begin_line = Point(shape.begin_x, shape.begin_y)
end_line = Point(shape.end_x, shape.end_y)
if not begin_line or not end_line:
return None

if not components:
return None
origin = self.__match_component(begin_line, components)
target = self.__match_component(end_line, components)

if not origin or not target:
return None

return DiagramConnector(shape.ID, origin, target, name=shape.text)

def __match_component(self, point, components):
for component in components:
polygon = self.representer.build_representation(component)
distance = polygon.exterior.distance(point)
if round(distance, 2) <= self.tolerance:
return component.ID
16 changes: 16 additions & 0 deletions slp_visio/slp_visio/load/strategies/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import abc


class Strategy(metaclass=abc.ABCMeta):
"""
Formal Interface to build a strategy
"""

@classmethod
def get_strategies(cls):
return [obj() for obj in cls.__get_subclasses()]

@classmethod
def __get_subclasses(cls):
for subclass in cls.__subclasses__():
yield subclass
13 changes: 3 additions & 10 deletions slp_visio/slp_visio/load/vsdx_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from vsdx import Shape, VisioFile

from slp_base import DiagramType
from slp_visio.slp_visio.load.connector_identifier import ConnectorIdentifier
from slp_visio.slp_visio.load.objects.diagram_objects import Diagram, DiagramComponentOrigin, DiagramLimits
from slp_visio.slp_visio.load.parent_calculator import ParentCalculator
from slp_visio.slp_visio.load.representation.simple_component_representer import SimpleComponentRepresenter
Expand Down Expand Up @@ -41,20 +42,12 @@ def parse(self, visio_diagram_filename) -> Diagram:

return Diagram(DiagramType.VISIO, self._visio_components, self._visio_connectors, diagram_limits)

@staticmethod
def _is_connector(shape: Shape) -> bool:
for connect in shape.connects:
if shape.ID == connect.connector_shape_id:
return True

return False

@staticmethod
def _is_boundary(shape: Shape) -> bool:
return shape.shape_name is not None and 'Curved panel' in shape.shape_name

def _is_component(self, shape: Shape) -> bool:
return get_shape_text(shape) and not self._is_connector(shape)
return get_shape_text(shape) and not ConnectorIdentifier.is_connector(shape)

def __calculate_diagram_limits(self) -> DiagramLimits:
floor_coordinates = [None, None]
Expand All @@ -79,7 +72,7 @@ def __calculate_diagram_limits(self) -> DiagramLimits:

def _load_page_elements(self):
for shape in self.page.child_shapes:
if self._is_connector(shape):
if ConnectorIdentifier.is_connector(shape):
self._add_connector(shape)
elif self._is_boundary(shape):
self._add_boundary_component(shape)
Expand Down
1 change: 1 addition & 0 deletions slp_visio/slp_visio/lucid/load/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .strategies.connector.impl import connector_identifier_by_lucid_line_name
13 changes: 0 additions & 13 deletions slp_visio/slp_visio/lucid/load/lucid_vsdx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,9 @@
from slp_visio.slp_visio.load.parent_calculator import ParentCalculator
from slp_visio.slp_visio.load.vsdx_parser import VsdxParser

LUCID_LINE = 'com.lucidchart.Line'


class LucidVsdxParser(VsdxParser):

@staticmethod
def _is_connector(shape: Shape) -> bool:
for connect in shape.connects:
if shape.ID == connect.connector_shape_id:
return True

if shape.shape_name and shape.shape_name.startswith(f'{LUCID_LINE}'):
return True

return False

def _add_connector(self, connector_shape: Shape):
shape_components = [c for c in self.page.child_shapes if self._is_component(c) and not self._is_boundary(c)]

Expand Down
35 changes: 7 additions & 28 deletions slp_visio/slp_visio/lucid/load/objects/lucid_diagram_factories.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from typing import Optional

from shapely.geometry import Point
from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramComponent, DiagramConnector
from slp_visio.slp_visio.load.representation.simple_component_representer import SimpleComponentRepresenter
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy
from slp_visio.slp_visio.util.visio import get_shape_text, get_master_shape_text

LUCID_COMPONENT_PREFIX = 'com.lucidchart'
Expand Down Expand Up @@ -39,29 +38,9 @@ def create_component(shape, origin, representer) -> DiagramComponent:

class LucidConnectorFactory:

def __init__(self):
self.tolerance = 0.09
self.representer: SimpleComponentRepresenter() = SimpleComponentRepresenter()

def create_connector(self, shape: Shape, components: [Shape]) -> Optional[DiagramConnector]:

begin_line = Point(shape.begin_x, shape.begin_y)
end_line = Point(shape.end_x, shape.end_y)
if not begin_line or not end_line:
return None

origin = self.__match_component(begin_line, components)
target = self.__match_component(end_line, components)

if not origin or not target:
return None

return DiagramConnector(shape.ID, origin, target, name=shape.text)

def __match_component(self, point, components):

for component in components:
polygon = self.representer.build_representation(component)
distance = polygon.exterior.distance(point)
if distance <= self.tolerance:
return component.ID
@staticmethod
def create_connector(shape: Shape, components: [Shape]) -> Optional[DiagramConnector]:
for strategy in CreateConnectorStrategy.get_strategies():
connector = strategy.create_connector(shape, components=components)
if connector:
return connector
Empty file.
Empty file.
Empty file.
Loading

0 comments on commit 0dfa56d

Please sign in to comment.