4e57c75e |
1 | // Created by: Peter KURNEV |
973c2be1 |
2 | // Copyright (c) 2010-2014 OPEN CASCADE SAS |
4e57c75e |
3 | // Copyright (c) 2007-2010 CEA/DEN, EDF R&D, OPEN CASCADE |
4 | // Copyright (c) 2003-2007 OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN, CEDRAT, |
5 | // EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS |
6 | // |
973c2be1 |
7 | // This file is part of Open CASCADE Technology software library. |
4e57c75e |
8 | // |
d5f74e42 |
9 | // This library is free software; you can redistribute it and/or modify it under |
10 | // the terms of the GNU Lesser General Public License version 2.1 as published |
973c2be1 |
11 | // by the Free Software Foundation, with special exception defined in the file |
12 | // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT |
13 | // distribution for complete text of the license and disclaimer of any warranty. |
4e57c75e |
14 | // |
973c2be1 |
15 | // Alternatively, this file may be used under the terms of Open CASCADE |
16 | // commercial license or contractual agreement. |
4e57c75e |
17 | |
4e57c75e |
18 | |
42cf5bc1 |
19 | #include <BOPAlgo_PaveFiller.hxx> |
8ae442a8 |
20 | #include <BOPAlgo_Tools.hxx> |
505abfb8 |
21 | #include <BOPCol_NCVector.hxx> |
c7b59798 |
22 | #include <BOPCol_Parallel.hxx> |
42cf5bc1 |
23 | #include <BOPDS_DS.hxx> |
4e57c75e |
24 | #include <BOPDS_Interf.hxx> |
42cf5bc1 |
25 | #include <BOPDS_Iterator.hxx> |
25dfc507 |
26 | #include <BOPDS_Pair.hxx> |
42cf5bc1 |
27 | #include <BOPDS_PaveBlock.hxx> |
28 | #include <BOPDS_VectorOfInterfVE.hxx> |
3510db62 |
29 | #include <BOPTools_AlgoTools.hxx> |
42cf5bc1 |
30 | #include <BRep_Tool.hxx> |
42cf5bc1 |
31 | #include <gp_Pnt.hxx> |
32 | #include <IntTools_Context.hxx> |
8ae442a8 |
33 | #include <Precision.hxx> |
34 | #include <TopoDS.hxx> |
42cf5bc1 |
35 | #include <TopoDS_Edge.hxx> |
42cf5bc1 |
36 | #include <TopoDS_Vertex.hxx> |
4e57c75e |
37 | |
505abfb8 |
38 | //======================================================================= |
8ae442a8 |
39 | //class : BOPAlgo_VertexEdge |
505abfb8 |
40 | //purpose : |
41 | //======================================================================= |
36f4947b |
42 | class BOPAlgo_VertexEdge : public BOPAlgo_Algo { |
43 | |
505abfb8 |
44 | public: |
36f4947b |
45 | DEFINE_STANDARD_ALLOC |
46 | |
47 | BOPAlgo_VertexEdge() : |
48 | BOPAlgo_Algo(), |
0d0481c7 |
49 | myIV(-1), myIE(-1), myFlag(-1), myT(-1.), myTolVNew(-1.) { |
505abfb8 |
50 | }; |
51 | // |
36f4947b |
52 | virtual ~BOPAlgo_VertexEdge(){ |
505abfb8 |
53 | }; |
54 | // |
55 | void SetIndices(const Standard_Integer nV, |
0d0481c7 |
56 | const Standard_Integer nE) { |
505abfb8 |
57 | myIV=nV; |
58 | myIE=nE; |
505abfb8 |
59 | } |
60 | // |
61 | void Indices(Standard_Integer& nV, |
0d0481c7 |
62 | Standard_Integer& nE) const { |
505abfb8 |
63 | nV=myIV; |
64 | nE=myIE; |
505abfb8 |
65 | } |
66 | // |
67 | void SetVertex(const TopoDS_Vertex& aV) { |
68 | myV=aV; |
69 | } |
70 | // |
505abfb8 |
71 | void SetEdge(const TopoDS_Edge& aE) { |
72 | myE=aE; |
73 | } |
74 | // |
3510db62 |
75 | const TopoDS_Vertex& Vertex() const { |
76 | return myV; |
77 | } |
78 | // |
79 | const TopoDS_Edge& Edge() const { |
505abfb8 |
80 | return myE; |
81 | } |
82 | // |
83 | Standard_Integer Flag()const { |
84 | return myFlag; |
85 | } |
86 | // |
87 | Standard_Real Parameter()const { |
88 | return myT; |
89 | } |
90 | // |
3510db62 |
91 | Standard_Real VertexNewTolerance()const { |
92 | return myTolVNew; |
93 | } |
94 | // |
1e143abb |
95 | void SetContext(const Handle(IntTools_Context)& aContext) { |
505abfb8 |
96 | myContext=aContext; |
97 | } |
98 | // |
1e143abb |
99 | const Handle(IntTools_Context)& Context()const { |
505abfb8 |
100 | return myContext; |
101 | } |
102 | // |
8ae442a8 |
103 | void SetPaveBlock(const Handle(BOPDS_PaveBlock)& thePB) { |
104 | myPB = thePB; |
105 | } |
106 | // |
107 | const Handle(BOPDS_PaveBlock)& PaveBlock() const { |
108 | return myPB; |
109 | } |
110 | // |
36f4947b |
111 | virtual void Perform() { |
112 | BOPAlgo_Algo::UserBreak(); |
0d0481c7 |
113 | myFlag=myContext->ComputeVE (myV, myE, myT, myTolVNew, myFuzzyValue); |
505abfb8 |
114 | }; |
115 | // |
116 | protected: |
117 | Standard_Integer myIV; |
118 | Standard_Integer myIE; |
505abfb8 |
119 | Standard_Integer myFlag; |
120 | Standard_Real myT; |
3510db62 |
121 | Standard_Real myTolVNew; |
505abfb8 |
122 | TopoDS_Vertex myV; |
123 | TopoDS_Edge myE; |
1e143abb |
124 | Handle(IntTools_Context) myContext; |
8ae442a8 |
125 | Handle(BOPDS_PaveBlock) myPB; |
505abfb8 |
126 | }; |
127 | //======================================================================= |
128 | typedef BOPCol_NCVector |
129 | <BOPAlgo_VertexEdge> BOPAlgo_VectorOfVertexEdge; |
130 | // |
c7b59798 |
131 | typedef BOPCol_ContextFunctor |
505abfb8 |
132 | <BOPAlgo_VertexEdge, |
133 | BOPAlgo_VectorOfVertexEdge, |
1e143abb |
134 | Handle(IntTools_Context), |
135 | IntTools_Context> BOPAlgo_VertexEdgeFunctor; |
505abfb8 |
136 | // |
c7b59798 |
137 | typedef BOPCol_ContextCnt |
505abfb8 |
138 | <BOPAlgo_VertexEdgeFunctor, |
139 | BOPAlgo_VectorOfVertexEdge, |
1e143abb |
140 | Handle(IntTools_Context)> BOPAlgo_VertexEdgeCnt; |
505abfb8 |
141 | // |
4e57c75e |
142 | //======================================================================= |
143 | // function: PerformVE |
144 | // purpose: |
145 | //======================================================================= |
505abfb8 |
146 | void BOPAlgo_PaveFiller::PerformVE() |
4e57c75e |
147 | { |
3510db62 |
148 | FillShrunkData(TopAbs_VERTEX, TopAbs_EDGE); |
149 | // |
4e57c75e |
150 | myIterator->Initialize(TopAbs_VERTEX, TopAbs_EDGE); |
8ae442a8 |
151 | Standard_Integer iSize = myIterator->ExpectedLength(); |
4e57c75e |
152 | if (!iSize) { |
153 | return; |
154 | } |
155 | // |
8ae442a8 |
156 | // Prepare pairs for intersection |
157 | BOPDS_IndexedDataMapOfPaveBlockListOfInteger aMVEPairs; |
4e57c75e |
158 | for (; myIterator->More(); myIterator->Next()) { |
8ae442a8 |
159 | Standard_Integer nV, nE; |
25dfc507 |
160 | myIterator->Value(nV, nE); |
4e57c75e |
161 | // |
162 | const BOPDS_ShapeInfo& aSIE=myDS->ShapeInfo(nE); |
163 | if (aSIE.HasSubShape(nV)) { |
164 | continue; |
165 | } |
166 | // |
167 | if (aSIE.HasFlag()){ |
168 | continue; |
169 | } |
170 | // |
171 | if (myDS->HasInterfShapeSubShapes(nV, nE)) { |
4e57c75e |
172 | continue; |
173 | } |
174 | // |
3510db62 |
175 | const BOPDS_ListOfPaveBlock& aLPB = myDS->PaveBlocks(nE); |
01b5b3df |
176 | if (aLPB.IsEmpty()) { |
177 | continue; |
178 | } |
179 | // |
180 | const Handle(BOPDS_PaveBlock)& aPB = aLPB.First(); |
181 | if (!aPB->IsSplittable()) { |
3510db62 |
182 | // this is a micro edge, ignore it |
183 | continue; |
184 | } |
185 | // |
8ae442a8 |
186 | BOPCol_ListOfInteger* pLV = aMVEPairs.ChangeSeek(aPB); |
187 | if (!pLV) |
188 | pLV = &aMVEPairs(aMVEPairs.Add(aPB, BOPCol_ListOfInteger())); |
189 | pLV->Append(nV); |
190 | } |
191 | // |
192 | IntersectVE(aMVEPairs); |
193 | } |
194 | |
195 | //======================================================================= |
196 | // function: IntersectVE |
197 | // purpose: |
198 | //======================================================================= |
199 | void BOPAlgo_PaveFiller::IntersectVE |
200 | (const BOPDS_IndexedDataMapOfPaveBlockListOfInteger& theVEPairs, |
201 | const Standard_Boolean theAddInterfs) |
202 | { |
203 | Standard_Integer i, aNbVE = theVEPairs.Extent(); |
204 | if (!aNbVE) { |
205 | return; |
206 | } |
207 | // |
208 | BOPDS_VectorOfInterfVE& aVEs = myDS->InterfVE(); |
209 | if (theAddInterfs) { |
210 | aVEs.SetIncrement(aNbVE); |
211 | } |
212 | // |
213 | // Prepare for intersection. |
214 | BOPAlgo_VectorOfVertexEdge aVVE; |
215 | // Map to collect all SD connections to add interferences |
216 | // for all vertices having the same SD vertex. |
217 | // It will also be used as a Fence map to avoid repeated |
218 | // intersection of the same SD vertex with edge |
219 | NCollection_DataMap<BOPDS_Pair, BOPCol_ListOfInteger, BOPDS_PairMapHasher> aDMVSD; |
220 | // |
221 | for (i = 1; i <= aNbVE; ++i) { |
222 | const Handle(BOPDS_PaveBlock)& aPB = theVEPairs.FindKey(i); |
223 | Standard_Integer nE = aPB->OriginalEdge(); |
505abfb8 |
224 | // |
8ae442a8 |
225 | const BOPCol_ListOfInteger& aLV = theVEPairs(i); |
226 | BOPCol_ListIteratorOfListOfInteger aItLV(aLV); |
227 | for (; aItLV.More(); aItLV.Next()) { |
228 | Standard_Integer nV = aItLV.Value(); |
229 | // |
230 | Standard_Integer nVSD = nV; |
231 | myDS->HasShapeSD(nV, nVSD); |
232 | // |
233 | BOPDS_Pair aPair(nVSD, nE); |
234 | BOPCol_ListOfInteger* pLI = aDMVSD.ChangeSeek(aPair); |
235 | if (pLI) { |
236 | // Already added |
237 | pLI->Append(nV); |
238 | continue; |
239 | } |
240 | // New pair |
241 | pLI = aDMVSD.Bound(aPair, BOPCol_ListOfInteger()); |
242 | pLI->Append(nV); |
243 | // |
244 | const TopoDS_Vertex& aV = TopoDS::Vertex(myDS->Shape(nVSD)); |
245 | const TopoDS_Edge& aE = TopoDS::Edge(myDS->Shape(nE)); |
246 | // |
247 | BOPAlgo_VertexEdge& aVESolver = aVVE.Append1(); |
248 | aVESolver.SetIndices(nVSD, nE); |
249 | aVESolver.SetVertex(aV); |
250 | aVESolver.SetEdge(aE); |
251 | aVESolver.SetPaveBlock(aPB); |
252 | aVESolver.SetFuzzyValue(myFuzzyValue); |
253 | aVESolver.SetProgressIndicator(myProgressIndicator); |
254 | } |
255 | } |
505abfb8 |
256 | // |
8ae442a8 |
257 | // Perform intersection |
505abfb8 |
258 | //============================================================= |
259 | BOPAlgo_VertexEdgeCnt::Perform(myRunParallel, aVVE, myContext); |
260 | //============================================================= |
261 | // |
8ae442a8 |
262 | // Keep the modified edges for further update |
263 | BOPCol_MapOfInteger aMEdges; |
264 | // |
265 | // Analyze intersections |
266 | aNbVE = aVVE.Extent(); |
267 | for (i = 0; i < aNbVE; ++i) { |
268 | const BOPAlgo_VertexEdge& aVESolver = aVVE(i); |
269 | if (aVESolver.Flag() != 0) { |
270 | continue; |
271 | } |
272 | // |
273 | Standard_Integer nV, nE; |
274 | aVESolver.Indices(nV, nE); |
275 | // Parameter of vertex on edge |
276 | Standard_Real aT = aVESolver.Parameter(); |
277 | // 1. Update vertex V/E if necessary |
278 | Standard_Real aTolVNew = aVESolver.VertexNewTolerance(); |
279 | Standard_Integer nVx = UpdateVertex(nV, aTolVNew); |
280 | // 2. Create new pave and add it as extra pave to pave block |
281 | // for further splitting of the edge |
282 | const Handle(BOPDS_PaveBlock)& aPB = aVESolver.PaveBlock(); |
283 | BOPDS_Pave aPave; |
284 | aPave.SetIndex(nVx); |
285 | aPave.SetParameter(aT); |
286 | aPB->AppendExtPave(aPave); |
287 | aMEdges.Add(nE); |
288 | // |
289 | if (theAddInterfs) { |
290 | // Add interferences into DS |
291 | BOPDS_Pair aPair(nV, nE); |
292 | const BOPCol_ListOfInteger& aLI = aDMVSD.Find(aPair); |
293 | BOPCol_ListIteratorOfListOfInteger aItLI(aLI); |
294 | for (; aItLI.More(); aItLI.Next()) { |
295 | const Standard_Integer nVOld = aItLI.Value(); |
296 | // 3. Create interference V/E |
297 | BOPDS_InterfVE& aVE = aVEs.Append1(); |
298 | aVE.SetIndices(nVOld, nE); |
299 | aVE.SetParameter(aT); |
300 | // 2. Add a pair in the whole table of interferences |
301 | myDS->AddInterf(nVOld, nE); |
302 | // 4. Set index of new vertex in the interference |
303 | if (myDS->IsNewShape(nVx)) { |
304 | aVE.SetIndexNew(nVx); |
3510db62 |
305 | } |
306 | } |
8ae442a8 |
307 | } |
308 | } |
309 | // |
310 | // Split pave blocks of the intersected edges with the extra paves. |
311 | // At the same time compute shrunk data for the new pave blocks |
312 | // and in case there is no valid range for the pave block, |
313 | // the vertices of this pave block should be unified. |
314 | SplitPaveBlocks(aMEdges, theAddInterfs); |
315 | } |
316 | |
317 | //======================================================================= |
318 | // function: MakeNewCommonBlock |
319 | // purpose: Make new Common Block from the given list of Pave Blocks |
320 | //======================================================================= |
321 | static |
322 | void MakeNewCommonBlock(const BOPDS_ListOfPaveBlock& theLPB, |
323 | const BOPCol_ListOfInteger& theLFaces, |
324 | BOPDS_PDS& theDS) |
325 | { |
326 | // Make Common Block from the pave blocks in the list |
327 | Handle(BOPDS_CommonBlock) aCBNew = new BOPDS_CommonBlock; |
328 | aCBNew->SetPaveBlocks(theLPB); |
329 | aCBNew->SetFaces(theLFaces); |
330 | // |
331 | BOPDS_ListIteratorOfListOfPaveBlock aItLPB(theLPB); |
332 | for (; aItLPB.More(); aItLPB.Next()) { |
333 | theDS->SetCommonBlock(aItLPB.ChangeValue(), aCBNew); |
334 | } |
335 | } |
336 | |
337 | //======================================================================= |
338 | // function: SplitPaveBlocks |
339 | // purpose: |
340 | //======================================================================= |
341 | void BOPAlgo_PaveFiller::SplitPaveBlocks(const BOPCol_MapOfInteger& theMEdges, |
342 | const Standard_Boolean theAddInterfs) |
343 | { |
344 | // Fence map to avoid unification of the same vertices twice |
345 | BOPDS_MapOfPair aMPairs; |
346 | // Map to treat the Common Blocks |
347 | NCollection_IndexedDataMap<Handle(BOPDS_CommonBlock), |
348 | BOPDS_ListOfPaveBlock, |
349 | TColStd_MapTransientHasher> aMCBNewPB; |
350 | // |
351 | BOPCol_MapIteratorOfMapOfInteger aItM(theMEdges); |
352 | for (; aItM.More(); aItM.Next()) { |
353 | Standard_Integer nE = aItM.Value(); |
354 | BOPDS_ListOfPaveBlock& aLPB = myDS->ChangePaveBlocks(nE); |
355 | // |
356 | BOPDS_ListIteratorOfListOfPaveBlock aItLPB(aLPB); |
357 | for (; aItLPB.More();) { |
358 | Handle(BOPDS_PaveBlock)& aPB = aItLPB.ChangeValue(); |
359 | // |
360 | if (!aPB->IsToUpdate()) { |
361 | aItLPB.Next(); |
3510db62 |
362 | continue; |
8ae442a8 |
363 | } |
364 | // |
365 | const Handle(BOPDS_CommonBlock)& aCB = myDS->CommonBlock(aPB); |
366 | // |
367 | // Compute new pave blocks |
368 | BOPDS_ListOfPaveBlock aLPBN; |
369 | aPB->Update(aLPBN); |
370 | // |
371 | // Make sure that each new pave block has a valid range, |
372 | // otherwise unify the vertices of the pave block |
373 | BOPDS_ListIteratorOfListOfPaveBlock aItLPBN(aLPBN); |
374 | for (; aItLPBN.More(); aItLPBN.Next()) { |
375 | Handle(BOPDS_PaveBlock)& aPBN = aItLPBN.ChangeValue(); |
376 | myDS->UpdatePaveBlockWithSDVertices(aPBN); |
377 | FillShrunkData(aPBN); |
378 | // |
379 | if (!aPBN->HasShrunkData()) { |
380 | // No valid range, unify vertices |
381 | Standard_Integer nV1, nV2; |
382 | aPBN->Indices(nV1, nV2); |
383 | if (nV1 != nV2) { |
384 | BOPDS_Pair aPair; |
385 | aPair.SetIndices(nV1, nV2); |
386 | if (aMPairs.Add(aPair)) { |
387 | BOPCol_ListOfInteger aLV; |
388 | aLV.Append(nV1); |
389 | aLV.Append(nV2); |
390 | MakeSDVertices(aLV, theAddInterfs); |
391 | } |
392 | } |
393 | continue; |
394 | } |
395 | // |
396 | // Update the list with new pave block |
397 | aLPB.Append(aPBN); |
398 | // Treat the common block |
399 | if (!aCB.IsNull()) { |
400 | // Store the new pave block to make new common block |
401 | BOPDS_ListOfPaveBlock* pLPBCB = aMCBNewPB.ChangeSeek(aCB); |
402 | if (!pLPBCB) { |
403 | pLPBCB = &aMCBNewPB(aMCBNewPB.Add(aCB, BOPDS_ListOfPaveBlock())); |
404 | } |
405 | pLPBCB->Append(aPBN); |
406 | } |
407 | } |
408 | // Remove old pave block |
409 | aLPB.Remove(aItLPB); |
410 | } |
411 | } |
412 | // |
413 | // Make Common Blocks |
414 | Standard_Integer i, aNbCB = aMCBNewPB.Extent(); |
415 | for (i = 1; i <= aNbCB; ++i) { |
416 | const Handle(BOPDS_CommonBlock)& aCB = aMCBNewPB.FindKey(i); |
417 | const BOPDS_ListOfPaveBlock& aLPBN = aMCBNewPB(i); |
418 | // |
419 | // For each group of pave blocks with the same vertices make new common block |
420 | NCollection_IndexedDataMap<BOPDS_Pair, BOPDS_ListOfPaveBlock, BOPDS_PairMapHasher> aMInds; |
421 | BOPDS_ListIteratorOfListOfPaveBlock aItLPB(aLPBN); |
422 | for (; aItLPB.More(); aItLPB.Next()) { |
423 | const Handle(BOPDS_PaveBlock)& aPB = aItLPB.Value(); |
3510db62 |
424 | // |
8ae442a8 |
425 | BOPDS_Pair aPair; |
426 | aPair.SetIndices(aPB->Pave1().Index(), aPB->Pave2().Index()); |
3510db62 |
427 | // |
8ae442a8 |
428 | BOPDS_ListOfPaveBlock* pLPBx = aMInds.ChangeSeek(aPair); |
429 | if (!pLPBx) { |
430 | pLPBx = &aMInds(aMInds.Add(aPair, BOPDS_ListOfPaveBlock())); |
3510db62 |
431 | } |
8ae442a8 |
432 | pLPBx->Append(aPB); |
4e57c75e |
433 | } |
8ae442a8 |
434 | // |
435 | Standard_Integer nV1, nV2; |
436 | aCB->PaveBlock1()->Indices(nV1, nV2); |
437 | Standard_Boolean bIsClosed = (nV1 == nV2); |
438 | // |
439 | Standard_Integer j, aNbPairs = aMInds.Extent(); |
440 | for (j = 1; j <= aNbPairs; ++j) { |
441 | BOPDS_ListOfPaveBlock& aLPB = aMInds(j); |
442 | // |
443 | if (!bIsClosed) { |
444 | // Make Common Block from the pave blocks in the list |
445 | MakeNewCommonBlock(aLPB, aCB->Faces(), myDS); |
446 | continue; |
447 | } |
448 | // |
449 | // Find coinciding pave blocks |
450 | while (aLPB.Extent()) { |
451 | // Pave blocks forming the common block |
452 | BOPDS_ListOfPaveBlock aLPBCB; |
453 | // Point in the middle of the first pave block in the common block |
454 | gp_Pnt aPMFirst(0., 0., 0.); |
455 | // Tolerance of the first edge in the common block |
456 | Standard_Real aTolEFirst = 0.; |
457 | // |
458 | aItLPB.Initialize(aLPB); |
459 | for (; aItLPB.More();) { |
460 | const Handle(BOPDS_PaveBlock)& aPB = aItLPB.Value(); |
461 | if (aLPBCB.IsEmpty()) { |
462 | aLPBCB.Append(aPB); |
463 | const TopoDS_Edge& aEFirst = TopoDS::Edge(myDS->Shape(aPB->OriginalEdge())); |
464 | aTolEFirst = BRep_Tool::MaxTolerance(aEFirst, TopAbs_VERTEX); |
465 | // |
466 | Standard_Real aTmFirst = (aPB->Pave1().Parameter() + aPB->Pave2().Parameter()) / 2.; |
467 | BOPTools_AlgoTools::PointOnEdge(aEFirst, aTmFirst, aPMFirst); |
468 | // |
469 | aLPB.Remove(aItLPB); |
470 | continue; |
471 | } |
472 | // |
473 | // Check pave blocks for coincidence |
474 | const TopoDS_Edge& aE = TopoDS::Edge(myDS->Shape(aPB->OriginalEdge())); |
475 | Standard_Real aTolE = BRep_Tool::MaxTolerance(aE, TopAbs_VERTEX); |
476 | // |
477 | Standard_Real aTOut, aDist; |
478 | Standard_Integer iErr = |
479 | myContext->ComputePE(aPMFirst, aTolEFirst + aTolE + myFuzzyValue, aE, aTOut, aDist); |
480 | if (!iErr && ((aTOut > aPB->Pave1().Parameter()) && (aTOut < aPB->Pave2().Parameter()))) { |
481 | aLPBCB.Append(aPB); |
482 | aLPB.Remove(aItLPB); |
483 | continue; |
484 | } |
485 | aItLPB.Next(); |
486 | } |
487 | // |
488 | // Make Common Block from the pave blocks in the list |
489 | MakeNewCommonBlock(aLPBCB, aCB->Faces(), myDS); |
490 | } |
491 | } |
492 | } |
493 | } |