Commit 022e1342 authored by Michel Rasquin's avatar Michel Rasquin

Simplify xml mshi file for plugin GmshReader

parent 0fa7f742
# GmshReader plugin
* The XML interface file (.mshi) for the GmshReader plugin has been simplified and has become more flexible. The documentation has been updated accordingly in Plugins/GmshReader/README.md, along with the test data.
* A major Gmsh API has changed since Gmsh 4.1.0 so that another variant of the msh 2 format named SeparateViews, which is better suited for massively parallel simulations, can be handled by the plugin, along with the traditional SingleFile and SeparateFiles variants of the msh 2 format.
* The minimum Gmsh version required by the plugin has been incremented to 4.1.0.
......@@ -2,7 +2,7 @@ GmshReader Plugin for ParaView
ParaView/VTK reader for visualization of high-order polynomial solutions under the GMSH format.
Version: 1.0
GmshReader Plugin is copyright (C) 2015 Cenaero, belgium
GmshReader Plugin is copyright (C) 2019 Cenaero, belgium
<http://www.cenaero.be>
The main author of GmshReader Plugin is
......@@ -22,3 +22,8 @@ read unstructured grid data under the PHASTA format. See the original version
of this reader already implemented in the ParaView source code (.pht extension),
or a second version currently implemented as an external plugin (.phts extension)
and available from https://github.com/PHASTA/ParaViewSyncIOReaderPlugin.
If you find this plugin useful, we would be grateful if you could cite the following reference:
M. Rasquin, A. Bauer & K. Hillewaert (2019),
Scientific post hoc and in situ visualisation of high-order polynomial solutions from massively parallel simulations,
International Journal of Computational Fluid Dynamics, DOI: 10.1080/10618562.2019.1618453
......@@ -3,7 +3,7 @@ ParaView/VTK reader for visualization of high-order polynomial solutions under t
Version: 1.0
See Copyright.txt, License.txt and Credits.txt for respective copyright,
license and authors information. You should have received a copy of
license and credits information. You should have received a copy of
these files along with ParaViewGMSHReaderPlugin.
-For more information on Gmsh see http://geuz.org/gmsh/
......@@ -20,7 +20,7 @@ Pre-requirements for building GmshReader Plugin:
----------------------------------------------------
To build this plugin you will need a version of the Gmsh library compiled with specific options.
The Gmsh version required by this plugin must be at least 3.0.7.
The Gmsh version required by this plugin must be at least 4.1.0.
The Gmsh source code can be downloaded from http://gitlab.onelab.info/gmsh/gmsh.git.
For more information on Gmsh, see also http://gitlab.onelab.info/gmsh/gmsh/wikis/home.
......@@ -42,7 +42,7 @@ Then, build and install the library with for instance:
make -j 8 install
This configuration should keep the memory requirement for the Gmsh library as low as possible.
Beware: the default Gmsh library provided by some Linux distributions under their software package manager, or simply built with default cmake variables may include additional components that are not compatible with this plugin.
Beware: the default Gmsh library provided by some Linux distributions under their software package manager, or simply built with default cmake variables, may include additional components that are not compatible with this plugin.
It is strongly advised to build your own version of the Gmsh library using the cmake variables listed above.
Please contact us if you can't build your own version with the cmake command above.
......@@ -64,7 +64,8 @@ In this case, it is advised though to help cmake find the paths to the right ver
Loading the ParaView Gmsh reader plugin:
---------------------------------------
First make sure that libgmsh.so will be found at runtime, this will be automatic if you have set a standard CMAKE_INSTALL_PREFIX for gmsh
First make sure that libgmsh.so will be found at runtime.
This will be automatic if you have set a standard CMAKE_INSTALL_PREFIX for gmsh.
Alternativaly, you can set LD_LIBRARY_PATH to the directory containing libgmsh.so.
Open up ParaView, from the "Tools" menu, then choose "Manage Plugins".
......@@ -76,8 +77,8 @@ This plugin is a client and server plugin, so make sure to load it in both in cl
Note on the MSH file format:
---------------------------
This plugin has been tested with Gmsh format 2.0.
Under Gmsh format version 2.0, a partitioned mesh with N parts can be saved in
This plugin supports Gmsh IO format version 2.0, which is still supported under Gmsh 4.1.0.
Under IO format version 2.0, a partitioned mesh with N parts can be saved in
1. either one single .msh file (in this case, the lines storing the connectivity information for each element will also include the partition ID of this element);
2. N distinct .msh files corresponding to one file per mesh part.
......@@ -89,6 +90,8 @@ To save a partitioned mesh in N distinct files from the Gmsh GUI, follow this pr
* select "Mesh - Gmsh msh" format
* check "Save all" and "Save one file per partition".
Note that the Gmsh IO format version 4.0 is currently being developped and is not supported yet by the plugin.
Reading your msh files:
----------------------
......@@ -99,13 +102,10 @@ For a demo xml file, configure ParaView with BUILD_TESTING set to ON, then build
The format of this xml file is the following:
<GmshMetaFile number_of_partitioned_msh_files="4">
<GeometryInfo has_piece_entry="1"
has_time_entry="0"
pattern="./naca0012/naca_4part.msh_%06d"/>
<FieldInfo has_piece_entry="1"
has_time_entry="1">
<PatternList pattern="./naca0012/domainPressure_t%d.msh_%06d"/>
<PatternList pattern="./naca0012/domainVelocity_t%d.msh_%06d"/>
<GeometryInfo path="./naca0012/naca_4part_[partID].msh"/>
<FieldInfo>
<PathList path="./naca0012/domainPressure_t[step]_[partID].msh"/>
<PathList path="./naca0012/domainVelocity_t[step]_[partID].msh"/>
</FieldInfo>
<TimeSteps number_of_steps="3"
auto_generate_indices="1"
......@@ -132,32 +132,24 @@ However, each pvserver process will then have to read the single mesh file compl
If a mesh has been partitioned in N parts but both the mesh and solution files have been saved in a single file, rank 0 of the pvserver will load all the data (mesh plus solution), which can be extremely slow and put a high pressure on the memory consumption if the data are heavy.
If this step succeeds, it is still possible to enjoy the parallelism of the pvserver by redistributing the data amongs the pvserver processes with the D3 filter.
Side note: The upcoming Gmsh format version 3.0 under development should rely on a parallel IO format which should allow more flexibility in terms of number of mesh part per files.
* The GeometryInfo has three attributes:
1. has_piece_entry (0 or 1): Specifies whether the path pattern to the mesh msh file has a file (piece) placeholder.
The piece placeholder ("%06d" in the example above) is automatically by the update piece number.
Note that the current convention in Gmsh for file IDs includes always 6 digits
for better sorting and must be prepadded with 0 accordingly (use %06d instead of %d).
2. has_time_entry (0 or 1): Specifies whether the pattern has a time placeholder.
The time placeholder ("%d" in the example above) is replaced by an index specified in the TimeSteps element below.
Use 0 unless the mesh changes with time step through adaptation for instance.
3. pattern: This is the pattern used to access the Gmsh mesh, which includes the path
(absolute or relative to the mshi file location) and the mesh filename(s).
The %d and %06d placeholders in the example above will be replaced by appropriate indices following C++ convention.
The first index is time (if specified), the second one is piece.
Side note: The upcoming Gmsh format version 4.0 under development should rely on a format which will allow more flexibility in terms of number of mesh part per files.
* The GeometryInfo has one attribute:
1. path: This is the path used to access the Gmsh mesh (absolute or relative to the mshi file location) and the mesh filename(s).
Note that three keywords can be used in the path: "[step]", "[partID]" and "[zeroPadPartID]".
All occurences of "[step]" in the path will be replaced by the time step number(s) specified later in the mshi file.
All occurences of "[partID]" in the path will be replaced by the piece id of the mesh part.
All occurences of "[zeroPadPartID]" in the path will be replaced by the piece id of the mesh part, padded with leading zeros in such a way that the total number of digits in the part id number is equal to 6.
This corresponds to the old numbering format which was in place before Gmsh 4.1.0 was released.
* The FieldInfo element is optional. If not present, the gmsh mesh with no additional data will be displayed.
If present, FieldInfo contains at least three elements related to the solution data:
1. has_piece_entry (0 or 1): same as above.
2. has_time_entry (0 or 1): same as above.
3. PatternList pattern: This is the pattern used to get the Gmsh solution, which includes the path
If present, FieldInfo contains a series of paths pointing to the solution files with the follwing xml entry.
1. PathList path: This is the path used to get the Gmsh solution,
(absolute or relative to the mshi file location) and the solution filename(s).
There can be any arbitrary number of Gmsh solution filenames (velocity, pressure, etc).
In the examples above. two distinct sets of files are loaded (domainPressure* and domainVelocity*).
All the pfields included in these Gmsh files will be passed to ParaView.
As usual, the %d and %06d placeholders will be replaced by appropriate indices.
The first index is time (if specified), the second one is piece.
All the fields included in these Gmsh files will be passed to ParaView.
Like GeometryInfo, the same keywords "[step]", "[partID]" and "[zeroPadPartID]" can be used with the same meaning as above.
* The TimeSteps element contains TimeStep sub-elements. Each TimeStep element specifies an index (index_attribute), an index used in the geometry filename pattern (geometry_index), an index used in the field filename pattern (field_index) and a time value (float).
1. number_of_steps specifies how many steps of your solution you want to visualize. These steps can then be visualized sequentially with the ParaView play button (green arrow).
......
......@@ -7,7 +7,7 @@ set(private_headers
vtk_module_find_package(
PACKAGE Gmsh
VERSION 4.0.0)
VERSION 4.1.0)
vtk_module_add_module(GmshReader::vtkGmshReader
CLASSES ${classes}
......
......@@ -195,11 +195,7 @@ int vtkGmshMetaReader::RequestData(
output->SetBlock(0, multiPieceDataSet.Get());
const char* filePathPattern = nullptr;
const char* geometryPattern = nullptr;
int fieldHasPiece = 0;
int fieldHasTime = 0;
int geomHasPiece = 0;
int geomHasTime = 0;
const char* geometryPath = nullptr;
unsigned int numElements = rootElement->GetNumberOfNestedElements();
for (unsigned int i = 0; i < numElements; i++)
......@@ -208,43 +204,22 @@ int vtkGmshMetaReader::RequestData(
if (strcmp("GeometryInfo", nested->GetName()) == 0)
{
geometryPattern =
nested->GetAttribute("pattern"); // Only one geometry file pattern is expected
if (!nested->GetScalarAttribute("has_piece_entry", &geomHasPiece))
{
geomHasPiece = 0;
}
if (!nested->GetScalarAttribute("has_time_entry", &geomHasTime))
{
geomHasTime = 0;
}
geometryPath = nested->GetAttribute("path");
}
if (strcmp("FieldInfo", nested->GetName()) == 0)
{
unsigned int numElements2 = nested->GetNumberOfNestedElements();
if (!nested->GetScalarAttribute("has_piece_entry", &fieldHasPiece))
{
fieldHasPiece = 0;
}
if (!nested->GetScalarAttribute("has_time_entry", &fieldHasTime))
{
fieldHasTime = 0;
}
this->Reader->SetFieldInfoPieceTimeEntry(fieldHasPiece, fieldHasTime);
for (unsigned int j = 0; j < numElements2; j++)
{
// Several field file patterns are allowed
vtkPVXMLElement* nested2 = nested->GetNestedElement(j);
if (strcmp("PatternList", nested2->GetName()) == 0)
if (strcmp("PathList", nested2->GetName()) == 0)
{
filePathPattern = nested2->GetAttribute("pattern");
filePathPattern = nested2->GetAttribute("path");
if (!filePathPattern)
{
vtkErrorMacro("No field pattern was provided");
vtkErrorMacro("No field path was provided");
}
else
{
......@@ -259,15 +234,12 @@ int vtkGmshMetaReader::RequestData(
}
}
if (!geometryPattern)
if (!geometryPath)
{
vtkErrorMacro("No geometry pattern was specified. Cannot load file");
return 0;
}
char* geomName;
int geomName_size;
int numPiecesPerFile = numPieces / numFiles;
if (numPieces % numFiles != 0)
......@@ -292,40 +264,24 @@ int vtkGmshMetaReader::RequestData(
<< piece << ", loadingPiece+1=" << loadingPiece + 1 << ", numPieces=" << numPieces
<< ", fileID=" << fileID << ", numProcPieces=" << numProcPieces << ", timeStep=" << timeStep);
// Add +1 for null terminating character.
if (geomHasTime && geomHasPiece)
{
geomName_size = snprintf(nullptr, 0, geometryPattern,
this->Internal->TimeStepInfoMap[this->ActualTimeStep].GeomIndex, fileID) +
1;
geomName = new char[geomName_size];
snprintf(geomName, geomName_size, geometryPattern,
this->Internal->TimeStepInfoMap[this->ActualTimeStep].GeomIndex, fileID);
}
else if (geomHasPiece)
{
geomName_size = snprintf(nullptr, 0, geometryPattern, fileID) + 1;
geomName = new char[geomName_size];
snprintf(geomName, geomName_size, geometryPattern, fileID);
}
else if (geomHasTime)
{
geomName_size = snprintf(nullptr, 0, geometryPattern,
this->Internal->TimeStepInfoMap[this->ActualTimeStep].GeomIndex) +
1;
geomName = new char[geomName_size];
snprintf(geomName, geomName_size, geometryPattern,
this->Internal->TimeStepInfoMap[this->ActualTimeStep].GeomIndex);
}
else
std::string geomFName(geometryPath);
std::string pIdentifier;
pIdentifier = "[partID]";
this->Reader->ReplaceAllStringPattern(geomFName, pIdentifier, fileID);
// Test the old format with 6 digits with zero padding (000001, etc)
if (fileID < 1e7)
{
geomName_size = snprintf(nullptr, 0, geometryPattern, fileID) + 1;
geomName = new char[geomName_size];
strcpy(geomName, geometryPattern);
pIdentifier = "[zeroPadPartID]";
std::ostringstream paddedFileID;
paddedFileID << std::setw(6) << std::setfill('0') << fileID;
this->Reader->ReplaceAllStringPattern(geomFName, pIdentifier, paddedFileID.str());
}
std::string geomFName(geomName);
delete[] geomName;
pIdentifier = "[step]";
this->Reader->ReplaceAllStringPattern(
geomFName, pIdentifier, this->Internal->TimeStepInfoMap[this->ActualTimeStep].GeomIndex);
// Returns the directory path specified in the xml file without the geom file name
std::string gpath = vtksys::SystemTools::GetFilenamePath(geomFName);
......@@ -363,7 +319,7 @@ int vtkGmshMetaReader::RequestData(
//----------------------------------------------------------------------------
namespace
{
// Convert the fileName to a human readable name
// Convert the fileName to a human readable name for the GUI
std::string FormatArrayName(const std::string& fileName)
{
std::string outFieldName = fileName;
......@@ -372,10 +328,17 @@ std::string FormatArrayName(const std::string& fileName)
{
outFieldName = outFieldName.substr(s + 1);
}
s = outFieldName.find("t%d");
// Typically, time steps and iterations are sperated from the main name with dots or under scores.
// Split the name of the file based on these characters.
s = outFieldName.find(".");
if (s != std::string::npos)
{
outFieldName = outFieldName.substr(0, s);
}
s = outFieldName.find("_");
if (s != std::string::npos)
{
outFieldName = outFieldName.substr(0, s - 1);
outFieldName = outFieldName.substr(0, s);
}
return outFieldName;
}
......@@ -523,14 +486,14 @@ int vtkGmshMetaReader::RequestInformation(
unsigned int numElements2 = nested->GetNumberOfNestedElements();
for (unsigned int j = 0; j < numElements2; j++)
{
// Several field file patterns are allowed
// Several field file paths are allowed
vtkPVXMLElement* nested2 = nested->GetNestedElement(j);
if (strcmp("PatternList", nested2->GetName()) == 0)
if (strcmp("PathList", nested2->GetName()) == 0)
{
const char* filePathPattern = nested2->GetAttribute("pattern");
const char* filePathPattern = nested2->GetAttribute("path");
if (!filePathPattern)
{
vtkErrorMacro("No field pattern was provided");
vtkErrorMacro("No field path was provided");
}
else
{
......
......@@ -23,7 +23,7 @@
* See the Copyright.txt and License.txt files provided
* with ParaViewGmshReaderPlugin for license information.
*
*/
*/
#ifndef vtkGmshMetaReader_h
#define vtkGmshMetaReader_h
......@@ -69,9 +69,9 @@ public:
//@{
/** The following methods allow selective reading of solutions fields. By
* default, ALL point data fields are read,
* but this can be modified (e.g. from the ParaView GUI).
*/
* default, ALL point data fields are read,
* but this can be modified (e.g. from the ParaView GUI).
*/
int GetNumberOfPointArrays();
const char* GetPointArrayName(int index);
int GetPointArrayStatus(const char* name);
......
......@@ -51,12 +51,9 @@
//-----------------------------------------------------------------------------
struct vtkGmshReaderInternal
{
vtkGmshReaderInternal(
int adaptLevel = 0, double adaptTol = -0.0001, int fieldHasPiece = -1, int fieldHasTime = -1)
vtkGmshReaderInternal(int adaptLevel = 0, double adaptTol = -0.0001)
: AdaptTolerance(adaptTol)
, AdaptLevel(adaptLevel)
, FieldHasPiece(fieldHasPiece)
, FieldHasTime(fieldHasTime)
{
}
......@@ -103,8 +100,6 @@ struct vtkGmshReaderInternal
double AdaptTolerance;
int AdaptLevel;
int FieldHasPiece;
int FieldHasTime;
std::vector<std::string> FieldPathPattern;
std::vector<vectInt> Connectivity; // connectivity (vector of vector)
std::vector<int> CellType; // topology
......@@ -148,18 +143,6 @@ void vtkGmshReader::ClearFieldInfo()
this->Modified();
}
//-----------------------------------------------------------------------------
void vtkGmshReader::SetFieldInfoPieceTimeEntry(int hasPieceEntry, int hasTimeEntry)
{
if (this->Internal->FieldHasPiece != hasPieceEntry ||
this->Internal->FieldHasTime != hasTimeEntry)
{
this->Internal->FieldHasPiece = hasPieceEntry;
this->Internal->FieldHasTime = hasTimeEntry;
this->Modified();
}
}
//-----------------------------------------------------------------------------
void vtkGmshReader::SetFieldInfoPath(const std::string& addToPath)
{
......@@ -253,45 +236,26 @@ int vtkGmshReader::ReadGeomAndFieldFile(int& firstVertexNo, vtkUnstructuredGrid*
std::vector<std::string>::iterator itfieldpath = this->Internal->FieldPathPattern.begin();
std::vector<std::string>::iterator itfieldpathend = this->Internal->FieldPathPattern.end();
int fieldHasPiece = this->Internal->FieldHasPiece;
int fieldHasTime = this->Internal->FieldHasTime;
for (; itfieldpath != itfieldpathend; ++itfieldpath)
{
// ParaView sorts the fields by alphabetical order
char* field_name;
int field_name_size;
std::string str_field_name(itfieldpath->c_str());
std::string pIdentifier;
// Beware: According to Gmsh convention, the file id should
// always include 6 digits and be padded with 0 as needed.
// Add +1 for null terminating character.
if (fieldHasTime && fieldHasPiece)
{
field_name_size = snprintf(nullptr, 0, itfieldpath->c_str(), TimeStep, PartID) + 1;
field_name = new char[field_name_size];
snprintf(field_name, field_name_size, itfieldpath->c_str(), TimeStep, PartID);
}
else if (fieldHasPiece)
{
field_name_size = snprintf(nullptr, 0, itfieldpath->c_str(), FileID) + 1;
field_name = new char[field_name_size];
snprintf(field_name, field_name_size, itfieldpath->c_str(), FileID);
}
else if (fieldHasTime)
{
field_name_size = snprintf(nullptr, 0, itfieldpath->c_str(), TimeStep) + 1;
field_name = new char[field_name_size];
snprintf(field_name, field_name_size, itfieldpath->c_str(), TimeStep);
}
else
pIdentifier = "[partID]";
this->ReplaceAllStringPattern(str_field_name, pIdentifier, PartID);
// Test the old format with 6 digits and zero padding (000001, etc)
if (PartID < 1e7)
{
field_name_size = snprintf(nullptr, 0, "%s", itfieldpath->c_str()) + 1;
field_name = new char[field_name_size];
strcpy(field_name, itfieldpath->c_str());
pIdentifier = "[zeroPadPartID]";
std::ostringstream paddedFileID;
paddedFileID << std::setw(6) << std::setfill('0') << PartID;
this->ReplaceAllStringPattern(str_field_name, pIdentifier, paddedFileID.str());
}
std::string str_field_name(field_name); // convert to string
delete[] field_name;
pIdentifier = "[step]";
this->ReplaceAllStringPattern(str_field_name, pIdentifier, TimeStep);
// Returns the directory path specified in the xml file without the geom file name
std::string fpath = vtksys::SystemTools::GetFilenamePath(str_field_name);
......@@ -580,7 +544,11 @@ int vtkGmshReader::ReadGeomAndFieldFile(int& firstVertexNo, vtkUnstructuredGrid*
// Initialize datafilename string
std::string datafilename(mimargv[i]);
// Merge Field msh files
MergeFile(datafilename);
// Prototype:
// int MergeFile(const std::string &fileName, bool warnIfMissing = false,
// bool setBoundingBox = true, bool importPhysicalsInOnelab = true,
// int partitionToRead = --1);
MergeFile(datafilename, false, false, false, PartID - 1);
}
if (PView::list.empty())
......@@ -818,11 +786,7 @@ int vtkGmshReader::ReadGeomAndFieldFile(int& firstVertexNo, vtkUnstructuredGrid*
// Adapt and generate the GmshS object globVTKData
if (testPViewData->getAdaptiveData())
{
vtkWarningMacro("Pointer _adaptiveData is not empty...\nYou are likely using a "
"Gmsh library compiled in a non-optimal way for this plugin.\nIn "
"addition to a higher memory consumption, the plugin may not "
"behave as expected.\nUse at your own risk or rebuild Gmsh "
"according to the procedure described in the README file");
// Clean any existing adaptive data that could have been pre-allcoated
testPViewData->destroyAdaptiveData();
}
......@@ -1042,6 +1006,7 @@ int vtkGmshReader::ReadGeomAndFieldFile(int& firstVertexNo, vtkUnstructuredGrid*
{
field->AddArray(dataArray);
dataArray->Delete();
dataArray = nullptr;
}
this->Internal->ClearData();
......@@ -1056,3 +1021,29 @@ int vtkGmshReader::ReadGeomAndFieldFile(int& firstVertexNo, vtkUnstructuredGrid*
return 1;
} // end of ReadGeomAndFieldFile
//----------------------------------------------------------------------------
template <class T>
void vtkGmshReader::ReplaceAllStringPattern(
std::string& input, const std::string& pIdentifier, const T& target)
{
vtkDebugMacro("Entering vtkGmshMetaReader::ReplaceStringPatternByInt()");
size_t foundPID = input.find(pIdentifier);
while (foundPID != std::string::npos)
{
std::string base = input.substr(0, foundPID);
std::string ext = input.substr(foundPID + pIdentifier.size(), input.size());
std::ostringstream fStr;
fStr << base << target << ext;
input = fStr.str();
foundPID = input.find(pIdentifier);
}
}
// Explicit instantiations
template void vtkGmshReader::ReplaceAllStringPattern(
std::string& input, const std::string& pIdentifier, const int& target);
template void vtkGmshReader::ReplaceAllStringPattern(
std::string& input, const std::string& pIdentifier, const std::string& target);
......@@ -23,7 +23,7 @@
* See the Copyright.txt and License.txt files provided
* with ParaViewGmshReaderPlugin for license information.
*
*/
*/
#ifndef vtkGmshReader_h
#define vtkGmshReader_h
......@@ -39,6 +39,8 @@ public:
static vtkGmshReader* New();
vtkTypeMacro(vtkGmshReader, vtkUnstructuredGridAlgorithm);
void PrintSelf(ostream& os, vtkIndent indent) override;
template <class T>
void ReplaceAllStringPattern(std::string& input, const std::string& pIdentifier, const T& target);
//@{
/**
......@@ -102,7 +104,6 @@ public:
*/
void ClearFieldInfo();
void SetAdaptInfo(int adaptLevel, double adaptTol);
void SetFieldInfoPieceTimeEntry(int hasPieceEntry, int hasTimeEntry);
void SetFieldInfoPath(const std::string& addPath);
int GetSizeFieldPathPattern();
//@}
......
43999ceabbe9526aa5d15919a61d2c69c31ff3304b5964c048e0991c4f5f52478f370f4944507cf7663dd75e32560a5a83205b7f30db3d4ccc38af233da5b1ff
b98adab82d583bf92a0cb1da472adc04e18cee257a4c9d63806c03f3c79103a30c5fa088c67d3e44e48fe0ab157a49d2012fd52c8963790979c5cd1da4fcb0fd
1fd422042533a059441777b53e3f1395a93d356afcc14d375764fc3ac99872679ea7965e7f617538af780901d59626b5f5cc3824b1f14ea45cf7a2410d94e4f4
033665e25de9a448cf9401da6dc08a24a1e16d53789f591279fe6702a791882a6a53b68faedc21ddc9cfc8825609cfe70aead2cc151cfbe2251673fa3ec2087e
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment