2017-03-22 13:43:10 +00:00
|
|
|
/*
|
|
|
|
* This program source code file is part of KICAD, a free EDA CAD application.
|
|
|
|
*
|
|
|
|
* Copyright (C) 2013-2017 CERN
|
2023-01-23 16:23:43 +00:00
|
|
|
* Copyright (C) 2018-2023 KiCad Developers, see AUTHORS.txt for contributors.
|
2017-03-22 13:43:10 +00:00
|
|
|
* @author Maciej Suminski <maciej.suminski@cern.ch>
|
|
|
|
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program 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 General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, you may find one here:
|
|
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
|
|
*/
|
|
|
|
|
2017-11-15 17:33:06 +00:00
|
|
|
#ifndef __CONNECTIVITY_DATA_H
|
|
|
|
#define __CONNECTIVITY_DATA_H
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
#include <core/typeinfo.h>
|
Fix issues with zone filling connectivity locking
Two issues found with the locking system used to prevent access to
stale connectivity data during the zone fill process:
1) a std::mutex has undefined behavior if you try to use it to guard
against access from the same thread. Because of the use of wx event
loops (and coroutines) it is entirely possible, and in some situations
inevitable, that the same thread will try to redraw the ratsnest in the
middle of zone refilling.
2) The mutex was only guarding the ZONE_FILLER::Fill method, but the callers
of that method also do connectivity updates as part of the COMMIT::Push.
Redrawing the ratsnest after the Fill but before the Push will result in
stale connectivity pointers to zone filled areas.
Fixed (1) by switching to a trivial spinlock implementation. Spinlocks would
generally not be desirable if the contention for the connectivity data crossed
thread boundaries, but at the moment I believe it's guaranteed that the reads
and writes to connectivity that are guarded by this lock happen from the main
UI thread. The writes are also quite rare compared to reads, and reads are
generally fast, so I'm not really worried about the UI thread spinning for any
real amount of time.
Fixed (2) by moving the locking location up to the call sites of
ZONE_FILLER::Fill.
This issue was quite difficult to reproduce, but I found a fairly reliable way:
It only happens (for me) on Windows, MSYS2 build, with wxWidgets 3.0
It also only happens if I restrict PcbNew to use 2 CPU cores.
With those conditions, I can reproduce the issue described in #6471 by
repeatedly editing a zone properties and changing its net. The crash is
especially easy to trigger if you press some keys (such as 'e' for edit)
while the progress dialog is displayed. It's easiest to do this in a debug
build as the slower KiCad is running, the bigger the window is to trigger this
bug.
Fixes https://gitlab.com/kicad/code/kicad/-/issues/6471
Fixes https://gitlab.com/kicad/code/kicad/-/issues/7048
2021-01-18 17:24:07 +00:00
|
|
|
#include <core/spinlock.h>
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
#include <memory>
|
2019-06-05 22:55:52 +00:00
|
|
|
#include <mutex>
|
|
|
|
#include <vector>
|
|
|
|
#include <wx/string.h>
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
#include <math/vector2d.h>
|
2018-04-09 14:07:53 +00:00
|
|
|
#include <geometry/shape_poly_set.h>
|
2020-11-11 23:05:59 +00:00
|
|
|
#include <zone.h>
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2020-09-25 18:26:58 +00:00
|
|
|
class FROM_TO_CACHE;
|
2017-03-22 13:43:10 +00:00
|
|
|
class CN_CLUSTER;
|
|
|
|
class CN_CONNECTIVITY_ALGO;
|
2017-03-22 15:47:15 +00:00
|
|
|
class CN_EDGE;
|
2017-03-22 13:43:10 +00:00
|
|
|
class BOARD;
|
2019-05-25 01:55:40 +00:00
|
|
|
class BOARD_COMMIT;
|
2017-03-22 13:43:10 +00:00
|
|
|
class BOARD_CONNECTED_ITEM;
|
|
|
|
class BOARD_ITEM;
|
2020-11-11 23:05:59 +00:00
|
|
|
class ZONE;
|
2017-03-22 13:43:10 +00:00
|
|
|
class RN_DATA;
|
|
|
|
class RN_NET;
|
2021-06-11 21:07:02 +00:00
|
|
|
class PCB_TRACK;
|
2023-05-12 21:03:54 +00:00
|
|
|
class PCB_VIA;
|
2020-11-12 22:30:02 +00:00
|
|
|
class PAD;
|
2020-11-13 15:15:52 +00:00
|
|
|
class FOOTPRINT;
|
2017-11-23 16:20:27 +00:00
|
|
|
class PROGRESS_REPORTER;
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
struct CN_DISJOINT_NET_ENTRY
|
|
|
|
{
|
|
|
|
int net;
|
2017-06-30 11:40:20 +00:00
|
|
|
BOARD_CONNECTED_ITEM *a, *b;
|
2017-03-22 13:43:10 +00:00
|
|
|
VECTOR2I anchorA, anchorB;
|
|
|
|
};
|
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
struct RN_DYNAMIC_LINE
|
|
|
|
{
|
|
|
|
int netCode;
|
|
|
|
VECTOR2I a, b;
|
|
|
|
};
|
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
|
2021-03-23 21:42:56 +00:00
|
|
|
/**
|
|
|
|
* Controls how nets are propagated through clusters
|
|
|
|
*/
|
|
|
|
enum class PROPAGATE_MODE
|
|
|
|
{
|
2023-03-24 13:02:13 +00:00
|
|
|
SKIP_CONFLICTS, ///< Clusters with conflicting drivers are not updated (default)
|
|
|
|
RESOLVE_CONFLICTS ///< Clusters with conflicting drivers are updated to the most popular net
|
2021-03-23 21:42:56 +00:00
|
|
|
};
|
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
// a wrapper class encompassing the connectivity computation algorithm and the
|
2018-12-20 05:15:53 +00:00
|
|
|
class CONNECTIVITY_DATA
|
2017-03-22 13:43:10 +00:00
|
|
|
{
|
|
|
|
public:
|
|
|
|
CONNECTIVITY_DATA();
|
|
|
|
~CONNECTIVITY_DATA();
|
|
|
|
|
2020-06-23 03:35:34 +00:00
|
|
|
CONNECTIVITY_DATA( const std::vector<BOARD_ITEM*>& aItems, bool aSkipItems = false );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function Build()
|
|
|
|
* Builds the connectivity database for the board aBoard.
|
|
|
|
*/
|
2023-01-23 23:55:29 +00:00
|
|
|
bool Build( BOARD* aBoard, PROGRESS_REPORTER* aReporter = nullptr );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function Build()
|
|
|
|
* Builds the connectivity database for a set of items aItems.
|
|
|
|
*/
|
|
|
|
void Build( const std::vector<BOARD_ITEM*>& aItems );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function Add()
|
|
|
|
* Adds an item to the connectivity data.
|
|
|
|
* @param aItem is an item to be added.
|
|
|
|
* @return True if operation succeeded.
|
|
|
|
*/
|
|
|
|
bool Add( BOARD_ITEM* aItem );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function Remove()
|
|
|
|
* Removes an item from the connectivity data.
|
|
|
|
* @param aItem is an item to be updated.
|
|
|
|
* @return True if operation succeeded.
|
|
|
|
*/
|
|
|
|
bool Remove( BOARD_ITEM* aItem );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function Update()
|
|
|
|
* Updates the connectivity data for an item.
|
|
|
|
* @param aItem is an item to be updated.
|
|
|
|
* @return True if operation succeeded.
|
|
|
|
*/
|
|
|
|
bool Update( BOARD_ITEM* aItem );
|
|
|
|
|
2020-08-15 00:33:27 +00:00
|
|
|
/**
|
|
|
|
* Moves the connectivity list anchors. N.B., this does not move the bounding
|
2021-06-09 19:32:58 +00:00
|
|
|
* boxes for the RTree, so the use of this function will invalidate the
|
2020-08-15 00:33:27 +00:00
|
|
|
* connectivity data for uses other than the dynamic ratsnest
|
|
|
|
*
|
|
|
|
* @param aDelta vector for movement of the tree
|
|
|
|
*/
|
|
|
|
void Move( const VECTOR2I& aDelta );
|
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
/**
|
|
|
|
* Function Clear()
|
|
|
|
* Erases the connectivity database.
|
|
|
|
*/
|
2022-11-30 12:18:58 +00:00
|
|
|
void ClearRatsnest();
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function GetNetCount()
|
|
|
|
* Returns the total number of nets in the connectivity database.
|
|
|
|
*/
|
|
|
|
int GetNetCount() const;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function GetRatsnestForNet()
|
|
|
|
* Returns the ratsnest, expressed as a set of graph edges for a given net.
|
|
|
|
*/
|
2017-06-29 22:45:50 +00:00
|
|
|
RN_NET* GetRatsnestForNet( int aNet );
|
2017-06-30 11:40:20 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
/**
|
|
|
|
* Propagates the net codes from the source pads to the tracks/vias.
|
2021-03-23 21:42:56 +00:00
|
|
|
* @param aCommit is used to save the undo state of items modified by this call
|
|
|
|
* @param aMode controls how conflicts between pads are resolved
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2023-01-23 16:23:43 +00:00
|
|
|
void PropagateNets( BOARD_COMMIT* aCommit = nullptr );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
2023-03-25 10:44:46 +00:00
|
|
|
* Fill the isolate islands list for each layer of each zone. Isolated islands are individual
|
|
|
|
* polygons in a zone fill that don't connect to a net.
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2023-03-25 10:44:46 +00:00
|
|
|
void FillIsolatedIslandsMap( std::map<ZONE*, std::map<PCB_LAYER_ID, ISOLATED_ISLANDS>>& aMap,
|
|
|
|
bool aConnectivityAlreadyRebuilt = false );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function RecalculateRatsnest()
|
|
|
|
* Updates the ratsnest for the board.
|
2019-05-25 01:55:40 +00:00
|
|
|
* @param aCommit is used to save the undo state of items modified by this call
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2019-05-25 01:55:40 +00:00
|
|
|
void RecalculateRatsnest( BOARD_COMMIT* aCommit = nullptr );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
2022-09-29 16:07:42 +00:00
|
|
|
* @param aVisibleOnly include only visbile edges in the count
|
|
|
|
* @return the number of remaining edges in the ratsnest
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2022-09-29 16:07:42 +00:00
|
|
|
unsigned int GetUnconnectedCount( bool aVisibileOnly ) const;
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2022-08-20 09:27:35 +00:00
|
|
|
bool IsConnectedOnLayer( const BOARD_CONNECTED_ITEM* aItem, int aLayer,
|
2022-10-18 12:00:37 +00:00
|
|
|
const std::initializer_list<KICAD_T>& aTypes = {} ) const;
|
2020-07-27 19:41:50 +00:00
|
|
|
|
2017-03-22 13:51:07 +00:00
|
|
|
unsigned int GetNodeCount( int aNet = -1 ) const;
|
|
|
|
|
|
|
|
unsigned int GetPadCount( int aNet = -1 ) const;
|
|
|
|
|
2021-06-11 21:07:02 +00:00
|
|
|
const std::vector<PCB_TRACK*> GetConnectedTracks( const BOARD_CONNECTED_ITEM* aItem ) const;
|
2017-03-22 13:51:07 +00:00
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
const std::vector<PAD*> GetConnectedPads( const BOARD_CONNECTED_ITEM* aItem ) const;
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2020-11-12 22:30:02 +00:00
|
|
|
void GetConnectedPads( const BOARD_CONNECTED_ITEM* aItem, std::set<PAD*>* pads ) const;
|
2018-08-19 16:11:58 +00:00
|
|
|
|
2023-05-12 21:03:54 +00:00
|
|
|
void GetConnectedPadsAndVias( const BOARD_CONNECTED_ITEM* aItem, std::vector<PAD*>* pads,
|
|
|
|
std::vector<PCB_VIA*>* vias );
|
|
|
|
|
2020-11-08 22:43:31 +00:00
|
|
|
/**
|
|
|
|
* Function GetConnectedItemsAtAnchor()
|
|
|
|
* Returns a list of items connected to a source item aItem at position aAnchor
|
2021-03-31 20:13:08 +00:00
|
|
|
* with an optional maximum distance from the defined anchor.
|
2020-11-08 22:43:31 +00:00
|
|
|
* @param aItem is the reference item to find other connected items.
|
|
|
|
* @param aAnchor is the position to find connected items on.
|
|
|
|
* @param aTypes allows one to filter by item types.
|
2021-03-31 20:13:08 +00:00
|
|
|
* @param aMaxError Maximum distance of the found items' anchors to aAnchor in IU
|
2021-03-23 21:42:56 +00:00
|
|
|
* @return
|
2020-11-08 22:43:31 +00:00
|
|
|
*/
|
2022-08-20 09:27:35 +00:00
|
|
|
const std::vector<BOARD_CONNECTED_ITEM*>
|
|
|
|
GetConnectedItemsAtAnchor( const BOARD_CONNECTED_ITEM* aItem, const VECTOR2I& aAnchor,
|
|
|
|
const std::initializer_list<KICAD_T>& aTypes,
|
|
|
|
const int& aMaxError = 0 ) const;
|
2017-06-22 14:24:07 +00:00
|
|
|
|
2022-09-29 16:07:42 +00:00
|
|
|
void RunOnUnconnectedEdges( std::function<bool( CN_EDGE& )> aFunc );
|
2017-03-22 15:47:15 +00:00
|
|
|
|
2023-06-17 21:56:57 +00:00
|
|
|
bool TestTrackEndpointDangling( PCB_TRACK* aTrack, bool aIgnoreTracksInPads,
|
|
|
|
VECTOR2I* aPos = nullptr ) const;
|
2020-05-14 19:24:10 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
/**
|
2022-09-03 18:29:02 +00:00
|
|
|
* Function ClearLocalRatsnest()
|
|
|
|
* Erases the temporary, selection-based ratsnest (i.e. the ratsnest lines that pcbnew
|
|
|
|
* displays when moving an item/set of items).
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2022-09-03 18:29:02 +00:00
|
|
|
void ClearLocalRatsnest();
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2017-08-01 13:20:40 +00:00
|
|
|
/**
|
2022-09-03 18:29:02 +00:00
|
|
|
* Hides the temporary, selection-based ratsnest lines.
|
2017-08-01 13:20:40 +00:00
|
|
|
*/
|
2022-09-03 18:29:02 +00:00
|
|
|
void HideLocalRatsnest();
|
2017-08-01 13:20:40 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
/**
|
2022-09-03 18:29:02 +00:00
|
|
|
* Function ComputeLocalRatsnest()
|
|
|
|
* Calculates the temporary (usually selection-based) ratsnest for the set of \a aItems.
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2022-09-03 18:29:02 +00:00
|
|
|
void ComputeLocalRatsnest( const std::vector<BOARD_ITEM*>& aItems,
|
|
|
|
const CONNECTIVITY_DATA* aDynamicData,
|
|
|
|
VECTOR2I aInternalOffset = { 0, 0 } );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2022-09-03 18:29:02 +00:00
|
|
|
const std::vector<RN_DYNAMIC_LINE>& GetLocalRatsnest() const { return m_dynamicRatsnest; }
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function GetConnectedItems()
|
|
|
|
* Returns a list of items connected to a source item aItem.
|
|
|
|
* @param aItem is the reference item to find other connected items.
|
2018-07-08 12:12:38 +00:00
|
|
|
* @param aTypes allows one to filter by item types.
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2022-08-20 09:27:35 +00:00
|
|
|
const std::vector<BOARD_CONNECTED_ITEM*>
|
|
|
|
GetConnectedItems( const BOARD_CONNECTED_ITEM* aItem,
|
|
|
|
const std::initializer_list<KICAD_T>& aTypes,
|
|
|
|
bool aIgnoreNetcodes = false ) const;
|
2017-03-22 13:43:10 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Function GetNetItems()
|
|
|
|
* Returns the list of items that belong to a certain net.
|
|
|
|
* @param aNetCode is the net code.
|
2018-07-08 12:12:38 +00:00
|
|
|
* @param aTypes allows one to filter by item types.
|
2017-03-22 13:43:10 +00:00
|
|
|
*/
|
2022-08-20 09:27:35 +00:00
|
|
|
const std::vector<BOARD_CONNECTED_ITEM*>
|
|
|
|
GetNetItems( int aNetCode, const std::initializer_list<KICAD_T>& aTypes ) const;
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2017-03-22 13:51:07 +00:00
|
|
|
void BlockRatsnestItems( const std::vector<BOARD_ITEM*>& aItems );
|
|
|
|
|
2022-08-20 09:27:35 +00:00
|
|
|
std::shared_ptr<CN_CONNECTIVITY_ALGO> GetConnectivityAlgo() const { return m_connAlgo; }
|
2017-05-04 22:43:43 +00:00
|
|
|
|
2022-08-20 09:27:35 +00:00
|
|
|
KISPINLOCK& GetLock() { return m_lock; }
|
2018-12-20 05:15:53 +00:00
|
|
|
|
2017-07-02 00:05:42 +00:00
|
|
|
void MarkItemNetAsDirty( BOARD_ITEM* aItem );
|
2017-11-23 16:20:27 +00:00
|
|
|
void SetProgressReporter( PROGRESS_REPORTER* aReporter );
|
2018-08-24 11:05:18 +00:00
|
|
|
|
2022-08-20 09:27:35 +00:00
|
|
|
const std::map<int, wxString>& GetNetclassMap() const { return m_netclassMap; }
|
2020-07-11 17:42:00 +00:00
|
|
|
|
2018-08-24 11:05:18 +00:00
|
|
|
#ifndef SWIG
|
2023-06-24 18:54:50 +00:00
|
|
|
const std::vector<CN_EDGE> GetRatsnestForItems( const std::vector<BOARD_ITEM*>& aItems );
|
2020-06-23 03:35:34 +00:00
|
|
|
|
2021-10-25 20:35:19 +00:00
|
|
|
const std::vector<CN_EDGE> GetRatsnestForPad( const PAD* aPad );
|
|
|
|
|
2020-11-13 15:15:52 +00:00
|
|
|
const std::vector<CN_EDGE> GetRatsnestForComponent( FOOTPRINT* aComponent,
|
|
|
|
bool aSkipInternalConnections = false );
|
2018-08-24 11:05:18 +00:00
|
|
|
#endif
|
2017-11-23 16:20:27 +00:00
|
|
|
|
2022-08-20 09:27:35 +00:00
|
|
|
std::shared_ptr<FROM_TO_CACHE> GetFromToCache() { return m_fromToCache; }
|
2020-09-25 18:26:58 +00:00
|
|
|
|
2017-03-22 13:43:10 +00:00
|
|
|
private:
|
2020-08-15 00:33:27 +00:00
|
|
|
|
2023-02-03 14:22:36 +00:00
|
|
|
/**
|
|
|
|
* Updates the ratsnest for the board without locking the connectivity mutex.
|
|
|
|
* @param aCommit is used to save the undo state of items modified by this call
|
|
|
|
*/
|
|
|
|
void internalRecalculateRatsnest( BOARD_COMMIT* aCommit = nullptr );
|
|
|
|
void updateRatsnest();
|
|
|
|
|
|
|
|
void addRatsnestCluster( const std::shared_ptr<CN_CLUSTER>& aCluster );
|
2017-03-22 13:43:10 +00:00
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
private:
|
2017-03-22 13:43:10 +00:00
|
|
|
std::shared_ptr<CN_CONNECTIVITY_ALGO> m_connAlgo;
|
2017-11-23 16:20:27 +00:00
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
std::shared_ptr<FROM_TO_CACHE> m_fromToCache;
|
|
|
|
std::vector<RN_DYNAMIC_LINE> m_dynamicRatsnest;
|
|
|
|
std::vector<RN_NET*> m_nets;
|
2018-12-20 05:15:53 +00:00
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
/// Used to suppress ratsnest calculations on dynamic ratsnests
|
|
|
|
bool m_skipRatsnest = false;
|
2020-06-23 03:35:34 +00:00
|
|
|
|
2021-11-30 14:19:39 +00:00
|
|
|
KISPINLOCK m_lock;
|
2020-07-11 17:42:00 +00:00
|
|
|
|
|
|
|
/// Map of netcode -> netclass the net is a member of; used for ratsnest painting
|
2021-11-30 14:19:39 +00:00
|
|
|
std::map<int, wxString> m_netclassMap;
|
|
|
|
|
|
|
|
PROGRESS_REPORTER* m_progressReporter;
|
2017-03-22 13:43:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
#endif
|