# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2012  CEA/DEN, EDF R&D, OPEN CASCADE
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
#
"""
This module defines the different structural element parts. It is used to
build the geometric shapes of the structural elements. It should not be used
directly in the general case. Structural elements should be created by the
class :class:`~salome.geom.structelem.StructuralElementManager`.
"""

import math

import salome
import SALOMEDS

from salome.kernel.logger import Logger
from salome.kernel import termcolor
logger = Logger("salome.geom.structelem.parts", color = termcolor.RED)
from salome.geom.geomtools import getGeompy

import orientation

# Filling for the beams
FULL = "FULL"
HOLLOW = "HOLLOW"

# Minimum dimension for the shapes to extrude
MIN_DIM_FOR_EXTRUDED_SHAPE = 2e-4
MIN_LENGTH_FOR_EXTRUSION = 1e-4
MIN_THICKNESS = 1e-5

# Colors for the structural elements
GREEN = SALOMEDS.Color(0.0, 1.0, 0.0)
LIGHT_GREEN = SALOMEDS.Color(0.0, 1.0, 170.0/255.0)
BLUE = SALOMEDS.Color(0.0, 0.0, 1.0)
LIGHT_BLUE = SALOMEDS.Color(0.0, 0.5, 1.0)
RED = SALOMEDS.Color(1.0, 0.0, 0.0)
LIGHT_RED = SALOMEDS.Color(1.0, 0.5, 0.5)
PURPLE = SALOMEDS.Color(170.0/255.0, 85.0/255.0, 1.0)
ORANGE = SALOMEDS.Color(1.0, 170.0/255.0, 0.0)


class InvalidParameterError(Exception):
    """
    This exception is raised when an invalid parameter is used to build a
    structural element part.
    """
    
    def __init__(self, groupName, expression, minValue, value):
        self.groupName = groupName
        self.expression = expression
        self.minValue = minValue
        self.value = value
        
    def __str__(self):
        return "%s < %g (%s = %g in %s)" % (self.expression, self.minValue,
                                            self.expression, self.value,
                                            self.groupName)


class SubShapeID:
    """
    This class enables the use of sub-shapes in sets or as dictionary keys.
    It implements __eq__ and __hash__ methods so that sub-shapes with the same
    CORBA object `mainShape` and the same `id` are considered equal.
    """

    def __init__(self, mainShape, id):
        self._mainShape = mainShape
        self._id = id

    def getObj(self, geom):
        """
        Return the sub-shape (GEOM object). `geom` is a pseudo-geompy object
        used to find the geometrical object.
        """
        return geom.GetSubShape(self._mainShape, [self._id])
    
    def __eq__(self, other):
        return self._mainShape._is_equivalent(other._mainShape) and \
               self._id == other._id
    
    def __hash__(self):
        return self._mainShape._hash(2147483647) ^ self._id


class StructuralElementPart:
    """
    This class is the base class for all structural element parts. It should
    not be instantiated directly (consider it as an "abstract" class).

    :type  studyId: integer
    :param studyId: the ID of the study in which the part is created.

    :type  groupName: string
    :param groupName: the name of the underlying geometrical primitive in the
                      study.

    :type  groupGeomObj: GEOM object
    :param groupGeomObj: the underlying geometrical primitive.

    :type  parameters: dictionary
    :param parameters: parameters defining the structural element (see
                       subclasses for details).

    :type  name: string
    :param name: name to use for the created object in the study.

    """
    
    DEFAULT_NAME = "StructElemPart"

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = DEFAULT_NAME, color = None):
        self._parameters = parameters
        self.groupName = groupName
        self._groupGeomObj = groupGeomObj
        self._orientation = None
        self._paramUserName = {}
        self.name = name
        self.geom = getGeompy(studyId)
        self.baseShapesSet = set()
        self.isMainShape = groupGeomObj.IsMainShape()
        if not self.isMainShape:
            mainShape = self.geom.GetMainShape(groupGeomObj)
            listIDs = self.geom.GetObjectIDs(groupGeomObj)
            if mainShape is not None and listIDs is not None:
                for id in listIDs:
                    self.baseShapesSet.add(SubShapeID(mainShape, id))
        self.color = color
        if self.color is None:
            self.color = self._groupGeomObj.GetColor()

    def _getParameter(self, nameList, default = None):
        """
        This method finds the value of a parameter in the parameters
        dictionary. The argument is a list because some parameters can have
        several different names.
        """
        if len(nameList) > 0:
            paramName = nameList[0]
        for name in nameList:
            if self._parameters.has_key(name):
                self._paramUserName[paramName] = name
                return self._parameters[name]
        return default

    def _getParamUserName(self, paramName):
        """
        This method finds the user name for a parameter.
        """
        if self._paramUserName.has_key(paramName):
            return self._paramUserName[paramName]
        else:
            return paramName

    def __repr__(self):
        reprdict = self.__dict__.copy()
        del reprdict["_parameters"]
        del reprdict["groupName"]
        del reprdict["_groupGeomObj"]
        del reprdict["_paramUserName"]
        del reprdict["name"]
        del reprdict["geom"]
        del reprdict["baseShapesSet"]
        return '%s("%s", %s)' % (self.__class__.__name__, self.groupName,
                                 reprdict)

    def addOrientation(self, orientParams):
        """
        Add orientation information to the structural element part. See class
        :class:`~salome.geom.structelem.orientation.Orientation1D` for the description
        of the parameters.
        """
        self._orientation.addParams(orientParams)

    def _checkSize(self, value, mindim, expression):
        """
        This method checks that some parameters or some expressions involving
        those parameters are greater than a minimum value.
        """
        if value < mindim:
            raise InvalidParameterError(self.groupName, expression,
                                        mindim, value)

    def build(self):
        """
        Build the geometric shapes and the markers corresponding to the
        structural element part in the study `studyId`.
        """
        shape = self._buildPart()
        markers = self._buildMarkers()
        shape.SetColor(self.color)
        for marker in markers:
            marker.SetColor(self.color)
        return (shape, markers)

    def _buildPart(self):
        """
        This abstract method must be implemented in subclasses and should
        create the geometrical shape(s) of the structural element part.
        """
        raise NotImplementedError("Method _buildPart not implemented in class"
                                  " %s (it must be implemented in "
                                  "StructuralElementPart subclasses)." %
                                  self.__class__.__name__)

    def _buildMarkers(self):
        """
        This abstract method must be implemented in subclasses and should
        create the markers defining the orientation of the structural element
        part.
        """
        raise NotImplementedError("Method _buildMarker not implemented in "
                                  "class %s (it must be implemented in "
                                  "StructuralElementPart subclasses)." %
                                  self.__class__.__name__)

    def _getSubShapes(self, minDim = MIN_LENGTH_FOR_EXTRUSION):
        """
        Find and return the base sub-shapes in the structural element part.
        """
        if self.isMainShape:
            return [self._groupGeomObj]
        subShapes = []
        for subShapeID in self.baseShapesSet:
            subShape = subShapeID.getObj(self.geom)
            length = self.geom.BasicProperties(subShape)[0]
            if length < minDim:
                logger.warning("Length too short (%s - ID %s, length = %g), "
                               "subshape will not be used in structural "
                               "element" % (self.groupName, subShapeID._id,
                                            length))
            else:
                subShapes.append(subShape)
        return subShapes


class Beam(StructuralElementPart):
    """
    This class is an "abstract" class for all 1D structural element parts. It
    should not be instantiated directly. See class
    :class:`StructuralElementPart` for the description of the parameters.
    """

    DEFAULT_NAME = "Beam"

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = DEFAULT_NAME, color = None):
        StructuralElementPart.__init__(self, studyId, groupName, groupGeomObj,
                                       parameters, name, color)
        self._orientation = orientation.Orientation1D()

    def _isReversed(self, path):
        """
        This method checks if a 1D object is "reversed", i.e. if its
        orientation is different than the orientation of the underlying OCC
        object.
        """
        length = self.geom.BasicProperties(path)[0]
        p1 = self.geom.MakeVertexOnCurve(path, 0.0)
        p2 = self.geom.GetFirstVertex(path)
        dist = self.geom.MinDistance(p1, p2)
        return dist > length / 2

    def _getVertexAndTangentOnOrientedWire(self, path, param):
        """
        Get a vertex and the corresponding tangent on a wire by parameter.
        This method takes into account the "real" orientation of the wire
        (i.e. the orientation of the underlying OCC object).
        """
        if self._isReversed(path):
            vertex = self.geom.MakeVertexOnCurve(path, 1.0 - param)
            invtangent = self.geom.MakeTangentOnCurve(path, 1.0 - param)
            tanpoint = self.geom.MakeTranslationVectorDistance(vertex,
                                                               invtangent,
                                                               -1.0)
            tangent = self.geom.MakeVector(vertex, tanpoint)
        else:
            vertex = self.geom.MakeVertexOnCurve(path, param)
            tangent = self.geom.MakeTangentOnCurve(path, param)
        return (vertex, tangent)

    def _makeSolidPipeFromWires(self, wire1, wire2, point1, point2, path):
        """
        Create a solid by the extrusion of section `wire1` to section `wire2`
        along `path`.
        """
        face1 = self.geom.MakeFace(wire1, True)
        face2 = self.geom.MakeFace(wire2, True)
        shell = self.geom.MakePipeWithDifferentSections([wire1, wire2],
                                                        [point1, point2],
                                                        path, False, False)
        closedShell = self.geom.MakeShell([face1, face2, shell])
        solid = self.geom.MakeSolid([closedShell])
        return solid

    def _buildPart(self):
        """
        Build the structural element part.
        """
        # Get all the sub-shapes in the group (normally only edges and wires)
        paths = self._getSubShapes()
        listPipes = []
        withContact = False
        withCorrection = False
    
        for path in paths:
            # Build the sections (rectangular or circular) at each end of the
            # beam
            (fPoint, fNormal) = self._getVertexAndTangentOnOrientedWire(path,
                                                                        0.0)
            (lPoint, lNormal) = self._getVertexAndTangentOnOrientedWire(path,
                                                                        1.0)
            (outerWire1, innerWire1, outerWire2, innerWire2) = \
                    self._makeSectionWires(fPoint, fNormal, lPoint, lNormal)

            # Create the resulting solid
            outerSolid = self._makeSolidPipeFromWires(outerWire1, outerWire2,
                                                      fPoint, lPoint, path)
            if self.filling == HOLLOW:
                innerSolid = self._makeSolidPipeFromWires(innerWire1,
                                                          innerWire2, fPoint,
                                                          lPoint, path)
                resultSolid = self.geom.MakeCut(outerSolid, innerSolid)
                listPipes.append(resultSolid)
            else:
                listPipes.append(outerSolid)

        if len(listPipes) == 0:
            return None
        elif len(listPipes) == 1:
            return listPipes[0]
        else:
            return self.geom.MakeCompound(listPipes)

    def _buildMarkers(self):
        """
        Build the markers defining the orientation of the structural element
        part.
        """
        param = 0.5
        paths = self._getSubShapes()
        listMarkers = []
        for path in paths:
            (center, vecX) = self._getVertexAndTangentOnOrientedWire(path,
                                                                     param)
            marker = self._orientation.buildMarker(self.geom, center, vecX)
            listMarkers.append(marker)
        return listMarkers


class CircularBeam(Beam):
    """
    This class defines a beam with a circular section. It can be full or
    hollow, and its radius and thickness can vary from one end of the beam to
    the other. The valid parameters for circular beams are:

    * "R1" or "R": radius at the first end of the beam.
    * "R2" or "R": radius at the other end of the beam.
    * "EP1" or "EP" (optional): thickness at the first end of the beam.
      If not specified or equal to 0, the beam is considered full.
    * "EP2" or "EP" (optional): thickness at the other end of the beam.
      If not specified or equal to 0, the beam is considered full.

    See class :class:`StructuralElementPart` for the description of the
    other parameters.

    """

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = Beam.DEFAULT_NAME, color = None):
        if color is None:
            if parameters.has_key("R1"): # variable section
                color = LIGHT_RED
            else:                       # constant section
                color = RED

        Beam.__init__(self, studyId, groupName, groupGeomObj, parameters,
                      name, color)

        self.R1 = self._getParameter(["R1", "R"])
        self.R2 = self._getParameter(["R2", "R"])
        self.EP1 = self._getParameter(["EP1", "EP"])
        self.EP2 = self._getParameter(["EP2", "EP"])

        if self.EP1 is None or self.EP2 is None or \
                                self.EP1 == 0 or self.EP2 == 0:
            self.filling = FULL
        else:
            self.filling = HOLLOW

        logger.debug(repr(self))

        # Check parameters
        self._checkSize(self.R1, MIN_DIM_FOR_EXTRUDED_SHAPE / 2.0,
                        self._getParamUserName("R1"))
        self._checkSize(self.R2, MIN_DIM_FOR_EXTRUDED_SHAPE / 2.0,
                        self._getParamUserName("R2"))
        if self.filling == HOLLOW:
            self._checkSize(self.EP1, MIN_THICKNESS,
                            self._getParamUserName("EP1"))
            self._checkSize(self.EP2, MIN_THICKNESS,
                            self._getParamUserName("EP2"))
            self._checkSize(self.R1 - self.EP1,
                            MIN_DIM_FOR_EXTRUDED_SHAPE / 2.0,
                            "%s - %s" % (self._getParamUserName("R1"),
                                         self._getParamUserName("EP1")))
            self._checkSize(self.R2 - self.EP2,
                            MIN_DIM_FOR_EXTRUDED_SHAPE / 2.0,
                            "%s - %s" % (self._getParamUserName("R2"),
                                         self._getParamUserName("EP2")))

    def _makeSectionWires(self, fPoint, fNormal, lPoint, lNormal):
        """
        Create the circular sections used to build the pipe.
        """
        outerCircle1 = self.geom.MakeCircle(fPoint, fNormal, self.R1)
        outerCircle2 = self.geom.MakeCircle(lPoint, lNormal, self.R2)
        if self.filling == HOLLOW:
            innerCircle1 = self.geom.MakeCircle(fPoint, fNormal,
                                                self.R1 - self.EP1)
            innerCircle2 = self.geom.MakeCircle(lPoint, lNormal,
                                                self.R2 - self.EP2)
        else:
            innerCircle1 = None
            innerCircle2 = None

        return (outerCircle1, innerCircle1, outerCircle2, innerCircle2)


class RectangularBeam(Beam):
    """
    This class defines a beam with a rectangular section. It can be full or
    hollow, and its dimensions can vary from one end of the beam to the other.
    The valid parameters for rectangular beams are:

    * "HY1", "HY", "H1" or "H": width at the first end of the beam.
    * "HZ1", "HZ", "H1" or "H": height at the first end of the beam.
    * "HY2", "HY", "H2" or "H": width at the other end of the beam.
    * "HZ2", "HZ", "H2" or "H": height at the other end of the beam.
    * "EPY1", "EPY", "EP1" or "EP" (optional): thickness in the width
      direction at the first end of the beam. If not specified or equal to 0,
      the beam is considered full.
    * "EPZ1", "EPZ", "EP1" or "EP" (optional): thickness in the height
      direction at the first end of the beam. If not specified or equal to 0,
      the beam is considered full.
    * "EPY2", "EPY", "EP2" or "EP" (optional): thickness in the width
      direction at the other end of the beam. If not specified or equal to 0,
      the beam is considered full.
    * "EPZ2", "EPZ", "EP2" or "EP" (optional): thickness in the height
      direction at the other end of the beam. If not specified or equal to 0,
      the beam is considered full.

    See class :class:`StructuralElementPart` for the description of the
    other parameters.

    """

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = Beam.DEFAULT_NAME, color = None):
        if color is None:
            if parameters.has_key("HY1") or parameters.has_key("H1"):
                color = LIGHT_BLUE # variable section
            else:                  # constant section
                color = BLUE

        Beam.__init__(self, studyId, groupName, groupGeomObj, parameters,
                      name, color)

        self.HY1 = self._getParameter(["HY1", "HY", "H1", "H"])
        self.HZ1 = self._getParameter(["HZ1", "HZ", "H1", "H"])
        self.HY2 = self._getParameter(["HY2", "HY", "H2", "H"])
        self.HZ2 = self._getParameter(["HZ2", "HZ", "H2", "H"])
        self.EPY1 = self._getParameter(["EPY1", "EPY", "EP1", "EP"])
        self.EPZ1 = self._getParameter(["EPZ1", "EPZ", "EP1", "EP"])
        self.EPY2 = self._getParameter(["EPY2", "EPY", "EP2", "EP"])
        self.EPZ2 = self._getParameter(["EPZ2", "EPZ", "EP2", "EP"])

        if self.EPY1 is None or self.EPZ1 is None or \
           self.EPY2 is None or self.EPZ2 is None or \
           self.EPY1 == 0 or self.EPZ1 == 0 or \
           self.EPY2 == 0 or self.EPZ2 == 0:
            self.filling = FULL
        else:
            self.filling = HOLLOW

        logger.debug(repr(self))

        # Check parameters
        self._checkSize(self.HY1, MIN_DIM_FOR_EXTRUDED_SHAPE,
                        self._getParamUserName("HY1"))
        self._checkSize(self.HZ1, MIN_DIM_FOR_EXTRUDED_SHAPE,
                        self._getParamUserName("HZ1"))
        self._checkSize(self.HY2, MIN_DIM_FOR_EXTRUDED_SHAPE,
                        self._getParamUserName("HY2"))
        self._checkSize(self.HZ2, MIN_DIM_FOR_EXTRUDED_SHAPE,
                        self._getParamUserName("HZ2"))
        if self.filling == HOLLOW:
            self._checkSize(self.EPY1, MIN_THICKNESS,
                            self._getParamUserName("EPY1"))
            self._checkSize(self.EPZ1, MIN_THICKNESS,
                            self._getParamUserName("EPZ1"))
            self._checkSize(self.EPY2, MIN_THICKNESS,
                            self._getParamUserName("EPY2"))
            self._checkSize(self.EPZ2, MIN_THICKNESS,
                            self._getParamUserName("EPZ2"))
            self._checkSize(self.HY1 - 2 * self.EPY1,
                            MIN_DIM_FOR_EXTRUDED_SHAPE,
                            "%s - 2 * %s" % (self._getParamUserName("HY1"),
                                             self._getParamUserName("EPY1")))
            self._checkSize(self.HZ1 - 2 * self.EPZ1,
                            MIN_DIM_FOR_EXTRUDED_SHAPE,
                            "%s - 2 * %s" % (self._getParamUserName("HZ1"),
                                             self._getParamUserName("EPZ1")))
            self._checkSize(self.HY2 - 2 * self.EPY2,
                            MIN_DIM_FOR_EXTRUDED_SHAPE,
                            "%s - 2 * %s" % (self._getParamUserName("HY2"),
                                             self._getParamUserName("EPY2")))
            self._checkSize(self.HZ2 - 2 * self.EPZ2,
                            MIN_DIM_FOR_EXTRUDED_SHAPE,
                            "%s - 2 * %s" % (self._getParamUserName("HZ2"),
                                             self._getParamUserName("EPZ2")))

    def _makeRectangle(self, HY, HZ, lcs):
        """
        Create a rectangle in the specified plane.
        """
        halfHY = HY / 2.0
        halfHZ = HZ / 2.0
        sketchStr = "Sketcher:F %g %g:" % (-halfHY, -halfHZ)
        sketchStr += "TT %g %g:" % (halfHY, -halfHZ)
        sketchStr += "TT %g %g:" % (halfHY, halfHZ)
        sketchStr += "TT %g %g:WW" % (-halfHY, halfHZ)
        logger.debug('Drawing rectangle: "%s"' % sketchStr)
        sketch = self.geom.MakeSketcherOnPlane(sketchStr, lcs)
        return sketch

    def _makeSectionRectangles(self, point, vecX, HY, HZ, EPY, EPZ):
        """
        Create one side of the rectangular sections used to build the pipe.
        """
        (vecY, vecZ) = self._orientation.getVecYZ(self.geom, point, vecX)
        lcs = self.geom.MakeMarkerPntTwoVec(point, vecY, vecZ)
        outerRect = self._makeRectangle(HY, HZ, lcs)
        if self.filling == HOLLOW:
            innerRect = self._makeRectangle(HY - 2.0 * EPY,
                                            HZ - 2.0 * EPZ,
                                            lcs)
        else:
            innerRect = None
        return (outerRect, innerRect)

    def _makeSectionWires(self, fPoint, fNormal, lPoint, lNormal):
        """
        Create the rectangular sections used to build the pipe.
        """
        (outerRect1, innerRect1) = \
            self._makeSectionRectangles(fPoint, fNormal, self.HY1, self.HZ1,
                                        self.EPY1, self.EPZ1)
        (outerRect2, innerRect2) = \
            self._makeSectionRectangles(lPoint, lNormal, self.HY2, self.HZ2,
                                        self.EPY2, self.EPZ2)
        return (outerRect1, innerRect1, outerRect2, innerRect2)


def getParameterInDict(nameList, parametersDict, default = None):
    """
    This method finds the value of a parameter in the parameters
    dictionary. The argument is a list because some parameters can have
    several different names.
    """
    for name in nameList:
        if parametersDict.has_key(name):
            return parametersDict[name]
    return default


class GeneralBeam(RectangularBeam):
    """
    This class defines a beam with a generic section. It is represented as a
    full rectangular beam with the following parameters:
    
    * HY1 = sqrt(12 * IZ1 / A1)
    * HZ1 = sqrt(12 * IY1 / A1)
    * HY2 = sqrt(12 * IZ2 / A2)
    * HZ2 = sqrt(12 * IY2 / A2)
    
    See class :class:`StructuralElementPart` for the description of the other
    parameters.
    """

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = Beam.DEFAULT_NAME, color = None):
        self.IY1 = getParameterInDict(["IY1", "IY"], parameters)
        self.IZ1 = getParameterInDict(["IZ1", "IZ"], parameters)
        self.IY2 = getParameterInDict(["IY2", "IY"], parameters)
        self.IZ2 = getParameterInDict(["IZ2", "IZ"], parameters)
        self.A1 = getParameterInDict(["A1", "A"], parameters)
        self.A2 = getParameterInDict(["A2", "A"], parameters)
        parameters["HY1"] = math.sqrt(12 * self.IZ1 / self.A1)
        parameters["HZ1"] = math.sqrt(12 * self.IY1 / self.A1)
        parameters["HY2"] = math.sqrt(12 * self.IZ2 / self.A2)
        parameters["HZ2"] = math.sqrt(12 * self.IY2 / self.A2)

        if color is None:
            if parameters.has_key("IY1"): # variable section
                color = LIGHT_GREEN
            else:                         # constant section
                color = GREEN

        RectangularBeam.__init__(self, studyId, groupName, groupGeomObj, parameters,
                                 name, color)


class StructuralElementPart2D(StructuralElementPart):
    """
    This class is an "abstract" class for all 2D structural element parts. It
    should not be instantiated directly. See class
    :class:`StructuralElementPart` for the description of the parameters.
    """

    DEFAULT_NAME = "StructuralElementPart2D"

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = DEFAULT_NAME):
        StructuralElementPart.__init__(self, studyId, groupName, groupGeomObj,
                                       parameters, name)
        self._orientation = orientation.Orientation2D(
                                        self._getParameter(["angleAlpha"]),
                                        self._getParameter(["angleBeta"]),
                                        self._getParameter(["Vecteur"]))
        self.offset = self._getParameter(["Excentre"], 0.0)

    def _makeFaceOffset(self, face, offset, epsilon = 1e-6):
        """
        Create a copy of a face at a given offset.
        """
        if abs(offset) < epsilon:
            return self.geom.MakeCopy(face)
        else:
            offsetObj = self.geom.MakeOffset(face, offset)
            # We have to explode the resulting object into faces because it is
            # created as a polyhedron and not as a single face
            faces = self.geom.SubShapeAll(offsetObj,
                                          self.geom.ShapeType["FACE"])
            return faces[0]

    def _buildMarkersWithOffset(self, offset):
        """
        Build the markers for the structural element part with a given offset
        from the base face.
        """
        uParam = 0.5
        vParam = 0.5
        listMarkers = []
        subShapes = self._getSubShapes()
    
        for subShape in subShapes:
            faces = self.geom.SubShapeAll(subShape,
                                          self.geom.ShapeType["FACE"])
            for face in faces:
                offsetFace = self._makeFaceOffset(face, offset)
                # get tangent plane on surface by parameters
                center = self.geom.MakeVertexOnSurface(offsetFace,
                                                       uParam, vParam)
                tangPlane = self.geom.MakeTangentPlaneOnFace(offsetFace,
                                                             uParam, vParam,
                                                             1.0)
                normal = self.geom.GetNormal(tangPlane)
                marker = self._orientation.buildMarker(self.geom,
                                                       center, normal)
                listMarkers.append(marker)

        return listMarkers


class ThickShell(StructuralElementPart2D):
    """
    This class defines a shell with a given thickness. It can be shifted from
    the base face. The valid parameters for thick shells are:

    * "Epais": thickness of the shell.
    * "Excentre": offset of the shell from the base face.
    * "angleAlpha": angle used to build the markers (see class
      :class:`~salome.geom.structelem.orientation.Orientation2D`)
    * "angleBeta": angle used to build the markers (see class
      :class:`~salome.geom.structelem.orientation.Orientation2D`)
    * "Vecteur": vector used instead of the angles to build the markers (see
      class :class:`~salome.geom.structelem.orientation.Orientation2D`)

    See class :class:`StructuralElementPart` for the description of the
    other parameters.
    """

    DEFAULT_NAME = "ThickShell"

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = DEFAULT_NAME):
        StructuralElementPart2D.__init__(self, studyId, groupName,
                                         groupGeomObj, parameters, name)
        self.thickness = self._getParameter(["Epais"])
        logger.debug(repr(self))

    def _buildPart(self):
        """
        Create the geometrical shapes corresponding to the thick shell.
        """
        subShapes = self._getSubShapes()
        listSolids = []
    
        for subShape in subShapes:
            faces = self.geom.SubShapeAll(subShape,
                                          self.geom.ShapeType["FACE"])
            for face in faces:
                shape = self._buildThickShellForFace(face)
                listSolids.append(shape)

        if len(listSolids) == 0:
            return None
        elif len(listSolids) == 1:
            return listSolids[0]
        else:
            return self.geom.MakeCompound(listSolids)

    def _buildThickShellForFace(self, face):
        """
        Create the geometrical shapes corresponding to the thick shell for a
        given face.
        """
        epsilon = 1e-6
        if self.thickness < 2 * epsilon:
            return self._makeFaceOffset(face, self.offset, epsilon)

        upperOffset = self.offset + self.thickness / 2.0
        lowerOffset = self.offset - self.thickness / 2.0
        ruledMode = True
        modeSolid = False

        upperFace = self._makeFaceOffset(face, upperOffset, epsilon)
        lowerFace = self._makeFaceOffset(face, lowerOffset, epsilon)
        listShapes = [upperFace, lowerFace]
        upperWires = self.geom.SubShapeAll(upperFace,
                                           self.geom.ShapeType["WIRE"])
        lowerWires = self.geom.SubShapeAll(lowerFace,
                                           self.geom.ShapeType["WIRE"])
        if self.geom.KindOfShape(face)[0] == self.geom.kind.CYLINDER2D:
            # if the face is a cylinder, we remove the extra side edge
            upperWires = self._removeCylinderExtraEdge(upperWires)
            lowerWires = self._removeCylinderExtraEdge(lowerWires)
        for i in range(len(upperWires)):
            resShape = self.geom.MakeThruSections([upperWires[i],
                                                   lowerWires[i]],
                                                  modeSolid, epsilon,
                                                  ruledMode)
            listShapes.append(resShape)
        resultShell = self.geom.MakeShell(listShapes)
        resultSolid = self.geom.MakeSolid([resultShell])
        return resultSolid

    def _removeCylinderExtraEdge(self, wires):
        """
        Remove the side edge in a cylinder.
        """
        result = []
        for wire in wires:
            edges = self.geom.SubShapeAll(wire, self.geom.ShapeType["EDGE"])
            for edge in edges:
                if self.geom.KindOfShape(edge)[0] == self.geom.kind.CIRCLE:
                    result.append(edge)
        return result

    def _buildMarkers(self):
        """
        Build the markers defining the orientation of the thick shell.
        """
        return self._buildMarkersWithOffset(self.offset +
                                            self.thickness / 2.0)


class Grid(StructuralElementPart2D):
    """
    This class defines a grid. A grid is represented by a 2D face patterned
    with small lines in the main direction of the grid frame. The valid
    parameters for grids are:

    * "Excentre": offset of the grid from the base face.
    * "angleAlpha": angle used to build the markers (see class
      :class:`~salome.geom.structelem.orientation.Orientation2D`)
    * "angleBeta": angle used to build the markers (see class
      :class:`~salome.geom.structelem.orientation.Orientation2D`)
    * "Vecteur": vector used instead of the angles to build the markers (see
      class :class:`~salome.geom.structelem.orientation.Orientation2D`)
    * "origAxeX": X coordinate of the origin of the axis used to determine the
      orientation of the frame in the case of a cylindrical grid.
    * "origAxeY": Y coordinate of the origin of the axis used to determine the
      orientation of the frame in the case of a cylindrical grid.
    * "origAxeZ": Z coordinate of the origin of the axis used to determine the
      orientation of the frame in the case of a cylindrical grid.
    * "axeX": X coordinate of the axis used to determine the orientation of
      the frame in the case of a cylindrical grid.
    * "axeY": Y coordinate of the axis used to determine the orientation of
      the frame in the case of a cylindrical grid.
    * "axeZ": Z coordinate of the axis used to determine the orientation of
      the frame in the case of a cylindrical grid.

    See class :class:`StructuralElementPart` for the description of the
    other parameters.
    """

    DEFAULT_NAME = "Grid"

    def __init__(self, studyId, groupName, groupGeomObj, parameters,
                 name = DEFAULT_NAME):
        StructuralElementPart2D.__init__(self, studyId, groupName,
                                         groupGeomObj, parameters, name)
        self.xr = self._getParameter(["origAxeX"])
        self.yr = self._getParameter(["origAxeY"])
        self.zr = self._getParameter(["origAxeZ"])
        self.vx = self._getParameter(["axeX"])
        self.vy = self._getParameter(["axeY"])
        self.vz = self._getParameter(["axeZ"])
        logger.debug(repr(self))

    def _buildPart(self):
        """
        Create the geometrical shapes representing the grid.
        """
        subShapes = self._getSubShapes()
        listGridShapes = []
    
        for subShape in subShapes:
            faces = self.geom.SubShapeAll(subShape,
                                          self.geom.ShapeType["FACE"])
            for face in faces:
                if self.geom.KindOfShape(face)[0] == \
                                        self.geom.kind.CYLINDER2D and \
                        self.xr is not None and self.yr is not None and \
                        self.zr is not None and self.vx is not None and \
                        self.vy is not None and self.vz is not None:
                    shape = self._buildGridForCylinderFace(face)
                else:
                    shape = self._buildGridForNormalFace(face)
                listGridShapes.append(shape)

        if len(listGridShapes) == 0:
            return None
        elif len(listGridShapes) == 1:
            return listGridShapes[0]
        else:
            return self.geom.MakeCompound(listGridShapes)

    def _buildGridForNormalFace(self, face):
        """
        Create the geometrical shapes representing the grid for a given
        non-cylindrical face.
        """
        baseFace = self._makeFaceOffset(face, self.offset)
        gridList = [baseFace]
        
        # Compute display length for grid elements
        p1 = self.geom.MakeVertexOnSurface(baseFace, 0.0, 0.0)
        p2 = self.geom.MakeVertexOnSurface(baseFace, 0.1, 0.1)
        length = self.geom.MinDistance(p1, p2) / 2.0

        for u in range(1, 10):
            uParam = u * 0.1
            for v in range(1, 10):
                vParam = v * 0.1
                # get tangent plane on surface by parameters
                center = self.geom.MakeVertexOnSurface(baseFace,
                                                       uParam, vParam)
                tangPlane = self.geom.MakeTangentPlaneOnFace(baseFace, uParam,
                                                             vParam, 1.0)
                
                # use the marker to get the orientation of the frame
                normal = self.geom.GetNormal(tangPlane)
                marker = self._orientation.buildMarker(self.geom, center,
                                                       normal, False)
                [Ox,Oy,Oz, Zx,Zy,Zz, Xx,Xy,Xz] = self.geom.GetPosition(marker)
                xPoint = self.geom.MakeTranslation(center, Xx * length,
                                                   Xy * length, Xz * length)
                gridLine = self.geom.MakeLineTwoPnt(center, xPoint)
                gridList.append(gridLine)
        grid = self.geom.MakeCompound(gridList)
        return grid

    def _buildGridForCylinderFace(self, face):
        """
        Create the geometrical shapes representing the grid for a given
        cylindrical face.
        """
        baseFace = self._makeFaceOffset(face, self.offset)
        gridList = [baseFace]
        
        # Compute display length for grid elements
        p1 = self.geom.MakeVertexOnSurface(baseFace, 0.0, 0.0)
        p2 = self.geom.MakeVertexOnSurface(baseFace, 0.1, 0.1)
        length = self.geom.MinDistance(p1, p2) / 2.0
        
        # Create reference vector V
        origPoint = self.geom.MakeVertex(self.xr, self.yr, self.zr)
        vPoint = self.geom.MakeTranslation(origPoint,
                                           self.vx, self.vy, self.vz)
        refVec = self.geom.MakeVector(origPoint, vPoint)

        for u in range(10):
            uParam = u * 0.1
            for v in range(1, 10):
                vParam = v * 0.1
                
                # Compute the local orientation of the frame
                center = self.geom.MakeVertexOnSurface(baseFace,
                                                       uParam, vParam)
                locPlaneYZ = self.geom.MakePlaneThreePnt(origPoint, center,
                                                         vPoint, 1.0)
                locOrient = self.geom.GetNormal(locPlaneYZ)
                xPoint = self.geom.MakeTranslationVectorDistance(center,
                                                                 locOrient,
                                                                 length)
                gridLine = self.geom.MakeLineTwoPnt(center, xPoint)
                gridList.append(gridLine)

        grid = self.geom.MakeCompound(gridList)
        return grid

    def _buildMarkers(self):
        """
        Create the markers defining the orientation of the grid.
        """
        return self._buildMarkersWithOffset(self.offset)


def VisuPoutreGenerale(studyId, groupName, groupGeomObj, parameters,
                       name = "POUTRE"):
    """
    Alias for class :class:`GeneralBeam`.
    """
    return GeneralBeam(studyId, groupName, groupGeomObj, parameters, name)

def VisuPoutreCercle(studyId, groupName, groupGeomObj, parameters,
                     name = "POUTRE"):
    """
    Alias for class :class:`CircularBeam`.
    """
    return CircularBeam(studyId, groupName, groupGeomObj, parameters, name)
  
def VisuPoutreRectangle(studyId, groupName, groupGeomObj, parameters,
                        name = "POUTRE"):
    """
    Alias for class :class:`RectangularBeam`.
    """
    return RectangularBeam(studyId, groupName, groupGeomObj, parameters, name)
  
def VisuBarreGenerale(studyId, groupName, groupGeomObj, parameters,
                      name = "BARRE"):
    """
    Alias for class :class:`GeneralBeam`.
    """
    return GeneralBeam(studyId, groupName, groupGeomObj, parameters, name,
                       color = ORANGE)
      
def VisuBarreRectangle(studyId, groupName, groupGeomObj, parameters,
                       name = "BARRE"):
    """
    Alias for class :class:`RectangularBeam`.
    """
    return RectangularBeam(studyId, groupName, groupGeomObj, parameters, name,
                           color = ORANGE)

def VisuBarreCercle(studyId, groupName, groupGeomObj, parameters,
                    name = "BARRE"):
    """
    Alias for class :class:`CircularBeam`.
    """
    return CircularBeam(studyId, groupName, groupGeomObj, parameters, name,
                        color = ORANGE)

def VisuCable(studyId, groupName, groupGeomObj, parameters, name = "CABLE"):
    """
    Alias for class :class:`CircularBeam`.
    """
    return CircularBeam(studyId, groupName, groupGeomObj, parameters, name,
                        color = PURPLE)

def VisuCoque(studyId, groupName, groupGeomObj, parameters, name = "COQUE"):
    """
    Alias for class :class:`ThickShell`.
    """
    return ThickShell(studyId, groupName, groupGeomObj, parameters, name)
  
def VisuGrille(studyId, groupName, groupGeomObj, parameters, name = "GRILLE"):
    """
    Alias for class :class:`Grid`.
    """
    return Grid(studyId, groupName, groupGeomObj, parameters, name)
