File size: 8,616 Bytes
26f7fa0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cebb92d
26f7fa0
 
cebb92d
26f7fa0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
"""
MLSTRUCT-FP - DB - CFLOOR

Floor component, container of objects.
"""

__all__ = ['Floor']

from MLStructFP.utils import GeomPoint2D, BoundingBox
from MLStructFP._types import Dict, Tuple, Optional, TYPE_CHECKING, NumberType, NumberInstance

import math
import os
import plotly.graph_objects as go

if TYPE_CHECKING:
    from MLStructFP.db._c import BaseComponent
    from MLStructFP.db._c_rect import Rect
    from MLStructFP.db._c_point import Point
    from MLStructFP.db._c_slab import Slab
    from MLStructFP.db._c_room import Room
    from MLStructFP.db._c_item import Item


class Floor(object):
    """
    FP Floor.
    """
    _bb: Optional['BoundingBox']
    _last_mutation: Optional[Dict[str, float]]
    _rect: Dict[int, 'Rect']  # id => rect
    _point: Dict[int, 'Point']  # id => point
    _slab: Dict[int, 'Slab']  # id => slab
    _room: Dict[int, 'Room']  # id => room
    _item: Dict[int, 'Item']  # id => item
    category: int
    category_name: str
    elevation: bool
    id: int
    image_path: str
    image_scale: float
    project_id: int
    project_label: str

    def __init__(self, floor_id: int, image_path: str, image_scale: NumberType,
                 project_id: int, project_label: str = '', category: int = 0, category_name: str = '',
                 elevation: bool = False) -> None:
        """
        Constructor.

        :param floor_id: Floor ID
        :param image_path: Image path
        :param image_scale: Image scale (px to units)
        :param project_id: Project ID
        :param project_label: Project label (default empty)
        :param category: Project category
        :param category_name: Project category name
        :param elevation: Elevation mode
        """
        assert isinstance(floor_id, int) and floor_id > 0
        assert os.path.isfile(image_path), f'Image file {image_path} does not exist'
        assert isinstance(image_scale, NumberInstance) and image_scale > 0
        assert isinstance(elevation, bool)
        self.category = category
        self.category_name = category_name
        self.elevation = elevation
        self.id = floor_id
        self.image_path = image_path.replace('\\', '/')
        self.image_scale = float(image_scale)
        self.project_id = project_id
        self.project_label = project_label
        self._bb = None
        self._last_mutation = None
        # Object containers
        self._rect = {}
        self._point = {}
        self._slab = {}
        self._room = {}
        self._item = {}

    @property
    def rect(self) -> Tuple['Rect', ...]:
        # noinspection PyTypeChecker
        return tuple(self._rect.values())

    @property
    def point(self) -> Tuple['Point', ...]:
        # noinspection PyTypeChecker
        return tuple(self._point.values())

    @property
    def slab(self) -> Tuple['Slab', ...]:
        # noinspection PyTypeChecker
        return tuple(self._slab.values())

    @property
    def room(self) -> Tuple['Room', ...]:
        # noinspection PyTypeChecker
        return tuple(self._room.values())

    @property
    def item(self) -> Tuple['Item', ...]:
        # noinspection PyTypeChecker
        return tuple(self._item.values())

    def plot_basic(self) -> 'go.Figure':
        """
        Plot basic objects.
    
        :return: Go figure object
        """
        return self.plot_complex(fill=False)

    def plot_complex(
            self,
            fill: bool = True,
            draw_rect: bool = True,
            draw_point: bool = True,
            draw_slab: bool = True,
            draw_room: bool = True,
            draw_item: bool = True,
            **kwargs
    ) -> 'go.Figure':
        """
        Complex plot.

        :param fill: Fill figure
        :param draw_rect: Draw wall rects
        :param draw_point: Draw points
        :param draw_slab: Draw slabs
        :param draw_room: Draw rooms
        :param draw_item: Draw items
        :param kwargs: Optional keyword arguments
        """
        fig = go.Figure()

        if draw_slab:
            for s in self.slab:
                s.plot_plotly(
                    fig=fig,
                    fill=fill,
                    **kwargs
                )
        if draw_room:
            for r in self.room:
                r.plot_plotly(
                    fig=fig,
                    fill=fill,
                    **kwargs
                )
        if draw_item:
            for i in self.item:
                i.plot_plotly(
                    fig=fig,
                    fill=fill,
                    **kwargs
                )
        if draw_rect:
            for wr in self.rect:
                wr.plot_plotly(
                    fig=fig,
                    fill=fill,
                    **kwargs
                )
        if draw_point:
            for wp in self.point:
                wp.plot_plotly(
                    fig=fig,
                    fill=fill,
                    **kwargs
                )

        grid_color = kwargs.get('plot_gridcolor', '#d7d7d7')
        fig.update_layout(
            plot_bgcolor=kwargs.get('plot_bgcolor', '#ffffff'),
            showlegend=kwargs.get('show_legend', True),
            title=f'Floor - ID {self.id}',
            font=dict(
                size=kwargs.get('font_size', 14),
            ),
            yaxis_zeroline=False,
            xaxis_zeroline=False,
            xaxis=dict(
                gridcolor=grid_color
            ),
            yaxis=dict(
                gridcolor=grid_color,
                scaleanchor='x',
                scaleratio=1
            )
        )
        show_grid = kwargs.get('show_grid', True)
        fig.update_layout(xaxis_showgrid=show_grid, yaxis_showgrid=show_grid)
        fig.update_xaxes(title_text='x (m)', hoverformat='.3f')
        fig.update_yaxes(title_text='y (m)', hoverformat='.3f')
        return fig

    def mutate(self, angle: NumberType = 0, sx: NumberType = 1, sy: NumberType = 1,
               scale_first: bool = True) -> 'Floor':
        """
        Apply mutator for each object within the floor.

        :param angle: Angle
        :param sx: Scale on x-axis
        :param sy: Scale on y-axis
        :param scale_first: Scale first, then rotate
        :return: Floor reference
        """
        assert isinstance(angle, NumberInstance)
        assert isinstance(sx, NumberInstance) and sx != 0
        assert isinstance(sy, NumberInstance) and sy != 0

        # Undo last mutation
        if self._last_mutation is not None:
            _angle, _sx, _sy = self.mutator_angle, self.mutator_scale_x, self.mutator_scale_y
            self._last_mutation = None  # Reset mutation
            self.mutate(-_angle, 1 / _sx, 1 / _sy, scale_first=False)  # Reverse operation

        # Apply mutation
        rotation_center = GeomPoint2D()
        o: Tuple['BaseComponent']
        for o in (self.rect, self.point, self.slab, self.room, self.item):
            for c in o:
                for p in c.points:
                    if not scale_first:
                        p.rotate(rotation_center, angle)
                    p.x *= sx
                    p.y *= sy
                    if scale_first:
                        p.rotate(rotation_center, angle)

        # Update mutation
        self._bb = None
        self._last_mutation = {
            'angle': angle,
            'sx': sx,
            'sy': sy
        }

        return self

    @property
    def mutator_angle(self) -> float:
        return float(0 if self._last_mutation is None else self._last_mutation['angle'])

    @property
    def mutator_scale_x(self) -> float:
        return float(1 if self._last_mutation is None else self._last_mutation['sx'])

    @property
    def mutator_scale_y(self) -> float:
        return float(1 if self._last_mutation is None else self._last_mutation['sy'])

    @property
    def bounding_box(self) -> 'BoundingBox':
        """
        :return: Return the bounding box of the floor, calculated using the slab and the points from the rects.
        """
        if self._bb is not None:
            return self._bb
        xmin = math.inf
        xmax = -math.inf
        ymin = math.inf
        ymax = -math.inf
        o: Tuple['BaseComponent']
        for o in (self.rect, self.slab):
            for c in o:
                for p in c.points:
                    xmin = min(xmin, p.x)
                    xmax = max(xmax, p.x)
                    ymin = min(ymin, p.y)
                    ymax = max(ymax, p.y)
        self._bb = BoundingBox(xmin, xmax, ymin, ymax)
        return self._bb