2019-02-08 00:24:46 +08:00
/*
2022-07-19 19:40:43 +08:00
Copyright ( c ) 2008 - 2022 Jan W . Krieger & Sebastian Isbaner ( contour plot )
2019-02-08 00:24:46 +08:00
This software is free software : you can redistribute it and / or modify
it under the terms of the GNU Lesser General Public License ( LGPL ) as published by
the Free Software Foundation , either version 2.1 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 Lesser General Public License ( LGPL ) for more details .
You should have received a copy of the GNU Lesser General Public License ( LGPL )
along with this program . If not , see < http : //www.gnu.org/licenses/>.
*/
2019-06-20 22:06:31 +08:00
# include "jkqtplotter/graphs/jkqtpcontour.h"
2019-02-08 00:24:46 +08:00
# include "jkqtplotter/jkqtpbaseplotter.h"
2019-05-30 04:40:02 +08:00
# include "jkqtplotter/jkqtpimagetools.h"
# include "jkqtplotter/jkqtptools.h"
# include "jkqtcommon/jkqtpenhancedpainter.h"
2019-02-08 00:24:46 +08:00
# include "jkqtplotter/jkqtplotter.h"
- improved: geometric objects now use an adaptive drawing algorithm to represent curves (before e.g. ellipses were always separated into a fixed number of line-segments)
- improved: constructors and access functions for several geometric objects (e.g. more constructors, additional functions to retrieve parameters in diferent forms, iterators for polygons, ...)
- new: all geometric objects can either be drawn as graphic element (i.e. lines are straight line, even on non-linear axes), or as mathematical curve (i.e. on non-linear axes, lines become the appropriate curve representing the linear function, connecting the given start/end-points). The only exceptions are ellipses (and the derived arcs,pies,chords), which are always drawn as mathematical curves
2020-09-04 05:08:52 +08:00
# include "jkqtcommon/jkqtpgeometrytools.h"
2022-04-22 19:27:31 +08:00
# include "jkqtcommon/jkqttools.h"
2019-02-08 00:24:46 +08:00
# include <QDebug>
# include <QImageWriter>
# include <QFileDialog>
# include <QFileInfo>
# include <QApplication>
# include <QClipboard>
# include <QVector3D>
2019-05-19 04:41:38 +08:00
JKQTPContourPlot : : JKQTPContourPlot ( JKQTBasePlotter * parent ) :
2019-04-22 19:27:50 +08:00
JKQTPMathImage ( parent )
{
ignoreOnPlane = false ;
2019-05-19 04:41:38 +08:00
contourColoringMode = ColorContoursFromPaletteByValue ;
2019-04-22 19:27:50 +08:00
relativeLevels = false ;
2020-09-26 21:58:58 +08:00
initLineStyle ( parent , parentPlotStyle , JKQTPPlotStyleType : : Default ) ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
JKQTPContourPlot : : JKQTPContourPlot ( JKQTPlotter * parent ) :
JKQTPContourPlot ( parent - > getPlotter ( ) )
2019-04-22 19:27:50 +08:00
{
}
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : draw ( JKQTPEnhancedPainter & painter )
2019-02-08 00:24:46 +08:00
{
//qDebug()<<"JKQTPContourPlot::draw";
2019-05-19 04:41:38 +08:00
ensureImageData ( ) ;
int numberOfLevels = contourLevels . size ( ) ;
if ( numberOfLevels < = 0 ) return ;
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
int64_t colChecksum = - 1 ;
if ( data & & Nx * Ny > 0 ) {
2022-04-22 19:27:31 +08:00
colChecksum = static_cast < int64_t > ( jkqtp_checksum ( reinterpret_cast < const char * > ( data ) , static_cast < int64_t > ( Nx ) * static_cast < int64_t > ( Ny ) * static_cast < int64_t > ( getSampleSize ( ) / sizeof ( char ) ) ) ) ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
/*if (parent && parent->getDatastore() && imageColumn>=0) {
colChecksum = static_cast < int64_t > ( parent - > getDatastore ( ) - > getColumnChecksum ( imageColumn ) ) ;
} */
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
if ( contourLinesCache . isEmpty ( ) | | ( contourLinesCachedForChecksum ! = colChecksum ) | | ( contourLinesCachedForChecksum < 0 ) ) { // contour lines are only calculated once
QList < QVector < QLineF > > lines ;
lines . reserve ( contourLevels . size ( ) ) ;
2019-02-08 00:24:46 +08:00
for ( int i = 0 ; i < contourLevels . size ( ) ; + + i ) {
2019-05-19 04:41:38 +08:00
lines . append ( QVector < QLineF > ( 0 ) ) ;
}
this - > calcContourLines ( lines ) ;
contourLinesCache . clear ( ) ;
contourLinesCachedForChecksum = colChecksum ;
for ( const QVector < QLineF > & l : lines ) {
contourLinesCache . push_back ( JKQTPUnifyLinesToPolygons ( l , qMin ( getWidth ( ) / static_cast < double > ( getNx ( ) ) , getHeight ( ) / static_cast < double > ( getNy ( ) ) ) / 4.0 ) ) ;
2019-02-08 00:24:46 +08:00
}
}
// draw lines
painter . save ( ) ; auto __finalpaint = JKQTPFinally ( [ & painter ] ( ) { painter . restore ( ) ; } ) ;
2019-04-22 19:27:50 +08:00
QPen p = getLinePen ( painter , parent ) ;
2019-02-08 00:24:46 +08:00
painter . setPen ( p ) ;
2019-05-19 04:41:38 +08:00
// calculate an image with one pixel per contour level and fill it with the appropriate colors
QImage colorLevels = getPaletteImage ( palette , numberOfLevels ) ; // (contourColoringMode==ContourColoringMode::ColorContoursFromPalette)
if ( contourColoringMode = = ContourColoringMode : : SingleColorContours ) {
for ( int i = 0 ; i < numberOfLevels ; i + + ) colorLevels . setPixel ( i , 0 , getLineColor ( ) . rgba ( ) ) ;
} else if ( contourColoringMode = = ContourColoringMode : : ColorContoursFromPaletteByValue ) {
QImage colorDataLevels = getPaletteImage ( palette , 2000 ) ;
for ( int i = 0 ; i < numberOfLevels ; i + + ) {
colorLevels . setPixel ( i , 0 , colorDataLevels . pixel ( qBound < int > ( 0 , ( internalDataMax - contourLevels . value ( i , 0 ) ) * static_cast < double > ( colorDataLevels . width ( ) ) / ( internalDataMax - internalDataMin ) , colorDataLevels . width ( ) - 1 ) , 0 ) ) ;
}
}
// set override colors
for ( int i = 0 ; i < numberOfLevels ; i + + ) {
if ( contourOverrideColor . contains ( contourLevels [ i ] ) ) {
colorLevels . setPixel ( i , 0 , contourOverrideColor [ contourLevels [ i ] ] . rgba ( ) ) ;
}
}
getDataMinMax ( internalDataMin , internalDataMax ) ;
{
# ifdef JKQTBP_AUTOTIMER
JKQTPAutoOutputTimer jkaat ( QString ( " JKQTPContourPlot::draw() : draw lines ( incl . unify ) " )) ;
# endif
for ( int i = 0 ; i < numberOfLevels ; + + i ) {
//qDebug()<<"============================================================\n== LEVEL "<<i<<"\n============================================================";
QVector < QPolygonF > contourLinesTransformedSingleLevel ;
2019-02-08 00:24:46 +08:00
p . setColor ( QColor ( colorLevels . pixel ( i , 0 ) ) ) ;
painter . setPen ( p ) ;
2019-05-19 04:41:38 +08:00
// transform into plot coordinates
for ( auto polygon = contourLinesCache . at ( i ) . begin ( ) ; polygon ! = contourLinesCache . at ( i ) . end ( ) ; + + polygon ) {
contourLinesTransformedSingleLevel . push_back ( QPolygonF ( ) ) ;
2020-09-09 02:15:33 +08:00
for ( auto & poly : * polygon ) {
contourLinesTransformedSingleLevel . last ( ) . append ( transform ( x + poly . x ( ) / double ( Nx - 1 ) * width , y + poly . y ( ) / double ( Ny - 1 ) * height ) ) ;
2019-05-19 04:41:38 +08:00
}
//qDebug()<<lineTranformed;
}
2020-09-09 02:15:33 +08:00
for ( const QPolygonF & poly : contourLinesTransformedSingleLevel ) {
painter . drawPolyline ( poly ) ;
2019-05-19 04:41:38 +08:00
}
2019-02-08 00:24:46 +08:00
}
}
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : createContourLevels ( int nLevels )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
ensureImageData ( ) ;
clearContourLevel ( ) ;
2019-02-08 00:24:46 +08:00
if ( ! data ) return ;
if ( nLevels < 1 ) return ;
double min , max ;
getDataMinMax ( min , max ) ;
double delta = ( max - min ) / static_cast < double > ( nLevels + 1 ) ;
for ( int i = 1 ; i < = nLevels ; + + i ) {
contourLevels . append ( min + i * delta ) ;
}
relativeLevels = false ;
2019-05-19 04:41:38 +08:00
clearCachedContours ( ) ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : createContourLevelsLog ( int nLevels , int m )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
ensureImageData ( ) ;
clearContourLevel ( ) ;
2019-02-08 00:24:46 +08:00
if ( ! data ) return ;
if ( nLevels < 1 ) return ;
double min , max ;
getDataMinMax ( min , max ) ;
if ( min < = 0 ) min = 1 ; // FIXME get smallest number greater zero
int S = floor ( ( log10 ( max ) - log10 ( min ) ) / log10 ( m ) ) ;
if ( S < 2 ) S = 1 ;
int P = floor ( static_cast < double > ( nLevels ) / static_cast < double > ( S ) ) ;
if ( P < 1 ) P = 1 ;
double delta = min ;
contourLevels . append ( 2 * delta ) ;
for ( long s = 0 ; s < S ; s + + ) {
for ( long p = 0 ; p < P ; p + + ) {
{
contourLevels . append ( contourLevels . last ( ) + delta ) ;
}
}
delta = delta * m ;
}
if ( nLevels ! = contourLevels . size ( ) ) {
//qDebug()<<"nLevels="<<nLevels<<"contourLevels.size()="<<contourLevels.size();
//qDebug()<<"adapt m";
}
relativeLevels = false ;
2019-05-19 04:41:38 +08:00
clearCachedContours ( ) ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : setIgnoreOnPlane ( bool __value )
2019-04-22 19:27:50 +08:00
{
this - > ignoreOnPlane = __value ;
2019-05-19 04:41:38 +08:00
clearCachedContours ( ) ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
bool JKQTPContourPlot : : getIgnoreOnPlane ( ) const
2019-04-22 19:27:50 +08:00
{
return this - > ignoreOnPlane ;
}
2019-05-19 04:41:38 +08:00
int JKQTPContourPlot : : getNumberOfLevels ( ) const
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
return this - > contourLevels . size ( ) ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : setContourColoringMode ( ContourColoringMode __value )
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
this - > contourColoringMode = __value ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
JKQTPContourPlot : : ContourColoringMode JKQTPContourPlot : : getContourColoringMode ( ) const
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
return this - > contourColoringMode ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
QVector < double > JKQTPContourPlot : : getContourLevels ( ) const
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
return this - > contourLevels ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : setRelativeLevels ( bool __value )
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
this - > relativeLevels = __value ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
bool JKQTPContourPlot : : getRelativeLevels ( ) const
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
return this - > relativeLevels ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : addContourLevel ( double level )
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
contourLevels . append ( level ) ;
2019-11-18 20:08:42 +08:00
std : : sort ( contourLevels . begin ( ) , contourLevels . end ( ) ) ;
2019-05-19 04:41:38 +08:00
clearCachedContours ( ) ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : addContourLevel ( double level , QColor overrideColor )
2019-04-22 19:27:50 +08:00
{
2019-05-19 04:41:38 +08:00
addContourLevel ( level ) ;
setOverrideColor ( level , overrideColor ) ;
2019-04-22 19:27:50 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : setOverrideColor ( double level , QColor overrideColor )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
contourOverrideColor [ level ] = overrideColor ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
QColor JKQTPContourPlot : : getOverrideColor ( int level ) const
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
if ( level > = 0 & & level < contourLevels . size ( ) ) {
if ( contourOverrideColor . contains ( contourLevels . at ( level ) ) ) {
return contourOverrideColor . value ( contourLevels . at ( level ) ) ;
}
}
return getLineColor ( ) ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
bool JKQTPContourPlot : : hasOverrideColor ( int level ) const
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
if ( level > = 0 & & level < contourLevels . size ( ) ) {
if ( contourOverrideColor . contains ( contourLevels . at ( level ) ) ) {
return true ;
}
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
return false ;
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : removeOverrideColor ( int level )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
if ( level > = 0 & & level < contourLevels . size ( ) ) {
if ( contourOverrideColor . contains ( contourLevels . at ( level ) ) ) {
contourOverrideColor . remove ( contourLevels . at ( level ) ) ;
}
2019-02-08 00:24:46 +08:00
}
}
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : clearContourLevel ( )
{
contourLevels . clear ( ) ;
contourOverrideColor . clear ( ) ;
clearCachedContours ( ) ;
}
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : clearCachedContours ( )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
contourLinesCache . clear ( ) ;
contourLinesCachedForChecksum = - 1 ;
}
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
void JKQTPContourPlot : : calcContourLines ( QList < QVector < QLineF > > & ContourLines )
2019-02-08 00:24:46 +08:00
{
2019-05-19 04:41:38 +08:00
# ifdef JKQTBP_AUTOTIMER
JKQTPAutoOutputTimer jkaat ( QString ( " JKQTPContourPlot::calcContourLines() " )) ;
# else
2019-06-21 21:46:53 +08:00
//qDebug()<<"JKQTPContourPlot::calcContourLines()";
2019-05-19 04:41:38 +08:00
# endif
2019-02-08 00:24:46 +08:00
double scale = 1 ; ///< scale of the contour levels;
if ( relativeLevels ) {
double min ;
double max ;
getDataMinMax ( min , max ) ;
scale = 1 / ( max - min ) ;
}
enum Position
{
// the positions of points of one box
// vertex 1 +-------------------+ vertex 2
// | \ / |
// | \ m=3 / |
// | \ / |
// | \ / |
// | m=2 X m=2 | the center is vertex 0
// | / \ |
// | / \ |
// | / m=1 \ |
// | / \ |
// vertex 4 +-------------------+ vertex 3
Center = 0 ,
TopLeft = 1 ,
TopRight = 2 ,
BottomRight = 3 ,
BottomLeft = 4 ,
NumPositions = 5
} ;
for ( int yp = 0 ; yp < ( int64_t ) getNy ( ) - 1 ; + + yp ) { // go through image (pixel coordinates) in row major order
QVector < QVector3D > vertices ( NumPositions ) ;
2019-05-19 04:41:38 +08:00
for ( int xp = 0 ; xp < ( int64_t ) getNx ( ) - 1 ; + + xp ) {
2019-02-08 00:24:46 +08:00
if ( xp = = 0 )
{
vertices [ TopRight ] . setX ( xp ) ; // will be used for TopLeft later
vertices [ TopRight ] . setY ( yp ) ;
vertices [ TopRight ] . setZ (
2019-05-19 04:41:38 +08:00
getPixelValue ( vertices [ TopRight ] . x ( ) , vertices [ TopRight ] . y ( ) ) * scale
2019-02-08 00:24:46 +08:00
) ;
vertices [ BottomRight ] . setX ( xp ) ;
vertices [ BottomRight ] . setY ( yp + 1 ) ;
vertices [ BottomRight ] . setZ (
2019-05-19 04:41:38 +08:00
getPixelValue ( vertices [ BottomRight ] . x ( ) , vertices [ BottomRight ] . y ( ) ) * scale
2019-02-08 00:24:46 +08:00
) ;
}
vertices [ TopLeft ] = vertices [ TopRight ] ; // use right vertices of the last box as new left vertices
vertices [ BottomLeft ] = vertices [ BottomRight ] ;
vertices [ TopRight ] . setX ( xp + 1 ) ;
vertices [ TopRight ] . setY ( yp ) ; // <----
vertices [ TopRight ] . setZ (
2019-05-19 04:41:38 +08:00
getPixelValue ( vertices [ TopRight ] . x ( ) , vertices [ TopRight ] . y ( ) ) * scale
2019-02-08 00:24:46 +08:00
) ;
vertices [ BottomRight ] . setX ( xp + 1 ) ;
vertices [ BottomRight ] . setY ( yp + 1 ) ;
vertices [ BottomRight ] . setZ (
2019-05-19 04:41:38 +08:00
getPixelValue ( vertices [ BottomRight ] . x ( ) , vertices [ BottomRight ] . y ( ) ) * scale
2019-02-08 00:24:46 +08:00
) ;
double zMin = vertices [ TopLeft ] . z ( ) ;
double zMax = zMin ;
double zSum = zMin ;
for ( int i = TopRight ; i < = BottomLeft ; + + i ) {
const double z = vertices [ i ] . z ( ) ;
zSum + = z ;
if ( z < zMin )
zMin = z ;
if ( z > zMax )
zMax = z ;
}
if ( zMax > = contourLevels . first ( ) & & zMin < = contourLevels . last ( ) ) {
vertices [ Center ] . setX ( xp + 0.5 ) ; // pseudo pixel coordinates
vertices [ Center ] . setY ( yp + 0.5 ) ;
vertices [ Center ] . setZ ( 0.25 * zSum ) ;
for ( int levelIdx = 0 ; levelIdx < contourLevels . size ( ) ; + + levelIdx ) {
if ( contourLevels . at ( levelIdx ) > = zMin & & contourLevels . at ( levelIdx ) < = zMax ) {
QLineF line ;
QVector < QVector3D > triangle ( 3 ) ;
/* triangle[1]
X
/ \
/ \
/ m \
/ \
triangle [ 2 ] + - - - - - - - - - - - - - - - - - - - + triangle [ 0 ]
*/
for ( int m = TopLeft ; m < NumPositions ; m + + ) { // construct triangles
triangle [ 0 ] = vertices [ m ] ;
triangle [ 1 ] = vertices [ Center ] ;
triangle [ 2 ] = vertices [ ( m ! = BottomLeft ) ? ( m + 1 ) : TopLeft ] ;
const bool intersects = intersect ( line , triangle . at ( 0 ) , triangle . at ( 1 ) , triangle . at ( 2 ) ,
contourLevels . at ( levelIdx ) ) ;
if ( intersects ) {
ContourLines [ levelIdx ] < < line ;
}
}
}
}
}
}
}
}
2019-05-19 04:41:38 +08:00
JKQTPColumnContourPlot : : JKQTPColumnContourPlot ( JKQTBasePlotter * parent ) :
JKQTPContourPlot ( parent )
{
2019-08-01 04:10:26 +08:00
this - > datatype = JKQTPMathImageDataType : : DoubleArray ;
2019-05-19 04:41:38 +08:00
}
2019-02-08 00:24:46 +08:00
2019-05-19 04:41:38 +08:00
JKQTPColumnContourPlot : : JKQTPColumnContourPlot ( JKQTPlotter * parent ) :
JKQTPColumnContourPlot ( parent - > getPlotter ( ) )
{
2019-02-08 00:24:46 +08:00
}
2019-05-19 04:41:38 +08:00
void JKQTPColumnContourPlot : : setImageColumn ( int __value )
{
2019-06-22 20:21:32 +08:00
this - > imageColumn = __value ;
if ( parent & & __value > = 0 & & parent - > getDatastore ( ) ) {
setNx ( parent - > getDatastore ( ) - > getColumnImageWidth ( __value ) ) ;
setNy ( parent - > getDatastore ( ) - > getColumnImageHeight ( __value ) ) ;
}
}
void JKQTPColumnContourPlot : : setImageColumn ( size_t __value )
{
setImageColumn ( static_cast < int > ( __value ) ) ;
2019-05-19 04:41:38 +08:00
}
int JKQTPColumnContourPlot : : getImageColumn ( ) const
{
return this - > imageColumn ;
}
bool JKQTPColumnContourPlot : : usesColumn ( int c ) const
{
return ( c = = imageColumn ) ;
}
void JKQTPColumnContourPlot : : ensureImageData ( )
{
if ( this - > Nx = = 0 | | imageColumn < 0 | | ! parent - > getDatastore ( ) - > getColumnPointer ( imageColumn , 0 ) ) {
this - > Ny = 0 ;
this - > data = nullptr ;
2019-08-01 04:10:26 +08:00
this - > datatype = JKQTPMathImageDataType : : DoubleArray ;
2019-05-19 04:41:38 +08:00
} else {
2019-08-01 04:10:26 +08:00
this - > datatype = JKQTPMathImageDataType : : DoubleArray ;
2019-05-19 04:41:38 +08:00
this - > data = parent - > getDatastore ( ) - > getColumnPointer ( imageColumn , 0 ) ;
2019-06-22 20:21:32 +08:00
this - > Ny = static_cast < int > ( parent - > getDatastore ( ) - > getRows ( imageColumn ) / this - > Nx ) ;
2019-05-19 04:41:38 +08:00
}
}