Skip to content

Commit 275eb94

Browse files
committed
[FEATURE] Add closest_point and shortest_line expression functions
closest_point: returns closest point a geometry to a second geometry shortest_line: returns the shortest possible line joining two geometries
1 parent 6fcb3ea commit 275eb94

File tree

10 files changed

+257
-0
lines changed

10 files changed

+257
-0
lines changed

python/core/geometry/qgsgeometry.sip

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,18 @@ class QgsGeometry
240240
*/
241241
double sqrDistToVertexAt( QgsPoint& point /In/, int atVertex ) const;
242242

243+
/** Returns the nearest point on this geometry to another geometry.
244+
* @note added in QGIS 2.14
245+
* @see shortestLine()
246+
*/
247+
QgsGeometry nearestPoint( const QgsGeometry& other ) const;
248+
249+
/** Returns the shortest line joining this geometry to another geometry.
250+
* @note added in QGIS 2.14
251+
* @see nearestPoint()
252+
*/
253+
QgsGeometry shortestLine( const QgsGeometry& other ) const;
254+
243255
/**
244256
* Searches for the closest vertex in this geometry to the given point.
245257
* @param point Specifiest the point for search
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "closest_point",
3+
"type": "function",
4+
"description": "Returns the point on geometry 1 that is closest to geometry 2.",
5+
"arguments": [
6+
{"arg":"geometry 1","description":"geometry to find closest point on"},
7+
{"arg":"geometry 2","description":"geometry to find closest point to"}
8+
],
9+
"examples": [
10+
{
11+
"expression":"geom_to_wkt(closest_point(geom_from_wkt('LINESTRING (20 80, 98 190, 110 180, 50 75 )'),geom_from_wkt('POINT(100 100)')))",
12+
"returns":"Point(73.0769 115.384)"
13+
}
14+
]
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "shortest_line",
3+
"type": "function",
4+
"description": "Returns the shortest line joining geometry 1 to geometry 2. The resultant line will start at geometry 1 and end at geometry 2.",
5+
"arguments": [
6+
{"arg":"geometry 1","description":"geometry to find shortest line from"},
7+
{"arg":"geometry 2","description":"geometry to find shortest line to"}
8+
],
9+
"examples": [
10+
{
11+
"expression":"geom_to_wkt(shortest_line(geom_from_wkt('LINESTRING (20 80, 98 190, 110 180, 50 75 )'),geom_from_wkt('POINT(100 100)')))",
12+
"returns":"LineString(73.0769 115.384, 100 100)"
13+
}
14+
]
15+
}

src/core/geometry/qgsgeometry.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,18 @@ double QgsGeometry::sqrDistToVertexAt( QgsPoint& point, int atVertex ) const
502502
return QgsGeometryUtils::sqrDistance2D( QgsPointV2( vertexPoint.x(), vertexPoint.y() ), QgsPointV2( point.x(), point.y() ) );
503503
}
504504

505+
QgsGeometry QgsGeometry::nearestPoint( const QgsGeometry& other ) const
506+
{
507+
QgsGeos geos( d->geometry );
508+
return geos.closestPoint( other );
509+
}
510+
511+
QgsGeometry QgsGeometry::shortestLine( const QgsGeometry& other ) const
512+
{
513+
QgsGeos geos( d->geometry );
514+
return geos.shortestLine( other );
515+
}
516+
505517
double QgsGeometry::closestVertexWithContext( const QgsPoint& point, int& atVertex ) const
506518
{
507519
if ( !d->geometry )

src/core/geometry/qgsgeometry.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ class CORE_EXPORT QgsGeometry
285285
*/
286286
double sqrDistToVertexAt( QgsPoint& point, int atVertex ) const;
287287

288+
/** Returns the nearest point on this geometry to another geometry.
289+
* @note added in QGIS 2.14
290+
* @see shortestLine()
291+
*/
292+
QgsGeometry nearestPoint( const QgsGeometry& other ) const;
293+
294+
/** Returns the shortest line joining this geometry to another geometry.
295+
* @note added in QGIS 2.14
296+
* @see nearestPoint()
297+
*/
298+
QgsGeometry shortestLine( const QgsGeometry& other ) const;
299+
288300
/**
289301
* Searches for the closest vertex in this geometry to the given point.
290302
* @param point Specifiest the point for search

src/core/geometry/qgsgeos.cpp

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,84 @@ QgsAbstractGeometryV2* QgsGeos::reshapeGeometry( const QgsLineStringV2& reshapeW
17641764
}
17651765
}
17661766

1767+
QgsGeometry QgsGeos::closestPoint( const QgsGeometry& other, QString* errorMsg ) const
1768+
{
1769+
if ( !mGeos || other.isEmpty() )
1770+
{
1771+
return QgsGeometry();
1772+
}
1773+
1774+
GEOSGeomScopedPtr otherGeom( asGeos( other.geometry(), mPrecision ) );
1775+
if ( !otherGeom )
1776+
{
1777+
return QgsGeometry();
1778+
}
1779+
1780+
double nx = 0.0;
1781+
double ny = 0.0;
1782+
try
1783+
{
1784+
GEOSCoordSequence* nearestCoord = GEOSNearestPoints_r( geosinit.ctxt, mGeos, otherGeom.get() );
1785+
1786+
( void )GEOSCoordSeq_getX_r( geosinit.ctxt, nearestCoord, 0, &nx );
1787+
( void )GEOSCoordSeq_getY_r( geosinit.ctxt, nearestCoord, 0, &ny );
1788+
GEOSCoordSeq_destroy_r( geosinit.ctxt, nearestCoord );
1789+
}
1790+
catch ( GEOSException &e )
1791+
{
1792+
if ( errorMsg )
1793+
{
1794+
*errorMsg = e.what();
1795+
}
1796+
return QgsGeometry();
1797+
}
1798+
1799+
return QgsGeometry( new QgsPointV2( nx, ny ) );
1800+
}
1801+
1802+
QgsGeometry QgsGeos::shortestLine( const QgsGeometry& other, QString* errorMsg ) const
1803+
{
1804+
if ( !mGeos || other.isEmpty() )
1805+
{
1806+
return QgsGeometry();
1807+
}
1808+
1809+
GEOSGeomScopedPtr otherGeom( asGeos( other.geometry(), mPrecision ) );
1810+
if ( !otherGeom )
1811+
{
1812+
return QgsGeometry();
1813+
}
1814+
1815+
double nx1 = 0.0;
1816+
double ny1 = 0.0;
1817+
double nx2 = 0.0;
1818+
double ny2 = 0.0;
1819+
try
1820+
{
1821+
GEOSCoordSequence* nearestCoord = GEOSNearestPoints_r( geosinit.ctxt, mGeos, otherGeom.get() );
1822+
1823+
( void )GEOSCoordSeq_getX_r( geosinit.ctxt, nearestCoord, 0, &nx1 );
1824+
( void )GEOSCoordSeq_getY_r( geosinit.ctxt, nearestCoord, 0, &ny1 );
1825+
( void )GEOSCoordSeq_getX_r( geosinit.ctxt, nearestCoord, 1, &nx2 );
1826+
( void )GEOSCoordSeq_getY_r( geosinit.ctxt, nearestCoord, 1, &ny2 );
1827+
1828+
GEOSCoordSeq_destroy_r( geosinit.ctxt, nearestCoord );
1829+
}
1830+
catch ( GEOSException &e )
1831+
{
1832+
if ( errorMsg )
1833+
{
1834+
*errorMsg = e.what();
1835+
}
1836+
return QgsGeometry();
1837+
}
1838+
1839+
QgsLineStringV2* line = new QgsLineStringV2();
1840+
line->addVertex( QgsPointV2( nx1, ny1 ) );
1841+
line->addVertex( QgsPointV2( nx2, ny2 ) );
1842+
return QgsGeometry( line );
1843+
}
1844+
17671845
GEOSGeometry* QgsGeos::reshapeLine( const GEOSGeometry* line, const GEOSGeometry* reshapeLineGeos , double precision )
17681846
{
17691847
if ( !line || !reshapeLineGeos )

src/core/geometry/qgsgeos.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ email : marco.hugentobler at sourcepole dot com
1818

1919
#include "qgsgeometryengine.h"
2020
#include "qgspointv2.h"
21+
#include "qgsgeometry.h"
2122
#include <geos_c.h>
2223

2324
class QgsLineStringV2;
@@ -86,6 +87,18 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
8687
QgsAbstractGeometryV2* offsetCurve( double distance, int segments, int joinStyle, double mitreLimit, QString* errorMsg = nullptr ) const override;
8788
QgsAbstractGeometryV2* reshapeGeometry( const QgsLineStringV2& reshapeWithLine, int* errorCode, QString* errorMsg = nullptr ) const;
8889

90+
/** Returns the closest point on the geometry to the other geometry.
91+
* @note added in QGIS 2.14
92+
* @see shortestLine()
93+
*/
94+
QgsGeometry closestPoint( const QgsGeometry& other, QString* errorMsg = nullptr ) const;
95+
96+
/** Returns the shortest line joining this geometry to the other geometry.
97+
* @note added in QGIS 2.14
98+
* @see closestPoint()
99+
*/
100+
QgsGeometry shortestLine( const QgsGeometry& other, QString* errorMsg = nullptr ) const;
101+
89102
/** Create a geometry from a GEOSGeometry
90103
* @param geos GEOSGeometry. Ownership is NOT transferred.
91104
*/

src/core/qgsexpression.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,6 +2168,28 @@ static QVariant fcnExtrude( const QVariantList& values, const QgsExpressionConte
21682168
return result;
21692169
}
21702170

2171+
static QVariant fcnClosestPoint( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
2172+
{
2173+
QgsGeometry fromGeom = getGeometry( values.at( 0 ), parent );
2174+
QgsGeometry toGeom = getGeometry( values.at( 1 ), parent );
2175+
2176+
QgsGeometry geom = fromGeom.nearestPoint( toGeom );
2177+
2178+
QVariant result = !geom.isEmpty() ? QVariant::fromValue( geom ) : QVariant();
2179+
return result;
2180+
}
2181+
2182+
static QVariant fcnShortestLine( const QVariantList& values, const QgsExpressionContext*, QgsExpression* parent )
2183+
{
2184+
QgsGeometry fromGeom = getGeometry( values.at( 0 ), parent );
2185+
QgsGeometry toGeom = getGeometry( values.at( 1 ), parent );
2186+
2187+
QgsGeometry geom = fromGeom.shortestLine( toGeom );
2188+
2189+
QVariant result = !geom.isEmpty() ? QVariant::fromValue( geom ) : QVariant();
2190+
return result;
2191+
}
2192+
21712193
static QVariant fcnRound( const QVariantList& values, const QgsExpressionContext *, QgsExpression* parent )
21722194
{
21732195
if ( values.length() == 2 )
@@ -2765,6 +2787,7 @@ const QStringList& QgsExpression::BuiltinFunctions()
27652787
<< "overlaps" << "within" << "buffer" << "centroid" << "bounds" << "reverse" << "exterior_ring"
27662788
<< "bounds_width" << "bounds_height" << "is_closed" << "convex_hull" << "difference"
27672789
<< "distance" << "intersection" << "sym_difference" << "combine"
2790+
<< "extrude" << "azimuth" << "closest_point" << "shortest_line"
27682791
<< "union" << "geom_to_wkt" << "geomToWKT" << "geometry"
27692792
<< "transform" << "get_feature" << "getFeature"
27702793
<< "levenshtein" << "longest_common_substring" << "hamming_distance"
@@ -2931,6 +2954,8 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
29312954
<< new StaticFunction( "geometry", 1, fcnGetGeometry, "GeometryGroup", QString(), true )
29322955
<< new StaticFunction( "transform", 3, fcnTransformGeometry, "GeometryGroup" )
29332956
<< new StaticFunction( "extrude", 3, fcnExtrude, "GeometryGroup", QString() )
2957+
<< new StaticFunction( "closest_point", 2, fcnClosestPoint, "GeometryGroup" )
2958+
<< new StaticFunction( "shortest_line", 2, fcnShortestLine, "GeometryGroup" )
29342959
<< new StaticFunction( "$rownum", 0, fcnRowNumber, "deprecated" )
29352960
<< new StaticFunction( "$id", 0, fcnFeatureId, "Record" )
29362961
<< new StaticFunction( "$currentfeature", 0, fcnFeature, "Record" )

tests/src/core/testqgsexpression.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,15 @@ class TestQgsExpression: public QObject
583583
QTest::newRow( "relate pattern false" ) << "relate( geom_from_wkt( 'LINESTRING(40 40,120 120)' ), geom_from_wkt( 'LINESTRING(40 40,60 120)' ), '**1F002**' )" << false << QVariant( false );
584584
QTest::newRow( "azimuth" ) << "toint(degrees(azimuth( make_point(25, 45), make_point(75, 100)))*1000000)" << false << QVariant( 42273689 );
585585
QTest::newRow( "azimuth" ) << "toint(degrees( azimuth( make_point(75, 100), make_point(25,45) ) )*1000000)" << false << QVariant( 222273689 );
586+
QTest::newRow( "extrude geom" ) << "geom_to_wkt(extrude( geom_from_wkt('LineString( 1 2, 3 2, 4 3)'),1,2))" << false << QVariant( "Polygon ((1 2, 3 2, 4 3, 5 5, 4 4, 2 4, 1 2))" );
587+
QTest::newRow( "extrude not geom" ) << "extrude('g',5,6)" << true << QVariant();
588+
QTest::newRow( "extrude null" ) << "extrude(NULL,5,6)" << false << QVariant();
589+
QTest::newRow( "closest_point geom" ) << "geom_to_wkt(closest_point( geom_from_wkt('LineString( 1 1, 5 1, 5 5 )'),geom_from_wkt('Point( 6 3 )')))" << false << QVariant( "Point (5 3)" );
590+
QTest::newRow( "closest_point not geom" ) << "closest_point('g','b')" << true << QVariant();
591+
QTest::newRow( "closest_point null" ) << "closest_point(NULL,NULL)" << false << QVariant();
592+
QTest::newRow( "shortest_line geom" ) << "geom_to_wkt(shortest_line( geom_from_wkt('LineString( 1 1, 5 1, 5 5 )'),geom_from_wkt('Point( 6 3 )')))" << false << QVariant( "LineString (5 3, 6 3)" );
593+
QTest::newRow( "shortest_line not geom" ) << "shortest_line('g','a')" << true << QVariant();
594+
QTest::newRow( "shortest_line null" ) << "shortest_line(NULL,NULL)" << false << QVariant();
586595

587596
// string functions
588597
QTest::newRow( "lower" ) << "lower('HeLLo')" << false << QVariant( "hello" );

tests/src/python/test_qgsgeometry.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,10 @@ def testTranslate(self):
11921192
wkt = polygon.exportToWkt()
11931193

11941194
def testExtrude(self):
1195+
# test with empty geometry
1196+
g = QgsGeometry()
1197+
self.assertTrue(g.extrude(1, 2).isEmpty())
1198+
11951199
points = [QgsPoint(1, 2), QgsPoint(3, 2), QgsPoint(4, 3)]
11961200
line = QgsGeometry.fromPolyline(points)
11971201
expected = QgsGeometry.fromWkt('Polygon ((1 2, 3 2, 4 3, 5 5, 4 4, 2 4, 1 2))')
@@ -1202,6 +1206,68 @@ def testExtrude(self):
12021206
expected = QgsGeometry.fromWkt('MultiPolygon (((1 2, 3 2, 4 4, 2 4, 1 2)),((4 3, 8 3, 9 5, 5 5, 4 3)))')
12031207
self.assertEqual(multiline.extrude(1, 2).exportToWkt(), expected.exportToWkt())
12041208

1209+
def testNearestPoint(self):
1210+
# test with empty geometries
1211+
g1 = QgsGeometry()
1212+
g2 = QgsGeometry()
1213+
self.assertTrue(g1.nearestPoint(g2).isEmpty())
1214+
g1 = QgsGeometry.fromWkt('LineString( 1 1, 5 1, 5 5 )')
1215+
self.assertTrue(g1.nearestPoint(g2).isEmpty())
1216+
self.assertTrue(g2.nearestPoint(g1).isEmpty())
1217+
1218+
g2 = QgsGeometry.fromWkt('Point( 6 3 )')
1219+
expWkt = 'Point( 5 3 )'
1220+
wkt = g1.nearestPoint(g2).exportToWkt()
1221+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1222+
expWkt = 'Point( 6 3 )'
1223+
wkt = g2.nearestPoint(g1).exportToWkt()
1224+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1225+
1226+
g1 = QgsGeometry.fromWkt('Polygon ((1 1, 5 1, 5 5, 1 5, 1 1))')
1227+
g2 = QgsGeometry.fromWkt('Point( 6 3 )')
1228+
expWkt = 'Point( 5 3 )'
1229+
wkt = g1.nearestPoint(g2).exportToWkt()
1230+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1231+
1232+
expWkt = 'Point( 6 3 )'
1233+
wkt = g2.nearestPoint(g1).exportToWkt()
1234+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1235+
g2 = QgsGeometry.fromWkt('Point( 2 3 )')
1236+
expWkt = 'Point( 2 3 )'
1237+
wkt = g1.nearestPoint(g2).exportToWkt()
1238+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1239+
1240+
def testShortestLine(self):
1241+
# test with empty geometries
1242+
g1 = QgsGeometry()
1243+
g2 = QgsGeometry()
1244+
self.assertTrue(g1.shortestLine(g2).isEmpty())
1245+
g1 = QgsGeometry.fromWkt('LineString( 1 1, 5 1, 5 5 )')
1246+
self.assertTrue(g1.shortestLine(g2).isEmpty())
1247+
self.assertTrue(g2.shortestLine(g1).isEmpty())
1248+
1249+
g2 = QgsGeometry.fromWkt('Point( 6 3 )')
1250+
expWkt = 'LineString( 5 3, 6 3 )'
1251+
wkt = g1.shortestLine(g2).exportToWkt()
1252+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1253+
expWkt = 'LineString( 6 3, 5 3 )'
1254+
wkt = g2.shortestLine(g1).exportToWkt()
1255+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1256+
1257+
g1 = QgsGeometry.fromWkt('Polygon ((1 1, 5 1, 5 5, 1 5, 1 1))')
1258+
g2 = QgsGeometry.fromWkt('Point( 6 3 )')
1259+
expWkt = 'LineString( 5 3, 6 3 )'
1260+
wkt = g1.shortestLine(g2).exportToWkt()
1261+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1262+
1263+
expWkt = 'LineString( 6 3, 5 3 )'
1264+
wkt = g2.shortestLine(g1).exportToWkt()
1265+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1266+
g2 = QgsGeometry.fromWkt('Point( 2 3 )')
1267+
expWkt = 'LineString( 2 3, 2 3 )'
1268+
wkt = g1.shortestLine(g2).exportToWkt()
1269+
self.assertTrue(compareWkt(expWkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt))
1270+
12051271
def testBoundingBox(self):
12061272
# 2-+-+-+-+-3
12071273
# | |

0 commit comments

Comments
 (0)