# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2013 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                                         *
# *   FreeCAD 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, or (at your option) any later version.                       *
# *                                                                         *
# *   FreeCAD 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 FreeCAD. If not, see                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************

__title__ = "FreeCAD Arch Space"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecad.org"

## @package ArchSpace
#  \ingroup ARCH
#  \brief The Space object and tools
#
#  This module provides tools to build Space objects.
#  Spaces define an open volume inside or outside a
#  building, ie. a room.

import re

import FreeCAD
import ArchComponent
import ArchCommands
import Draft

from draftutils import params

if FreeCAD.GuiUp:
    from PySide import QtCore, QtGui
    from PySide.QtCore import QT_TRANSLATE_NOOP
    import FreeCADGui
    from draftutils.translate import translate
else:
    # \cond
    def translate(ctxt, txt):
        return txt

    def QT_TRANSLATE_NOOP(ctxt, txt):
        return txt

    # \endcond

SpaceTypes = [
    "Undefined",
    "Exterior",
    "Exterior - Terrace",
    "Office",
    "Office - Enclosed",
    "Office - Open Plan",
    "Conference / Meeting / Multipurpose",
    "Classroom / Lecture / Training For Penitentiary",
    "Lobby",
    "Lobby - For Hotel",
    "Lobby - For Performing Arts Theater",
    "Lobby - For Motion Picture Theater",
    "Audience/Seating Area",
    "Audience/Seating Area - For Gymnasium",
    "Audience/Seating Area - For Exercise Center",
    "Audience/Seating Area - For Convention Center",
    "Audience/Seating Area - For Penitentiary",
    "Audience/Seating Area - For Religious Buildings",
    "Audience/Seating Area - For Sports Arena",
    "Audience/Seating Area - For Performing Arts Theater",
    "Audience/Seating Area - For Motion Picture Theater",
    "Audience/Seating Area - For Transportation",
    "Atrium",
    "Atrium - First Three Floors",
    "Atrium - Each Additional Floor",
    "Lounge / Recreation",
    "Lounge / Recreation - For Hospital",
    "Dining Area",
    "Dining Area - For Penitentiary",
    "Dining Area - For Hotel",
    "Dining Area - For Motel",
    "Dining Area - For Bar Lounge/Leisure Dining",
    "Dining Area - For Family Dining",
    "Food Preparation",
    "Laboratory",
    "Restrooms",
    "Dressing / Locker / Fitting",
    "Room",
    "Corridor / Transition",
    "Corridor / Transition - For Hospital",
    "Corridor / Transition - For Manufacturing Facility",
    "Stairs",
    "Active Storage",
    "Active Storage - For Hospital",
    "Inactive Storage",
    "Inactive Storage - For Museum",
    "Electrical / Mechanical",
    "Gymnasium / Exercise Center",
    "Gymnasium / Exercise Center - Playing Area",
    "Gymnasium / Exercise Center - Exercise Area",
    "Courthouse / Police Station / Penitentiary",
    "Courthouse / Police Station / Penitentiary - Courtroom",
    "Courthouse / Police Station / Penitentiary - Confinement Cells",
    "Courthouse / Police Station / Penitentiary - Judges' Chambers",
    "Fire Stations",
    "Fire Stations - Engine Room",
    "Fire Stations - Sleeping Quarters",
    "Post Office - Sorting Area",
    "Convention Center - Exhibit Space",
    "Library",
    "Library - Card File and Cataloging",
    "Library - Stacks",
    "Library - Reading Area",
    "Hospital",
    "Hospital - Emergency",
    "Hospital - Recovery",
    "Hospital - Nurses' Station",
    "Hospital - Exam / Treatment",
    "Hospital - Pharmacy",
    "Hospital - Patient Room",
    "Hospital - Operating Room",
    "Hospital - Nursery",
    "Hospital - Medical Supply",
    "Hospital - Physical Therapy",
    "Hospital - Radiology",
    "Hospital - Laundry-Washing",
    "Automotive - Service / Repair",
    "Manufacturing",
    "Manufacturing - Low Bay (< 7.5m Floor to Ceiling Height)",
    "Manufacturing - High Bay (> 7.5m Floor to Ceiling Height)",
    "Manufacturing - Detailed Manufacturing",
    "Manufacturing - Equipment Room",
    "Manufacturing - Control Room",
    "Hotel / Motel Guest Rooms",
    "Dormitory - Living Quarters",
    "Museum",
    "Museum - General Exhibition",
    "Museum - Restoration",
    "Bank / Office - Banking Activity Area",
    "Workshop",
    "Sales Area",
    "Religious Buildings",
    "Religious Buildings - Worship Pulpit, Choir",
    "Religious Buildings - Fellowship Hall",
    "Retail",
    "Retail - Sales Area",
    "Retail - Mall Concourse",
    "Sports Arena",
    "Sports Arena - Ring Sports Area",
    "Sports Arena - Court Sports Area",
    "Sports Arena - Indoor Playing Field Area",
    "Warehouse",
    "Warehouse - Fine Material Storage",
    "Warehouse - Medium / Bulky Material Storage",
    "Parking Garage - Garage Area",
    "Transportation",
    "Transportation - Airport / Concourse",
    "Transportation - Air / Train / Bus - Baggage Area",
    "Transportation - Terminal - Ticket Counter",
]

ConditioningTypes = [
    "Unconditioned",
    "Heated",
    "Cooled",
    "HeatedAndCooled",
    "Vented",
    "NaturallyVentedOnly",
]

AreaCalculationType = ["XY-plane projection", "At Center of Mass"]


class _Space(ArchComponent.Component):
    "A space object"

    def __init__(self, obj):

        ArchComponent.Component.__init__(self, obj)
        self.Type = "Space"
        self.setProperties(obj)
        obj.IfcType = "Space"
        obj.CompositionType = "ELEMENT"

    def setProperties(self, obj):

        pl = obj.PropertiesList
        if not "Boundaries" in pl:
            obj.addProperty(
                "App::PropertyLinkSubList",
                "Boundaries",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The objects that make the boundaries of this space object"
                ),
                locked=True,
            )
        if not "Area" in pl:
            obj.addProperty(
                "App::PropertyArea",
                "Area",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "Identical to Horizontal Area"),
                locked=True,
            )
        if not "FinishFloor" in pl:
            obj.addProperty(
                "App::PropertyString",
                "FinishFloor",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The finishing of the floor of this space"),
                locked=True,
            )
        if not "FinishWalls" in pl:
            obj.addProperty(
                "App::PropertyString",
                "FinishWalls",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The finishing of the walls of this space"),
                locked=True,
            )
        if not "FinishCeiling" in pl:
            obj.addProperty(
                "App::PropertyString",
                "FinishCeiling",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The finishing of the ceiling of this space"),
                locked=True,
            )
        if not "Group" in pl:
            obj.addProperty(
                "App::PropertyLinkList",
                "Group",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Objects that are included inside this space, such as furniture",
                ),
                locked=True,
            )
        if not "SpaceType" in pl:
            obj.addProperty(
                "App::PropertyEnumeration",
                "SpaceType",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The type of this space"),
                locked=True,
            )
            obj.SpaceType = SpaceTypes
        if not "FloorThickness" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "FloorThickness",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The thickness of the floor finish"),
                locked=True,
            )
        if not "NumberOfPeople" in pl:
            obj.addProperty(
                "App::PropertyInteger",
                "NumberOfPeople",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The number of people who typically occupy this space"
                ),
                locked=True,
            )
        if not "LightingPower" in pl:
            obj.addProperty(
                "App::PropertyFloat",
                "LightingPower",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The electric power needed to light this space in Watts"
                ),
                locked=True,
            )
        if not "EquipmentPower" in pl:
            obj.addProperty(
                "App::PropertyFloat",
                "EquipmentPower",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The electric power needed by the equipment of this space in Watts",
                ),
                locked=True,
            )
        if not "AutoPower" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "AutoPower",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If True, Equipment Power will be automatically filled by the equipment included in this space",
                ),
                locked=True,
            )
        if not "Conditioning" in pl:
            obj.addProperty(
                "App::PropertyEnumeration",
                "Conditioning",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The type of air conditioning of this space"),
                locked=True,
            )
            obj.Conditioning = ConditioningTypes
        if not "Internal" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "Internal",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property", "Specifies if this space is internal or external"
                ),
                locked=True,
            )
            obj.Internal = True
        if not "AreaCalculationType" in pl:
            obj.addProperty(
                "App::PropertyEnumeration",
                "AreaCalculationType",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Defines the calculation type for the horizontal area and its perimeter length",
                ),
                locked=True,
            )
            obj.AreaCalculationType = AreaCalculationType

    def onDocumentRestored(self, obj):

        ArchComponent.Component.onDocumentRestored(self, obj)
        self.setProperties(obj)

    def loads(self, state):

        self.Type = "Space"

    def execute(self, obj):

        if self.clone(obj):
            return

        # Space can do without Base.  Base validity is tested in getShape() code below.
        # Remarked out ensureBase() below
        # if not self.ensureBase(obj):
        #    return
        self.getShape(obj)

    def onChanged(self, obj, prop):

        if prop == "Group":
            if hasattr(obj, "EquipmentPower"):
                if obj.AutoPower:
                    p = 0
                    for o in Draft.getObjectsOfType(
                        Draft.get_group_contents(obj.Group, addgroups=True), "Equipment"
                    ):
                        if hasattr(o, "EquipmentPower"):
                            p += o.EquipmentPower
                    if p != obj.EquipmentPower:
                        obj.EquipmentPower = p
        elif prop == "Zone":
            if obj.Zone:
                if obj.Zone.ViewObject:
                    if hasattr(obj.Zone.ViewObject, "Proxy"):
                        if hasattr(obj.Zone.ViewObject.Proxy, "claimChildren"):
                            obj.Zone.ViewObject.Proxy.claimChildren()
        if hasattr(obj, "Area"):
            obj.setEditorMode("Area", 1)
        ArchComponent.Component.onChanged(self, obj, prop)

    def addSubobjects(self, obj, subobjects):
        "adds subobjects to this space"
        objs = obj.Boundaries
        for o in subobjects:
            if isinstance(o, tuple) or isinstance(o, list):
                if o[0].Name != obj.Name:
                    objs.append(tuple(o))
            else:
                for el in o.SubElementNames:
                    if "Face" in el:
                        if o.Object.Name != obj.Name:
                            objs.append((o.Object, el))
        obj.Boundaries = objs

    def removeSubobjects(self, obj, subobjects):
        "removes subobjects to this space"
        bounds = obj.Boundaries
        for o in subobjects:
            for b in bounds:
                if o.Name == b[0].Name:
                    bounds.remove(b)
                    break
        obj.Boundaries = bounds

    def addObject(self, obj, child):
        "Adds an object to this Space"

        if not child in obj.Group:
            g = obj.Group
            g.append(child)
            obj.Group = g

    def getShape(self, obj):
        "computes a shape from a base shape and/or boundary faces"
        import Part

        shape = None
        faces = []

        pl = obj.Placement

        # print("starting compute")

        # 1: if we have a base shape, we use it
        # Check if there is obj.Base and its validity to proceed
        if self.ensureBase(obj):
            if obj.Base.Shape.Solids:
                shape = obj.Base.Shape.copy()
                shape = shape.removeSplitter()

        # 2: if not, add all bounding boxes of considered objects and build a first shape
        if shape:
            # print("got shape from base object")
            bb = shape.BoundBox
        else:
            bb = None
            for b in obj.Boundaries:
                if hasattr(b[0], "Shape"):
                    if not bb:
                        bb = b[0].Shape.BoundBox
                    else:
                        bb.add(b[0].Shape.BoundBox)
            if not bb:
                # compute area even if we are not calculating the shape
                if obj.Shape and obj.Shape.Solids:
                    if hasattr(obj.Area, "Value"):
                        a = self.getArea(obj)
                        if obj.Area.Value != a:
                            obj.Area = a
                return
            shape = Part.makeBox(
                bb.XLength, bb.YLength, bb.ZLength, FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin)
            )
            # print("created shape from boundbox")

        # 3: identifying boundary faces
        goodfaces = []
        for b in obj.Boundaries:
            if hasattr(b[0], "Shape"):
                for sub in b[1]:
                    if "Face" in sub:
                        fn = int(sub[4:]) - 1
                        faces.append(b[0].Shape.Faces[fn])
                        # print("adding face ",fn," of object ",b[0].Name)

        # print("total: ", len(faces), " faces")

        # 4: get cutvolumes from faces
        cutvolumes = []
        for f in faces:
            f = f.copy()
            f.reverse()
            cutface, cutvolume, invcutvolume = ArchCommands.getCutVolume(f, shape)
            if cutvolume:
                # print("generated 1 cutvolume")
                cutvolumes.append(cutvolume.copy())
                # Part.show(cutvolume)
        for v in cutvolumes:
            # print("cutting")
            shape = shape.cut(v)

        # 5: get the final shape
        if shape:
            if shape.Solids:
                # print("setting objects shape")
                shape = self.processSubShapes(obj, shape.Solids[0], pl)
                self.applyShape(obj, shape, pl)
                if hasattr(obj.HorizontalArea, "Value"):
                    if hasattr(obj, "AreaCalculationType"):
                        if obj.AreaCalculationType == "At Center of Mass":
                            a = self.getArea(obj)
                            obj.HorizontalArea = a
                    if hasattr(obj, "Area"):
                        obj.Area = obj.HorizontalArea

                return

        print("Arch: error computing space boundary for", obj.Label)

    def getArea(self, obj, notouch=False):
        "returns the horizontal area at the center of the space"

        self.face = self.getFootprint(obj)
        if self.face:
            if not notouch:
                if hasattr(obj, "PerimeterLength"):
                    if self.face.OuterWire.Length != obj.PerimeterLength.Value:
                        obj.PerimeterLength = self.face.OuterWire.Length
            return self.face.Area
        else:
            return 0

    def getFootprint(self, obj):
        "returns a face that represents the footprint of this space at the center of mass"

        import Part
        import DraftGeomUtils

        if not hasattr(obj.Shape, "CenterOfMass"):
            return None
        try:
            pl = Part.makePlane(1, 1)
            pl.translate(obj.Shape.CenterOfMass)
            sh = obj.Shape.copy()
            cutplane, v1, v2 = ArchCommands.getCutVolume(pl, sh)
            e = sh.section(cutplane)
            e = Part.__sortEdges__(e.Edges)
            w = Part.Wire(e)
            dv = FreeCAD.Vector(
                obj.Shape.CenterOfMass.x, obj.Shape.CenterOfMass.y, obj.Shape.BoundBox.ZMin
            )
            dv = dv.sub(obj.Shape.CenterOfMass)
            w.translate(dv)
            return Part.Face(w)
        except Part.OCCError:
            return None


class _ViewProviderSpace(ArchComponent.ViewProviderComponent):
    "A View Provider for Section Planes"

    def __init__(self, vobj):

        ArchComponent.ViewProviderComponent.__init__(self, vobj)
        self.setProperties(vobj)
        vobj.Transparency = params.get_param_arch("defaultSpaceTransparency")
        vobj.LineWidth = params.get_param_view("DefaultShapeLineWidth")
        vobj.LineColor = ArchCommands.getDefaultColor("Space")
        vobj.DrawStyle = ["Solid", "Dashed", "Dotted", "Dashdot"][
            params.get_param_arch("defaultSpaceStyle")
        ]

    def setProperties(self, vobj):

        pl = vobj.PropertiesList
        if not "Text" in pl:
            vobj.addProperty(
                "App::PropertyStringList",
                "Text",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The text to show. Use $area, $label, $longname, $description or any other property name preceded with $ (case insensitive), or $floor, $walls, $ceiling for finishes, to insert the respective data",
                ),
                locked=True,
            )
            vobj.Text = ["$label", "$area"]
        if not "FontName" in pl:
            vobj.addProperty(
                "App::PropertyFont",
                "FontName",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The name of the font"),
                locked=True,
            )
            vobj.FontName = params.get_param("textfont")
        if not "TextColor" in pl:
            vobj.addProperty(
                "App::PropertyColor",
                "TextColor",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The color of the area text"),
                locked=True,
            )
            vobj.TextColor = (0.0, 0.0, 0.0, 1.0)
        if not "FontSize" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "FontSize",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The size of the text font"),
                locked=True,
            )
            vobj.FontSize = params.get_param("textheight") * params.get_param(
                "DefaultAnnoScaleMultiplier"
            )
        if not "FirstLine" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "FirstLine",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The size of the first line of text"),
                locked=True,
            )
            vobj.FirstLine = params.get_param("textheight") * params.get_param(
                "DefaultAnnoScaleMultiplier"
            )
        if not "LineSpacing" in pl:
            vobj.addProperty(
                "App::PropertyFloat",
                "LineSpacing",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The space between the lines of text"),
                locked=True,
            )
            vobj.LineSpacing = 1.0
        if not "TextPosition" in pl:
            vobj.addProperty(
                "App::PropertyVectorDistance",
                "TextPosition",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The position of the text. Leave (0,0,0) for automatic position",
                ),
                locked=True,
            )
        if not "TextAlign" in pl:
            vobj.addProperty(
                "App::PropertyEnumeration",
                "TextAlign",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "The justification of the text"),
                locked=True,
            )
            vobj.TextAlign = ["Left", "Center", "Right"]
            vobj.TextAlign = "Center"
        if not "Decimals" in pl:
            vobj.addProperty(
                "App::PropertyInteger",
                "Decimals",
                "Space",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The number of decimals to use for calculated texts"
                ),
                locked=True,
            )
            vobj.Decimals = params.get_param("dimPrecision")
        if not "ShowUnit" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowUnit",
                "Space",
                QT_TRANSLATE_NOOP("App::Property", "Show the unit suffix"),
                locked=True,
            )
            vobj.ShowUnit = params.get_param("showUnit")

    def onDocumentRestored(self, vobj):

        self.setProperties(vobj)

    def getIcon(self):

        import Arch_rc

        if hasattr(self, "Object"):
            if hasattr(self.Object, "CloneOf"):
                if self.Object.CloneOf:
                    return ":/icons/Arch_Space_Clone.svg"
        return ":/icons/Arch_Space_Tree.svg"

    def attach(self, vobj):

        ArchComponent.ViewProviderComponent.attach(self, vobj)
        from pivy import coin

        self.color = coin.SoBaseColor()
        self.font = coin.SoFont()
        self.text1 = coin.SoAsciiText()
        self.text1.string = " "
        self.text1.justification = coin.SoAsciiText.LEFT
        self.text2 = coin.SoAsciiText()
        self.text2.string = " "
        self.text2.justification = coin.SoAsciiText.LEFT
        self.coords = coin.SoTransform()
        self.header = coin.SoTransform()
        self.label = coin.SoSwitch()
        sep = coin.SoSeparator()
        self.label.whichChild = 0
        sep.addChild(self.coords)
        sep.addChild(self.color)
        sep.addChild(self.font)
        sep.addChild(self.text2)
        sep.addChild(self.header)
        sep.addChild(self.text1)
        self.label.addChild(sep)
        vobj.Annotation.addChild(self.label)
        self.onChanged(vobj, "TextColor")
        self.onChanged(vobj, "FontSize")
        self.onChanged(vobj, "FirstLine")
        self.onChanged(vobj, "LineSpacing")
        self.onChanged(vobj, "FontName")
        self.Object = vobj.Object
        # footprint mode
        self.fmat = coin.SoMaterial()
        self.fcoords = coin.SoCoordinate3()
        self.fset = coin.SoIndexedFaceSet()
        fhints = coin.SoShapeHints()
        fhints.vertexOrdering = fhints.COUNTERCLOCKWISE
        sep = coin.SoSeparator()
        sep.addChild(self.fmat)
        sep.addChild(self.fcoords)
        sep.addChild(fhints)
        sep.addChild(self.fset)
        vobj.RootNode.addChild(sep)

    def updateData(self, obj, prop):

        if prop in ["Shape", "Label", "Tag", "Area"]:
            self.onChanged(obj.ViewObject, "Text")
            self.onChanged(obj.ViewObject, "TextPosition")

    def getTextPosition(self, vobj):

        pos = FreeCAD.Vector()
        if hasattr(vobj, "TextPosition"):
            import DraftVecUtils

            if DraftVecUtils.isNull(vobj.TextPosition):
                try:
                    pos = vobj.Object.Shape.CenterOfMass
                    z = vobj.Object.Shape.BoundBox.ZMin
                    pos = FreeCAD.Vector(pos.x, pos.y, z)
                except (AttributeError, RuntimeError):
                    pos = FreeCAD.Vector()
            else:
                pos = vobj.Object.Placement.multVec(vobj.TextPosition)
        # placement's displacement will be already added by the coin node
        pos = vobj.Object.Placement.inverse().multVec(pos)
        return pos

    def onChanged(self, vobj, prop):

        if prop in ["Text", "Decimals", "ShowUnit"]:
            if hasattr(self, "text1") and hasattr(self, "text2") and hasattr(vobj, "Text"):
                self.text1.string.deleteValues(0)
                self.text2.string.deleteValues(0)
                text1 = []
                text2 = []
                first = True
                for t in vobj.Text:
                    if t:
                        t = t.replace("$label", vobj.Object.Label)
                        if hasattr(vobj.Object, "Area"):
                            from FreeCAD import Units

                            q = Units.Quantity(
                                vobj.Object.Area.Value, Units.Area
                            ).getUserPreferred()
                            qt = vobj.Object.Area.Value / q[1]
                            if hasattr(vobj, "Decimals"):
                                if vobj.Decimals == 0:
                                    qt = str(int(qt))
                                else:
                                    f = "%." + str(abs(vobj.Decimals)) + "f"
                                    qt = f % qt
                            else:
                                qt = str(qt)
                            if hasattr(vobj, "ShowUnit"):
                                if vobj.ShowUnit:
                                    qt = qt + q[2].replace("^2", "\xb2")  # square symbol
                            t = t.replace("$area", qt)
                        if hasattr(vobj.Object, "FinishFloor"):
                            t = t.replace("$floor", vobj.Object.FinishFloor)
                        if hasattr(vobj.Object, "FinishWalls"):
                            t = t.replace("$walls", vobj.Object.FinishWalls)
                        if hasattr(vobj.Object, "FinishCeiling"):
                            t = t.replace("$ceiling", vobj.Object.FinishCeiling)
                        # replace all other properties
                        props = vobj.Object.PropertiesList
                        lower_props = [p.lower() for p in props]
                        for rtag in re.findall(r"\$\w+", t):
                            lower_rtag = rtag[1:].lower()
                            if lower_rtag in lower_props:
                                prop = props[lower_props.index(lower_rtag)]
                                value = getattr(vobj.Object, prop, "")
                                if hasattr(value, "UserString"):
                                    value = value.UserString
                                elif hasattr(value, "Label"):
                                    value = value.Label
                                elif hasattr(value, "Name"):
                                    value = value.Name
                                t = t.replace(rtag, str(value))
                        if first:
                            text1.append(t)
                        else:
                            text2.append(t)
                    first = False
                if text1:
                    self.text1.string.setValues(text1)
                if text2:
                    self.text2.string.setValues(text2)

        elif prop == "FontName":
            if hasattr(self, "font") and hasattr(vobj, "FontName"):
                self.font.name = str(vobj.FontName)

        elif prop == "FontSize":
            if hasattr(self, "font") and hasattr(vobj, "FontSize"):
                self.font.size = vobj.FontSize.Value
                if hasattr(vobj, "FirstLine"):
                    scale = vobj.FirstLine.Value / vobj.FontSize.Value
                    self.header.scaleFactor.setValue([scale, scale, scale])
                    self.onChanged(vobj, "TextPosition")

        elif prop == "FirstLine":
            if hasattr(self, "header") and hasattr(vobj, "FontSize") and hasattr(vobj, "FirstLine"):
                scale = vobj.FirstLine.Value / vobj.FontSize.Value
                self.header.scaleFactor.setValue([scale, scale, scale])
                self.onChanged(vobj, "TextPosition")

        elif prop == "TextColor":
            if hasattr(self, "color") and hasattr(vobj, "TextColor"):
                c = vobj.TextColor
                self.color.rgb.setValue(c[0], c[1], c[2])

        elif prop == "TextPosition":
            if (
                hasattr(self, "coords")
                and hasattr(self, "header")
                and hasattr(vobj, "TextPosition")
                and hasattr(vobj, "FirstLine")
            ):
                pos = self.getTextPosition(vobj)
                self.coords.translation.setValue(
                    [pos.x, pos.y, pos.z + 0.01]
                )  # adding small z offset to separate from bottom face
                up = vobj.FirstLine.Value * vobj.LineSpacing
                self.header.translation.setValue([0, up, 0])

        elif prop == "LineSpacing":
            if hasattr(self, "text1") and hasattr(self, "text2") and hasattr(vobj, "LineSpacing"):
                self.text1.spacing = vobj.LineSpacing
                self.text2.spacing = vobj.LineSpacing
                self.onChanged(vobj, "TextPosition")

        elif prop == "TextAlign":
            if hasattr(self, "text1") and hasattr(self, "text2") and hasattr(vobj, "TextAlign"):
                from pivy import coin

                if vobj.TextAlign == "Center":
                    self.text1.justification = coin.SoAsciiText.CENTER
                    self.text2.justification = coin.SoAsciiText.CENTER
                elif vobj.TextAlign == "Right":
                    self.text1.justification = coin.SoAsciiText.RIGHT
                    self.text2.justification = coin.SoAsciiText.RIGHT
                else:
                    self.text1.justification = coin.SoAsciiText.LEFT
                    self.text2.justification = coin.SoAsciiText.LEFT

        elif prop == "Visibility":
            if vobj.Visibility:
                self.label.whichChild = 0
            else:
                self.label.whichChild = -1

        elif prop == "Transparency":
            if hasattr(vobj, "DisplayMode"):
                vobj.DisplayMode = "Wireframe" if vobj.Transparency == 100 else "Flat Lines"

    def setEdit(self, vobj, mode):
        if mode != 0:
            return None

        taskd = SpaceTaskPanel()
        taskd.obj = self.Object
        taskd.update()
        taskd.updateBoundaries()
        FreeCADGui.Control.showDialog(taskd)
        return True

    def getDisplayModes(self, vobj):

        modes = ArchComponent.ViewProviderComponent.getDisplayModes(self, vobj) + ["Footprint"]
        return modes

    def setDisplayMode(self, mode):

        self.fset.coordIndex.deleteValues(0)
        self.fcoords.point.deleteValues(0)
        if mode == "Footprint":
            if hasattr(self, "Object"):
                face = self.Object.Proxy.getFootprint(self.Object)
                if face:
                    verts = []
                    fdata = []
                    idx = 0
                    tri = face.tessellate(1)
                    for v in tri[0]:
                        verts.append([v.x, v.y, v.z])
                    for f in tri[1]:
                        fdata.extend([f[0] + idx, f[1] + idx, f[2] + idx, -1])
                    idx += len(tri[0])
                    self.fcoords.point.setValues(verts)
                    self.fset.coordIndex.setValues(0, len(fdata), fdata)
            return "Points"
        return ArchComponent.ViewProviderComponent.setDisplayMode(self, mode)


class SpaceTaskPanel(ArchComponent.ComponentTaskPanel):
    "A modified version of the Arch component task panel"

    def __init__(self):

        ArchComponent.ComponentTaskPanel.__init__(self)
        self.editButton = QtGui.QPushButton(self.form)
        self.editButton.setObjectName("editButton")
        self.editButton.setIcon(QtGui.QIcon(":/icons/Draft_Edit.svg"))
        self.grid.addWidget(self.editButton, 4, 0, 1, 2)
        self.editButton.setText(QtGui.QApplication.translate("Arch", "Set text position", None))
        QtCore.QObject.connect(self.editButton, QtCore.SIGNAL("clicked()"), self.setTextPos)
        boundLabel = QtGui.QLabel(self.form)
        self.grid.addWidget(boundLabel, 5, 0, 1, 2)
        boundLabel.setText(QtGui.QApplication.translate("Arch", "Space boundaries", None))
        self.boundList = QtGui.QListWidget(self.form)
        self.grid.addWidget(self.boundList, 6, 0, 1, 2)
        self.addCompButton = QtGui.QPushButton(self.form)
        self.addCompButton.setObjectName("addCompButton")
        self.addCompButton.setIcon(QtGui.QIcon(":/icons/Arch_Add.svg"))
        self.grid.addWidget(self.addCompButton, 7, 0, 1, 1)
        self.addCompButton.setText(QtGui.QApplication.translate("Arch", "Add", None))
        QtCore.QObject.connect(self.addCompButton, QtCore.SIGNAL("clicked()"), self.addBoundary)
        self.delCompButton = QtGui.QPushButton(self.form)
        self.delCompButton.setObjectName("delCompButton")
        self.delCompButton.setIcon(QtGui.QIcon(":/icons/Arch_Remove.svg"))
        self.grid.addWidget(self.delCompButton, 7, 1, 1, 1)
        self.delCompButton.setText(QtGui.QApplication.translate("Arch", "Remove", None))
        QtCore.QObject.connect(self.delCompButton, QtCore.SIGNAL("clicked()"), self.delBoundary)

    def updateBoundaries(self):

        self.boundList.clear()
        if self.obj:
            for b in self.obj.Boundaries:
                s = b[0].Label
                for n in b[1]:
                    s += ", " + n
                it = QtGui.QListWidgetItem(s)
                it.setToolTip(b[0].Name)
                self.boundList.addItem(it)

    def setTextPos(self):

        FreeCADGui.runCommand("Draft_Edit")

    def addBoundary(self):

        if self.obj:
            if FreeCADGui.Selection.getSelectionEx():
                self.obj.Proxy.addSubobjects(self.obj, FreeCADGui.Selection.getSelectionEx())
                self.updateBoundaries()

    def delBoundary(self):

        if self.boundList.currentRow() >= 0:
            it = self.boundList.item(self.boundList.currentRow())
            if it and self.obj:
                on = it.toolTip()
                bounds = self.obj.Boundaries
                for b in bounds:
                    if b[0].Name == on:
                        bounds.remove(b)
                        break
                self.obj.Boundaries = bounds
                self.updateBoundaries()
