[mesh] fix bug in rendering datasets with inactive faces

This commit is contained in:
Peter Petrik 2018-08-17 14:05:27 +02:00 committed by Martin Dobias
parent b39ee5a40c
commit 419ec1f736
15 changed files with 206 additions and 38 deletions

View File

@ -359,6 +359,18 @@ Returns dataset metadata
Returns vector/scalar value associated with the index from the dataset
See QgsMeshDatasetMetadata.isVector() to check if the returned value is vector or scalar
%End
virtual bool faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const = 0;
%Docstring
Returns whether the face is active for particular dataset
For example to represent the situation when F1 and F3 are flooded, but F2 is dry,
some solvers store water depth on vertices V1-V8 (all non-zero values) and
set active flag for F2 to false.
V1 ---- V2 ---- V5-----V7
| F1 | F2 | F3 |
V3 ---- V4 ---- V6-----V8
%End
};

View File

@ -337,6 +337,18 @@ class CORE_EXPORT QgsMeshDatasetSourceInterface SIP_ABSTRACT
* See QgsMeshDatasetMetadata::isVector() to check if the returned value is vector or scalar
*/
virtual QgsMeshDatasetValue datasetValue( QgsMeshDatasetIndex index, int valueIndex ) const = 0;
/**
* \brief Returns whether the face is active for particular dataset
*
* For example to represent the situation when F1 and F3 are flooded, but F2 is dry,
* some solvers store water depth on vertices V1-V8 (all non-zero values) and
* set active flag for F2 to false.
* V1 ---- V2 ---- V5-----V7
* | F1 | F2 | F3 |
* V3 ---- V4 ---- V6-----V8
*/
virtual bool faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const = 0;
};

View File

@ -127,29 +127,36 @@ QgsMeshDatasetValue QgsMeshLayer::datasetValue( const QgsMeshDatasetIndex &index
int faceIndex = mTriangularMesh->faceIndexForPoint( point ) ;
if ( faceIndex >= 0 )
{
if ( dataProvider()->datasetGroupMetadata( index ).dataType() == QgsMeshDatasetGroupMetadata::DataOnFaces )
int nativeFaceIndex = mTriangularMesh->trianglesToNativeFaces().at( faceIndex );
if ( dataProvider()->faceIsActive( index, nativeFaceIndex ) )
{
int nativeFaceIndex = mTriangularMesh->trianglesToNativeFaces().at( faceIndex );
return dataProvider()->datasetValue( index, nativeFaceIndex );
}
else
{
const QgsMeshFace &face = mTriangularMesh->triangles()[faceIndex];
const int v1 = face[0], v2 = face[1], v3 = face[2];
const QgsPoint p1 = mTriangularMesh->vertices()[v1], p2 = mTriangularMesh->vertices()[v2], p3 = mTriangularMesh->vertices()[v3];
const QgsMeshDatasetValue val1 = dataProvider()->datasetValue( index, v1 );
const QgsMeshDatasetValue val2 = dataProvider()->datasetValue( index, v2 );
const QgsMeshDatasetValue val3 = dataProvider()->datasetValue( index, v3 );
const double x = QgsMeshLayerInterpolator::interpolateFromVerticesData( p1, p2, p3, val1.x(), val2.x(), val3.x(), point );
double y = std::numeric_limits<double>::quiet_NaN();
bool isVector = dataProvider()->datasetGroupMetadata( index ).isVector();
if ( isVector )
y = QgsMeshLayerInterpolator::interpolateFromVerticesData( p1, p2, p3, val1.y(), val2.y(), val3.y(), point );
return QgsMeshDatasetValue( x, y );
if ( dataProvider()->datasetGroupMetadata( index ).dataType() == QgsMeshDatasetGroupMetadata::DataOnFaces )
{
int nativeFaceIndex = mTriangularMesh->trianglesToNativeFaces().at( faceIndex );
value = dataProvider()->datasetValue( index, nativeFaceIndex );
}
else
{
const QgsMeshFace &face = mTriangularMesh->triangles()[faceIndex];
const int v1 = face[0], v2 = face[1], v3 = face[2];
const QgsPoint p1 = mTriangularMesh->vertices()[v1], p2 = mTriangularMesh->vertices()[v2], p3 = mTriangularMesh->vertices()[v3];
const QgsMeshDatasetValue val1 = dataProvider()->datasetValue( index, v1 );
const QgsMeshDatasetValue val2 = dataProvider()->datasetValue( index, v2 );
const QgsMeshDatasetValue val3 = dataProvider()->datasetValue( index, v3 );
const double x = QgsMeshLayerInterpolator::interpolateFromVerticesData( p1, p2, p3, val1.x(), val2.x(), val3.x(), point );
double y = std::numeric_limits<double>::quiet_NaN();
bool isVector = dataProvider()->datasetGroupMetadata( index ).isVector();
if ( isVector )
y = QgsMeshLayerInterpolator::interpolateFromVerticesData( p1, p2, p3, val1.y(), val2.y(), val3.y(), point );
value = QgsMeshDatasetValue( x, y );
}
}
}
}
return value;
}

View File

@ -107,14 +107,14 @@ double interpolateFromFacesData( const QgsPointXY &p1, const QgsPointXY &p2, con
return val;
}
QgsMeshLayerInterpolator::QgsMeshLayerInterpolator(
const QgsTriangularMesh &m,
const QVector<double> &datasetValues,
bool dataIsOnVertices,
const QgsRenderContext &context,
const QSize &size )
QgsMeshLayerInterpolator::QgsMeshLayerInterpolator( const QgsTriangularMesh &m,
const QVector<double> &datasetValues, const QVector<bool> &activeFaceFlagValues,
bool dataIsOnVertices,
const QgsRenderContext &context,
const QSize &size )
: mTriangularMesh( m ),
mDatasetValues( datasetValues ),
mActiveFaceFlagValues( activeFaceFlagValues ),
mContext( context ),
mDataOnVertices( dataIsOnVertices ),
mOutputSize( size )
@ -162,6 +162,11 @@ QgsRasterBlock *QgsMeshLayerInterpolator::block( int, const QgsRectangle &extent
const int v1 = face[0], v2 = face[1], v3 = face[2];
const QgsPoint p1 = vertices[v1], p2 = vertices[v2], p3 = vertices[v3];
const int nativeFaceIndex = mTriangularMesh.trianglesToNativeFaces()[i];
const bool isActive = mActiveFaceFlagValues[nativeFaceIndex];
if ( !isActive )
continue;
QgsRectangle bbox;
bbox.combineExtentWith( p1.x(), p1.y() );
bbox.combineExtentWith( p2.x(), p2.y() );

View File

@ -49,6 +49,7 @@ class QgsMeshLayerInterpolator : public QgsRasterInterface
//! Ctor
QgsMeshLayerInterpolator( const QgsTriangularMesh &m,
const QVector<double> &datasetValues,
const QVector<bool> &activeFaceFlagValues,
bool dataIsOnVertices,
const QgsRenderContext &context,
const QSize &size );
@ -78,6 +79,7 @@ class QgsMeshLayerInterpolator : public QgsRasterInterface
private:
const QgsTriangularMesh &mTriangularMesh;
const QVector<double> &mDatasetValues;
const QVector<bool> &mActiveFaceFlagValues;
const QgsRenderContext &mContext;
bool mDataOnVertices = true;
QSize mOutputSize;

View File

@ -110,6 +110,14 @@ void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer )
mScalarDatasetValues[i] = v;
}
// populate face active flag, always defined on faces
mScalarActiveFaceFlagValues.resize( mNativeMesh.faces.count() );
for ( int i = 0; i < mNativeMesh.faces.count(); ++i )
{
bool active = layer->dataProvider()->faceIsActive( datasetIndex, i );
mScalarActiveFaceFlagValues[i] = active;
}
QgsMeshLayerUtils::calculateMinimumMaximum( mScalarDatasetMinimum, mScalarDatasetMaximum, mScalarDatasetValues );
}
}
@ -209,7 +217,8 @@ void QgsMeshLayerRenderer::renderScalarDataset()
QgsColorRampShader *fcn = new QgsColorRampShader( scalarSettings.colorRampShader() );
QgsRasterShader *sh = new QgsRasterShader();
sh->setRasterShaderFunction( fcn ); // takes ownership of fcn
QgsMeshLayerInterpolator interpolator( mTriangularMesh, mScalarDatasetValues, mScalarDataOnVertices, mContext, mOutputSize );
QgsMeshLayerInterpolator interpolator( mTriangularMesh, mScalarDatasetValues, mScalarActiveFaceFlagValues,
mScalarDataOnVertices, mContext, mOutputSize );
QgsSingleBandPseudoColorRenderer renderer( &interpolator, 0, sh ); // takes ownership of sh
renderer.setClassificationMin( scalarSettings.classificationMinimum() );
renderer.setClassificationMax( scalarSettings.classificationMaximum() );

View File

@ -34,6 +34,7 @@ class QgsSymbol;
#include "qgstriangularmesh.h"
#include "qgsmeshlayer.h"
#include "qgssymbol.h"
#include "qgsmeshdataprovider.h"
///@cond PRIVATE
@ -69,7 +70,6 @@ class QgsMeshLayerRenderer : public QgsMapLayerRenderer
void renderVectorDataset();
void copyScalarDatasetValues( QgsMeshLayer *layer );
void copyVectorDatasetValues( QgsMeshLayer *layer );
void createMeshSymbol( std::unique_ptr<QgsSymbol> &symbol, const QgsMeshRendererMeshSettings &settings );
void calculateOutputSize();
@ -85,6 +85,7 @@ class QgsMeshLayerRenderer : public QgsMapLayerRenderer
// copy of the scalar dataset
QVector<double> mScalarDatasetValues;
QVector<bool> mScalarActiveFaceFlagValues;
bool mScalarDataOnVertices = true;
double mScalarDatasetMinimum = std::numeric_limits<double>::quiet_NaN();
double mScalarDatasetMaximum = std::numeric_limits<double>::quiet_NaN();

View File

@ -407,5 +407,12 @@ QgsMeshDatasetValue QgsMeshMemoryDataProvider::datasetValue( QgsMeshDatasetIndex
}
}
bool QgsMeshMemoryDataProvider::faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const
{
Q_UNUSED( index );
Q_UNUSED( faceIndex );
return true;
}
///@endcond

View File

@ -128,6 +128,7 @@ class QgsMeshMemoryDataProvider: public QgsMeshDataProvider
QgsMeshDatasetGroupMetadata datasetGroupMetadata( int groupIndex ) const override;
QgsMeshDatasetMetadata datasetMetadata( QgsMeshDatasetIndex index ) const override;
QgsMeshDatasetValue datasetValue( QgsMeshDatasetIndex index, int valueIndex ) const override;
bool faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const override;
//! Returns the memory provider key
static QString providerKey();

View File

@ -216,6 +216,19 @@ QgsMeshDatasetValue QgsMdalProvider::datasetValue( QgsMeshDatasetIndex index, in
return val;
}
bool QgsMdalProvider::faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const
{
DatasetGroupH group = MDAL_M_datasetGroup( mMeshH, index.group() );
if ( !group )
return false;
DatasetH dataset = MDAL_G_dataset( group, index.dataset() );
if ( !dataset )
return false;
return MDAL_D_active( dataset, faceIndex );
}
/*----------------------------------------------------------------------------------------------*/
/**

View File

@ -64,6 +64,7 @@ class QgsMdalProvider : public QgsMeshDataProvider
QgsMeshDatasetGroupMetadata datasetGroupMetadata( int groupIndex ) const override;
QgsMeshDatasetMetadata datasetMetadata( QgsMeshDatasetIndex index ) const override;
QgsMeshDatasetValue datasetValue( QgsMeshDatasetIndex index, int valueIndex ) const override;
bool faceIsActive( QgsMeshDatasetIndex index, int faceIndex ) const override;
private:
MeshH mMeshH;

View File

@ -55,6 +55,7 @@ class TestQgsMeshLayer : public QObject
void test_read_vertex_vector_dataset();
void test_read_face_scalar_dataset();
void test_read_face_vector_dataset();
void test_read_vertex_scalar_dataset_with_inactive_face();
void test_extent();
};
@ -178,6 +179,8 @@ void TestQgsMeshLayer::test_read_vertex_scalar_dataset()
QCOMPARE( QgsMeshDatasetValue( 3.0 + i ), dp->datasetValue( ds, 2 ) );
QCOMPARE( QgsMeshDatasetValue( 2.0 + i ), dp->datasetValue( ds, 3 ) );
QCOMPARE( QgsMeshDatasetValue( 1.0 + i ), dp->datasetValue( ds, 4 ) );
QVERIFY( dp->faceIsActive( ds, 0 ) );
}
}
}
@ -216,6 +219,8 @@ void TestQgsMeshLayer::test_read_vertex_vector_dataset()
QCOMPARE( QgsMeshDatasetValue( 3 + i, 2 + i ), dp->datasetValue( ds, 2 ) );
QCOMPARE( QgsMeshDatasetValue( 2 + i, 2 + i ), dp->datasetValue( ds, 3 ) );
QCOMPARE( QgsMeshDatasetValue( 1 + i, -2 + i ), dp->datasetValue( ds, 4 ) );
QVERIFY( dp->faceIsActive( ds, 0 ) );
}
}
}
@ -251,6 +256,8 @@ void TestQgsMeshLayer::test_read_face_scalar_dataset()
// We have 2 values, since dp->faceCount() = 2
QCOMPARE( QgsMeshDatasetValue( 1 + i ), dp->datasetValue( ds, 0 ) );
QCOMPARE( QgsMeshDatasetValue( 2 + i ), dp->datasetValue( ds, 1 ) );
QVERIFY( dp->faceIsActive( ds, 0 ) );
}
}
}
@ -287,10 +294,47 @@ void TestQgsMeshLayer::test_read_face_vector_dataset()
// We have 2 values, since dp->faceCount() = 2
QCOMPARE( QgsMeshDatasetValue( 1 + i, 1 + i ), dp->datasetValue( ds, 0 ) );
QCOMPARE( QgsMeshDatasetValue( 2 + i, 2 + i ), dp->datasetValue( ds, 1 ) );
QVERIFY( dp->faceIsActive( ds, 0 ) );
}
}
}
void TestQgsMeshLayer::test_read_vertex_scalar_dataset_with_inactive_face()
{
QString uri( mDataDir + "/quad_and_triangle.2dm" );
QgsMeshLayer layer( uri, "Triangle and Quad MDAL", "mdal" );
layer.dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_scalar_with_inactive_face.dat" );
QgsMeshDataProvider *dp = layer.dataProvider();
QVERIFY( dp != nullptr );
QVERIFY( dp->isValid() );
QCOMPARE( 1, dp->datasetGroupCount() );
for ( int i = 0; i < 2 ; ++i )
{
QgsMeshDatasetIndex ds( 0, i );
const QgsMeshDatasetGroupMetadata meta = dp->datasetGroupMetadata( ds );
QCOMPARE( meta.name(), QString( "VertexScalarDatasetWithInactiveFace1" ) );
QVERIFY( meta.isScalar() );
QVERIFY( meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices );
const QgsMeshDatasetMetadata dmeta = dp->datasetMetadata( ds );
QVERIFY( qgsDoubleNear( dmeta.time(), i ) );
QVERIFY( dmeta.isValid() );
// We have 5 values, since dp->vertexCount() = 5
QCOMPARE( QgsMeshDatasetValue( 1.0 + i ), dp->datasetValue( ds, 0 ) );
QCOMPARE( QgsMeshDatasetValue( 2.0 + i ), dp->datasetValue( ds, 1 ) );
QCOMPARE( QgsMeshDatasetValue( 3.0 + i ), dp->datasetValue( ds, 2 ) );
QCOMPARE( QgsMeshDatasetValue( 2.0 + i ), dp->datasetValue( ds, 3 ) );
QCOMPARE( QgsMeshDatasetValue( 1.0 + i ), dp->datasetValue( ds, 4 ) );
// We have 2 faces
QVERIFY( !dp->faceIsActive( ds, 0 ) );
QVERIFY( dp->faceIsActive( ds, 1 ) );
}
}
void TestQgsMeshLayer::test_extent()
{

View File

@ -52,6 +52,7 @@ class TestQgsMeshRenderer : public QObject
private:
QString mDataDir;
QgsMeshLayer *mMemoryLayer = nullptr;
QgsMeshLayer *mMdalLayer = nullptr;
QgsMapSettings *mMapSettings = nullptr;
QString mReport;
@ -60,7 +61,7 @@ class TestQgsMeshRenderer : public QObject
void cleanupTestCase();// will be called after the last testfunction was executed.
void init(); // will be called before each testfunction is executed.
void cleanup() {} // will be called after every testfunction.
bool imageCheck( const QString &testType );
bool imageCheck( const QString &testType, QgsMeshLayer *layer );
QString readFile( const QString &fname ) const;
@ -71,6 +72,7 @@ class TestQgsMeshRenderer : public QObject
void test_vertex_vector_dataset_rendering();
void test_face_scalar_dataset_rendering();
void test_face_vector_dataset_rendering();
void test_vertex_scalar_dataset_with_inactive_face_rendering();
void test_signals();
};
@ -83,6 +85,13 @@ void TestQgsMeshRenderer::init()
rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() );
rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() );
mMemoryLayer->setRendererSettings( rendererSettings );
rendererSettings = mMdalLayer->rendererSettings();
rendererSettings.setActiveScalarDataset();
rendererSettings.setActiveVectorDataset();
rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() );
rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() );
mMdalLayer->setRendererSettings( rendererSettings );
}
void TestQgsMeshRenderer::initTestCase()
@ -98,6 +107,11 @@ void TestQgsMeshRenderer::initTestCase()
mMapSettings = new QgsMapSettings();
// Mdal layer
mMdalLayer = new QgsMeshLayer( mDataDir + "/quad_and_triangle.2dm", "Triangle and Quad Mdal", "mdal" );
mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_scalar_with_inactive_face.dat" );
QVERIFY( mMdalLayer->isValid() );
// Memory layer
mMemoryLayer = new QgsMeshLayer( readFile( "/quad_and_triangle.txt" ), "Triangle and Quad Memory", "mesh_memory" );
mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_vertex_scalar.txt" ) );
@ -105,10 +119,12 @@ void TestQgsMeshRenderer::initTestCase()
mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_face_scalar.txt" ) );
mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_face_vector.txt" ) );
QVERIFY( mMemoryLayer->isValid() );
// Add layers
QgsProject::instance()->addMapLayers(
QList<QgsMapLayer *>() << mMemoryLayer );
QList<QgsMapLayer *>() << mMemoryLayer << mMdalLayer );
mMapSettings->setLayers(
QList<QgsMapLayer *>() << mMemoryLayer );
QList<QgsMapLayer *>() << mMemoryLayer << mMdalLayer );
// here we check that datasets automatically get our default color ramp applied ("Plasma")
QgsMeshDatasetIndex ds( 0, 0 );
@ -143,11 +159,11 @@ QString TestQgsMeshRenderer::readFile( const QString &fname ) const
return uri;
}
bool TestQgsMeshRenderer::imageCheck( const QString &testType )
bool TestQgsMeshRenderer::imageCheck( const QString &testType, QgsMeshLayer *layer )
{
mReport += "<h2>" + testType + "</h2>\n";
mMapSettings->setExtent( mMemoryLayer->extent() );
mMapSettings->setDestinationCrs( mMemoryLayer->crs() );
mMapSettings->setExtent( layer->extent() );
mMapSettings->setDestinationCrs( layer->crs() );
mMapSettings->setOutputDpi( 96 );
QgsRenderChecker myChecker;
myChecker.setControlPathPrefix( QStringLiteral( "mesh" ) );
@ -167,7 +183,7 @@ void TestQgsMeshRenderer::test_native_mesh_rendering()
settings.setLineWidth( 1. );
rendererSettings.setNativeMeshSettings( settings );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_native_mesh" ) );
QVERIFY( imageCheck( "quad_and_triangle_native_mesh", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_triangular_mesh_rendering()
@ -179,7 +195,7 @@ void TestQgsMeshRenderer::test_triangular_mesh_rendering()
settings.setLineWidth( 0.26 );
rendererSettings.setTriangularMeshSettings( settings );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_triangular_mesh" ) );
QVERIFY( imageCheck( "quad_and_triangle_triangular_mesh", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_vertex_scalar_dataset_rendering()
@ -192,7 +208,7 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_rendering()
rendererSettings.setActiveScalarDataset( ds );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset" ) );
QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_vertex_vector_dataset_rendering()
@ -208,7 +224,7 @@ void TestQgsMeshRenderer::test_vertex_vector_dataset_rendering()
rendererSettings.setActiveVectorDataset( ds );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset" ) );
QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_face_scalar_dataset_rendering()
@ -221,7 +237,7 @@ void TestQgsMeshRenderer::test_face_scalar_dataset_rendering()
rendererSettings.setActiveScalarDataset( ds );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_face_scalar_dataset" ) );
QVERIFY( imageCheck( "quad_and_triangle_face_scalar_dataset", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_face_vector_dataset_rendering()
@ -234,7 +250,20 @@ void TestQgsMeshRenderer::test_face_vector_dataset_rendering()
rendererSettings.setActiveVectorDataset( ds );
mMemoryLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_face_vector_dataset" ) );
QVERIFY( imageCheck( "quad_and_triangle_face_vector_dataset", mMemoryLayer ) );
}
void TestQgsMeshRenderer::test_vertex_scalar_dataset_with_inactive_face_rendering()
{
QgsMeshDatasetIndex ds( 0, 1 );
const QgsMeshDatasetGroupMetadata metadata = mMdalLayer->dataProvider()->datasetGroupMetadata( ds );
QVERIFY( metadata.name() == "VertexScalarDatasetWithInactiveFace1" );
QgsMeshRendererSettings rendererSettings = mMdalLayer->rendererSettings();
rendererSettings.setActiveScalarDataset( ds );
mMdalLayer->setRendererSettings( rendererSettings );
QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", mMdalLayer ) );
}
void TestQgsMeshRenderer::test_signals()

View File

@ -0,0 +1,25 @@
DATASET
OBJTYPE "mesh2d"
RT_JULIAN 2433282.500000
BEGSCL
ND 5
NC 2
NAME "VertexScalarDatasetWithInactiveFace1"
TIMEUNITS se
TS 1 0.000000
0
1
1
2
3
2
1
TS 1 3600.000000
0
1
2
3
4
3
2
ENDDS