Skip to content

Commit

Permalink
Improve results when using path stroking
Browse files Browse the repository at this point in the history
Use GEOS to do the path stroking
  • Loading branch information
nyalldawson committed Jun 3, 2024
1 parent 65fc7b0 commit be73e84
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@




class QgsGeometryPaintDevice: QPaintDevice
{
%Docstring(signature="appended")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@




class QgsGeometryPaintDevice: QPaintDevice
{
%Docstring(signature="appended")
Expand Down
146 changes: 102 additions & 44 deletions src/core/painting/qgsgeometrypaintdevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ QPaintEngine::Type QgsGeometryPaintEngine::type() const
return QPaintEngine::User;
}

void QgsGeometryPaintEngine::updateState( const QPaintEngineState & )
void QgsGeometryPaintEngine::updateState( const QPaintEngineState &state )
{
if ( mUsePathStroker && state.state().testFlag( QPaintEngine::DirtyFlag::DirtyPen ) )
{
mPen = state.pen();
}
}

void QgsGeometryPaintEngine::drawImage( const QRectF &, const QImage &, const QRectF &, Qt::ImageConversionFlags )
Expand Down Expand Up @@ -320,12 +324,76 @@ void QgsGeometryPaintEngine::drawPolygon( const QPointF *points, int pointCount,
}
}

void QgsGeometryPaintEngine::addStrokedLine( const QgsLineString *line, double penWidth, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, const QTransform *matrix )
{
QgsGeos geos( line );

std::unique_ptr< QgsAbstractGeometry > buffered( geos.buffer( penWidth / 2, mStrokedPathsSegments, endCapStyle, joinStyle, miterLimit ) );
if ( !buffered )
return;

if ( matrix )
buffered->transform( *matrix );

if ( QgsGeometryCollection *bufferedCollection = qgsgeometry_cast< QgsGeometryCollection * >( buffered.get() ) )
{
mGeometry.addGeometries( bufferedCollection->takeGeometries() );
}
else if ( buffered )
{
mGeometry.addGeometry( buffered.release() );
}
}

Qgis::EndCapStyle QgsGeometryPaintEngine::penStyleToCapStyle( Qt::PenCapStyle style )
{
switch ( style )
{
case Qt::FlatCap:
return Qgis::EndCapStyle::Flat;
case Qt::SquareCap:
return Qgis::EndCapStyle::Square;
case Qt::RoundCap:
return Qgis::EndCapStyle::Round;
case Qt::MPenCapStyle:
// undocumented?
break;
}

return Qgis::EndCapStyle::Round;
}

Qgis::JoinStyle QgsGeometryPaintEngine::penStyleToJoinStyle( Qt::PenJoinStyle style )
{
switch ( style )
{
case Qt::MiterJoin:
case Qt::SvgMiterJoin:
return Qgis::JoinStyle::Miter;
case Qt::BevelJoin:
return Qgis::JoinStyle::Bevel;
case Qt::RoundJoin:
return Qgis::JoinStyle::Round;
case Qt::MPenJoinStyle:
// undocumented?
break;
}
return Qgis::JoinStyle::Round;
}

// based on QPainterPath::toSubpathPolygons()
void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath &path, const QTransform &matrix )
void QgsGeometryPaintEngine::addSubpathGeometries( const QPainterPath &path, const QTransform &matrix )
{
if ( path.isEmpty() )
return;

const bool transformIsIdentity = matrix.isIdentity();

const Qgis::EndCapStyle endCapStyle = penStyleToCapStyle( mPen.capStyle() );
const Qgis::JoinStyle joinStyle = penStyleToJoinStyle( mPen.joinStyle() );
const double penWidth = mPen.widthF() <= 0 ? 1 : mPen.widthF();
const double miterLimit = mPen.miterLimit();

QVector< double > currentX;
QVector< double > currentY;
const int count = path.elementCount();
Expand All @@ -343,33 +411,37 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
if ( currentX.size() > 1 )
{
std::unique_ptr< QgsLineString > line = std::make_unique< QgsLineString >( currentX, currentY );
if ( line->isClosed() )
if ( mUsePathStroker )
{
addStrokedLine( line.get(), penWidth, endCapStyle, joinStyle, miterLimit, transformIsIdentity ? nullptr : &matrix );
}
else if ( line->isClosed() )
{
if ( !transformIsIdentity )
line->transform( matrix );
queuedPolygons.emplace_back( std::make_unique< QgsPolygon >( line.release() ) );
}
else
{
collection.addGeometry( line.release() );
if ( !transformIsIdentity )
line->transform( matrix );
mGeometry.addGeometry( line.release() );
}
}
currentX.resize( 0 );
currentY.resize( 0 );

currentX.reserve( 16 );
currentY.reserve( 16 );
double tx, ty;
matrix.map( e.x, e.y, &tx, &ty );
currentX << tx;
currentY << ty;
currentX << e.x;
currentY << e.y;
break;
}

case QPainterPath::LineToElement:
{
double tx, ty;
matrix.map( e.x, e.y, &tx, &ty );
currentX << tx;
currentY << ty;
currentX << e.x;
currentY << e.y;
break;
}

Expand All @@ -381,30 +453,18 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
const double x1 = path.elementAt( i - 1 ).x;
const double y1 = path.elementAt( i - 1 ).y;

double tx1, ty1;
matrix.map( x1, y1, &tx1, &ty1 );

double tx2, ty2;
matrix.map( e.x, e.y, &tx2, &ty2 );

const double x3 = path.elementAt( i + 1 ).x;
const double y3 = path.elementAt( i + 1 ).y;

double tx3, ty3;
matrix.map( x3, y3, &tx3, &ty3 );

const double x4 = path.elementAt( i + 2 ).x;
const double y4 = path.elementAt( i + 2 ).y;

double tx4, ty4;
matrix.map( x4, y4, &tx4, &ty4 );

// TODO -- we could likely reduce the number of segmented points here!
std::unique_ptr< QgsLineString> bezier( QgsLineString::fromBezierCurve(
QgsPoint( tx1, ty1 ),
QgsPoint( tx2, ty2 ),
QgsPoint( tx3, ty3 ),
QgsPoint( tx4, ty4 ) ) );
QgsPoint( x1, y1 ),
QgsPoint( e.x, e.y ),
QgsPoint( x3, y3 ),
QgsPoint( x4, y4 ) ) );

currentX << bezier->xVector();
currentY << bezier->yVector();
Expand All @@ -421,20 +481,28 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
if ( currentX.size() > 1 )
{
std::unique_ptr< QgsLineString > line = std::make_unique< QgsLineString >( currentX, currentY );
if ( line->isClosed() )
if ( mUsePathStroker )
{
addStrokedLine( line.get(), penWidth, endCapStyle, joinStyle, miterLimit, transformIsIdentity ? nullptr : &matrix );
}
else if ( line->isClosed() )
{
if ( !transformIsIdentity )
line->transform( matrix );
queuedPolygons.emplace_back( std::make_unique< QgsPolygon >( line.release() ) );
}
else
{
collection.addGeometry( line.release() );
if ( !transformIsIdentity )
line->transform( matrix );
mGeometry.addGeometry( line.release() );
}
}

if ( queuedPolygons.empty() )
return;

collection.reserve( collection.numGeometries() + queuedPolygons.size() );
mGeometry.reserve( mGeometry.numGeometries() + queuedPolygons.size() );

QgsMultiPolygon tempMultiPolygon;
tempMultiPolygon.reserve( queuedPolygons.size() );
Expand All @@ -451,24 +519,14 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath

for ( auto it = g->const_parts_begin(); it != g->const_parts_end(); ++it )
{
collection.addGeometry( ( *it )->clone() );
mGeometry.addGeometry( ( *it )->clone() );
}
}

void QgsGeometryPaintEngine::drawPath( const QPainterPath &path )
{
QPainterPath realPath = path;
if ( mUsePathStroker )
{
QPen pen = painter()->pen();
QPainterPathStroker stroker( pen );
QPainterPath strokedPath = stroker.createStroke( path );
realPath = strokedPath;
}

QTransform transform = painter()->combinedTransform();

addSubpathGeometries( mGeometry, realPath, transform );
const QTransform transform = painter()->combinedTransform();
addSubpathGeometries( path, transform );
}

//
Expand Down
9 changes: 9 additions & 0 deletions src/core/painting/qgsgeometrypaintdevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#include <QPaintEngine>
#include <memory>

class QgsLineString;

#ifndef SIP_RUN

/**
Expand Down Expand Up @@ -82,8 +84,15 @@ class QgsGeometryPaintEngine: public QPaintEngine

private:

void addSubpathGeometries( const QPainterPath &path, const QTransform &matrix );
void addStrokedLine( const QgsLineString *line, double penWidth, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, const QTransform *matrix );
static Qgis::EndCapStyle penStyleToCapStyle( Qt::PenCapStyle style );
static Qgis::JoinStyle penStyleToJoinStyle( Qt::PenJoinStyle style );

bool mUsePathStroker = false;
QPen mPen;
QgsGeometryCollection mGeometry;
int mStrokedPathsSegments = 8;
};

#endif
Expand Down
26 changes: 16 additions & 10 deletions tests/src/python/test_qgsgeometrypaintdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ def test_lines(self):
)
painter.end()

self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (LineString (5.5 10.7, 6.8 12.9),LineString (15.5 12.7, 3.8 42.9),LineString (-4 -1, 2 3))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (LineString (15.5 12.7, 3.8 42.9),LineString (5.5 10.7, 6.8 12.9),LineString (-4 -1, 2 3))')

self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
Expand Down Expand Up @@ -104,8 +106,10 @@ def test_lines(self):

painter.end()

self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (LineString (11 32.1, 13.6 38.7),LineString (31 38.1, 7.6 128.7),LineString (-8 -3, 4 9))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (LineString (31 38.1, 7.6 128.7),LineString (11 32.1, 13.6 38.7),LineString (-8 -3, 4 9))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
39)
Expand Down Expand Up @@ -134,8 +138,10 @@ def test_stroked_lines(self):

painter.end()

self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (Polygon ((6.15 10.32, 7.45 12.52, 6.15 13.28, 4.85 11.08, 6.15 10.32)),Polygon ((16.2 12.97, 4.5 43.17, 3.1 42.63, 14.8 12.43, 16.2 12.97)),Polygon ((-3.58 -1.62, 2.42 2.38, 1.58 3.62, -4.42 -0.38, -3.58 -1.62)))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 7.45 12.52, 6.15 10.32, 4.85 11.08)),Polygon ((3.1 42.63, 4.5 43.17, 16.2 12.97, 14.8 12.43, 3.1 42.63)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')

self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
Expand Down Expand Up @@ -378,7 +384,7 @@ def test_stroked_polygons(self):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 6.82 13.65, 15.52 13.45, 15.65 11.96, 5.65 9.96, 4.85 11.08),(8.71 12.11, 15.48 11.95, 15.5 12.7, 15.35 13.44, 8.71 12.11),(6.78 12.15, 7.22 12.14, 7.45 12.52, 6.8 12.9, 6.78 12.15),(5.35 11.44, 5.5 10.7, 6.15 10.32, 7 11.76, 5.35 11.44),(7 11.76, 8.71 12.11, 7.22 12.14, 7 11.76)),Polygon ((13.29 11.24, 21.29 35.24, 22.71 34.76, 14.71 10.76, 13.29 11.24)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 6.82 13.65, 15.52 13.45, 15.65 11.96, 5.65 9.96, 4.85 11.08),(7 11.76, 8.71 12.11, 7.22 12.14, 7 11.76)),Polygon ((13.29 11.24, 21.29 35.24, 22.71 34.76, 14.71 10.76, 13.29 11.24)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')

self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
Expand Down Expand Up @@ -409,7 +415,7 @@ def test_stroked_polygons(self):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((9.71 33.24, 12.31 39.84, 13.63 40.95, 31.03 40.35, 31.29 35.89, 11.29 29.89, 9.71 33.24),(17.41 36.32, 30.97 35.85, 31 38.1, 30.71 40.31, 17.41 36.32),(13.57 36.45, 14.44 36.42, 14.89 37.56, 13.6 38.7, 13.57 36.45),(10.71 34.31, 11 32.1, 12.29 30.96, 14 35.29, 10.71 34.31),(14 35.29, 17.41 36.32, 14.44 36.42, 14 35.29)),Polygon ((26.58 33.71, 42.58 105.71, 45.42 104.29, 29.42 32.29, 26.58 33.71)),Polygon ((-8.83 -1.13, 3.17 10.87, 4.83 7.13, -7.17 -4.87, -8.83 -1.13)))')
'GeometryCollection (Polygon ((9.71 33.24, 12.31 39.84, 13.63 40.95, 31.03 40.35, 31.29 35.89, 11.29 29.89, 9.71 33.24),(14 35.29, 17.41 36.32, 14.44 36.42, 14 35.29)),Polygon ((26.58 33.71, 42.58 105.71, 45.42 104.29, 29.42 32.29, 26.58 33.71)),Polygon ((-8.83 -1.13, 3.17 10.87, 4.83 7.13, -7.17 -4.87, -8.83 -1.13)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
54)
Expand Down Expand Up @@ -489,7 +495,7 @@ def test_stroked_paths(self):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.82 10.39, 5.35 11.44, 15.35 13.44, 16.23 12.51, 13.8 3.2, 13.7 2.98, 13.61 2.85, 13.52 2.72, 13.43 2.6, 13.34 2.47, 13.25 2.35, 13.16 2.23, 13.07 2.11, 12.97 1.99, 12.87 1.87, 12.78 1.76, 12.68 1.64, 12.58 1.53, 12.48 1.42, 12.38 1.31, 12.28 1.2, 12.18 1.09, 12.07 0.99, 11.97 0.88, 11.86 0.78, 11.76 0.68, 11.65 0.58, 11.54 0.49, 11.43 0.39, 11.32 0.3, 11.21 0.21, 11.1 0.12, 10.99 0.03, 10.88 -0.05, 10.77 -0.13, 10.66 -0.21, 9.55 0.09, 4.82 10.39),(6.58 10.15, 10.51 1.58, 10.57 1.62, 10.66 1.71, 10.75 1.79, 10.85 1.88, 10.94 1.96, 11.03 2.05, 11.12 2.14, 11.2 2.23, 11.29 2.33, 11.38 2.42, 11.46 2.52, 11.55 2.62, 11.63 2.72, 11.72 2.82, 11.8 2.92, 11.88 3.03, 11.97 3.13, 12.05 3.24, 12.13 3.35, 12.21 3.46, 12.29 3.58, 12.37 3.69, 12.38 3.71, 14.47 11.73, 6.58 10.15),(9.8 1.02, 10.23 0.4, 10.91 0.72, 10.51 1.58, 10.48 1.54, 10.38 1.46, 10.29 1.39, 10.19 1.31, 10.09 1.23, 10 1.16, 9.9 1.09, 9.8 1.02),(14.47 11.73, 15.65 11.96, 15.5 12.7, 14.77 12.89, 14.47 11.73),(12.35 3.58, 13.07 3.39, 12.45 3.81, 12.38 3.71, 12.35 3.58),(5.5 10.7, 5.65 9.96, 6.58 10.15, 6.18 11.01, 5.5 10.7)))')
'GeometryCollection (Polygon ((4.82 10.39, 5.35 11.44, 15.35 13.44, 16.23 12.51, 13.8 3.2, 13.69 2.97, 13.61 2.85, 13.61 2.85, 13.52 2.72, 13.52 2.72, 13.43 2.6, 13.43 2.59, 13.34 2.47, 13.34 2.47, 13.25 2.35, 13.25 2.34, 13.16 2.23, 13.16 2.22, 13.07 2.11, 13.06 2.1, 12.97 1.99, 12.97 1.98, 12.88 1.87, 12.87 1.87, 12.78 1.76, 12.78 1.75, 12.69 1.64, 12.68 1.64, 12.59 1.53, 12.58 1.52, 12.49 1.42, 12.48 1.41, 12.39 1.31, 12.38 1.3, 12.29 1.2, 12.28 1.2, 12.19 1.1, 12.18 1.09, 12.08 0.99, 12.08 0.99, 11.98 0.89, 11.97 0.88, 11.87 0.79, 11.87 0.78, 11.77 0.69, 11.76 0.68, 11.66 0.59, 11.66 0.59, 11.56 0.5, 11.55 0.49, 11.45 0.4, 11.44 0.4, 11.34 0.31, 11.33 0.3, 11.23 0.22, 11.22 0.21, 11.12 0.13, 11.11 0.12, 11 0.04, 11 0.04, 10.89 -0.04, 10.88 -0.05, 10.78 -0.13, 10.77 -0.13, 10.66 -0.21, 9.55 0.09, 4.82 10.39),(6.58 10.15, 10.51 1.58, 10.56 1.62, 10.65 1.7, 10.75 1.79, 10.84 1.87, 10.93 1.96, 11.02 2.05, 11.11 2.14, 11.2 2.23, 11.29 2.33, 11.37 2.42, 11.46 2.52, 11.55 2.62, 11.63 2.72, 11.72 2.82, 11.8 2.93, 11.88 3.03, 11.97 3.14, 12.05 3.25, 12.13 3.36, 12.21 3.47, 12.29 3.58, 12.37 3.69, 12.38 3.71, 14.47 11.73, 6.58 10.15)))')

self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
Expand All @@ -512,7 +518,7 @@ def test_stroked_paths(self):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((9.64 31.16, 10.71 34.31, 30.71 40.31, 32.45 37.53, 27.59 9.61, 27.39 8.93, 27.22 8.55, 27.04 8.17, 26.87 7.79, 26.69 7.42, 26.5 7.05, 26.32 6.69, 26.13 6.33, 25.94 5.97, 25.75 5.62, 25.56 5.27, 25.36 4.92, 25.16 4.58, 24.96 4.25, 24.76 3.92, 24.56 3.59, 24.35 3.27, 24.14 2.96, 23.93 2.65, 23.72 2.34, 23.51 2.04, 23.3 1.75, 23.08 1.46, 22.87 1.18, 22.65 0.9, 22.43 0.63, 22.21 0.36, 21.99 0.1, 21.77 -0.15, 21.54 -0.39, 21.32 -0.63, 19.09 0.27, 9.64 31.16),(13.16 30.45, 21.03 4.73, 21.14 4.87, 21.33 5.12, 21.51 5.37, 21.69 5.63, 21.87 5.89, 22.05 6.16, 22.23 6.43, 22.41 6.7, 22.58 6.99, 22.76 7.27, 22.93 7.56, 23.1 7.86, 23.27 8.16, 23.44 8.46, 23.6 8.77, 23.77 9.08, 23.93 9.4, 24.1 9.73, 24.26 10.06, 24.42 10.39, 24.58 10.73, 24.74 11.08, 24.76 11.12, 28.94 35.19, 13.16 30.45),(19.6 3.05, 20.46 1.21, 21.82 2.15, 21.03 4.73, 20.95 4.63, 20.76 4.39, 20.57 4.16, 20.38 3.93, 20.19 3.7, 19.99 3.48, 19.79 3.26, 19.6 3.05),(28.94 35.19, 31.29 35.89, 31 38.1, 29.55 38.67, 28.94 35.19),(24.69 10.75, 26.14 10.18, 24.89 11.43, 24.76 11.12, 24.69 10.75),(11 32.1, 11.29 29.89, 13.16 30.45, 12.36 33.04, 11 32.1)))')
'GeometryCollection (Polygon ((9.64 31.16, 10.71 34.31, 30.71 40.31, 32.45 37.53, 27.59 9.61, 27.39 8.92, 27.22 8.56, 27.21 8.54, 27.05 8.17, 27.04 8.15, 26.87 7.8, 26.86 7.78, 26.69 7.42, 26.68 7.4, 26.51 7.05, 26.5 7.03, 26.32 6.69, 26.31 6.67, 26.14 6.33, 26.12 6.31, 25.95 5.97, 25.94 5.95, 25.76 5.62, 25.75 5.6, 25.57 5.27, 25.55 5.25, 25.37 4.93, 25.36 4.91, 25.18 4.59, 25.16 4.57, 24.98 4.26, 24.97 4.24, 24.78 3.93, 24.77 3.91, 24.58 3.61, 24.56 3.59, 24.37 3.29, 24.36 3.27, 24.17 2.98, 24.15 2.96, 23.96 2.67, 23.95 2.65, 23.75 2.37, 23.74 2.35, 23.54 2.07, 23.52 2.05, 23.33 1.78, 23.31 1.76, 23.11 1.49, 23.1 1.47, 22.89 1.2, 22.88 1.19, 22.67 0.93, 22.66 0.91, 22.45 0.66, 22.44 0.64, 22.23 0.39, 22.22 0.37, 22.01 0.13, 21.99 0.11, 21.78 -0.13, 21.77 -0.15, 21.56 -0.38, 21.54 -0.4, 21.33 -0.62, 19.09 0.27, 9.64 31.16),(13.16 30.45, 21.03 4.73, 21.12 4.85, 21.31 5.1, 21.49 5.36, 21.67 5.62, 21.86 5.88, 22.04 6.15, 22.22 6.42, 22.39 6.7, 22.57 6.98, 22.75 7.27, 22.92 7.56, 23.09 7.86, 23.26 8.16, 23.43 8.47, 23.6 8.78, 23.77 9.09, 23.93 9.41, 24.1 9.74, 24.26 10.07, 24.42 10.4, 24.58 10.74, 24.74 11.08, 24.76 11.12, 28.94 35.19, 13.16 30.45)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
22)
Expand Down

0 comments on commit be73e84

Please sign in to comment.