tag:blogger.com,1999:blog-24208605293446944492024-03-07T01:46:50.182-08:00Lin.ear th.inkingBecause the shortest distance between two thoughts is a straight lineDr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.comBlogger225125tag:blogger.com,1999:blog-2420860529344694449.post-40436768139514157562023-03-13T14:53:00.010-07:002023-03-13T21:22:00.251-07:00Simplifying Polygonal Coverages with JTS<p>A new capability for the JTS Topology Suite is operations to process <b>Simple Polygonal Coverages</b>. A <a href="http://lin-ear-th-inking.blogspot.com/2022/07/polygonal-coverages-and-operations-in.html">Simple Polygon Coverage</a> is a set of edge-matched, non-overlapping polygonal geometries (which may be non-contiguous, and have holes). Typically this is used to model an area in which every point has a value from some domain. A classic example of a polygonal coverage is a set of administrative boundaries, such as those available from GADM or Natural Earth.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHT0wtC-afVp5cPu64bAiazfSz9e_16FOYXpt2rP7MD90vUHPdN3qwmtscwauElFw9AOXVP9KftN6WRKS6lrkCeuE-AyAy40PEF9uf-oHt04fycLVE-K19eS-H6yOX4w6D3qESjVBLnRuYbsCA6J9olg8iVGsu3woHhQ1QKADN26JebtDaycEjQUEo/s713/poly-cov-simp-france-orig.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="472" data-original-width="713" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjHT0wtC-afVp5cPu64bAiazfSz9e_16FOYXpt2rP7MD90vUHPdN3qwmtscwauElFw9AOXVP9KftN6WRKS6lrkCeuE-AyAy40PEF9uf-oHt04fycLVE-K19eS-H6yOX4w6D3qESjVBLnRuYbsCA6J9olg8iVGsu3woHhQ1QKADN26JebtDaycEjQUEo/w400-h265/poly-cov-simp-france-orig.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i><a href="https://gadm.org/data.html">GADM</a> polygonal coverage for France Level 1 ( 198,350 vertices)</i></div><p>The first coverage operations provided are:</p><p></p><ul style="text-align: left;"><li><a href="http://lin-ear-th-inking.blogspot.com/2022/08/validating-polygonal-coverages-in-jts.html">Coverage Validation</a>, to check if a set of polygons forms a topologically-valid coverage</li><li><a href="http://lin-ear-th-inking.blogspot.com/2023/03/fast-coverage-union-in-jts.html">Coverage Union</a>, which takes advantage of coverage topology to provide a very fast union operation</li></ul><p></p><p>Another operation on polygonal coverages is <b>simplification</b>. Simplifying a coverage reduces the number of vertices it contains, while preserving the coverage topology. Preserving topology means that the simplified polygons still form a valid coverage, and that polygons which had a common edge in the input coverage (i.e. which were adjacent) still share an edge in the simplified result. Reducing dataset size via simplification can provide more efficient storage and download, and faster visualization at smaller scales.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYz0YcqnVxh4Q83eSwU5p6lyLxhB1lcUCKDOuXCyV6LFxdJbU04ILLjFzN-FY7FCU_403o7azXMkQmAaZgvPQ1COqtVXzhGoPM39bcnzLwJ4HmQd99jSLJfRY360Wt6NxwRyeMASbLgX8a4MkShlKRQItWARNvwy4RToXLPMTLMJ-NAmNViW7aGFIT/s683/poly-cov-simp-fra-0p01.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="462" data-original-width="683" height="270" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYz0YcqnVxh4Q83eSwU5p6lyLxhB1lcUCKDOuXCyV6LFxdJbU04ILLjFzN-FY7FCU_403o7azXMkQmAaZgvPQ1COqtVXzhGoPM39bcnzLwJ4HmQd99jSLJfRY360Wt6NxwRyeMASbLgX8a4MkShlKRQItWARNvwy4RToXLPMTLMJ-NAmNViW7aGFIT/w400-h270/poly-cov-simp-fra-0p01.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>France coverage simplified with tolerance 0.01 (7,918 vertices)</i></div><div class="separator" style="clear: both; text-align: center;"><i>The decrease in resolution is hardly noticeable at this scale</i></div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOUXPqNhh5loOWlVQSXjBFGd3xiVDhZFUYCCnwepVmthfx-tOBf6rh0jkMPoBpnIWA_2Cj5CDf-uwBLGkUfDV4DLCzKn2OeAmJGPKbuGVgMKTMWretq6RHJq0fZUN5hpbqvYZSuglwSjCnTeivrqgS6qA-GOI7h-TFYZQRCRpTNw99oYGJax21siao/s498/poly-cov-simp-fra-closeup.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="392" data-original-width="498" height="315" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOUXPqNhh5loOWlVQSXjBFGd3xiVDhZFUYCCnwepVmthfx-tOBf6rh0jkMPoBpnIWA_2Cj5CDf-uwBLGkUfDV4DLCzKn2OeAmJGPKbuGVgMKTMWretq6RHJq0fZUN5hpbqvYZSuglwSjCnTeivrqgS6qA-GOI7h-TFYZQRCRpTNw99oYGJax21siao/w400-h315/poly-cov-simp-fra-closeup.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Closeup of simplified coverage (tolerance = 0.01)</i></div><p>Simplification is perhaps the most requested algorithm for polygonal coverages (for example, see GIS StackExchange questions <a href="https://gis.stackexchange.com/questions/83855/simplifying-adjacent-polygons-using-qgis-simplify-geometries-tool">here</a>, <a href="https://gis.stackexchange.com/questions/213377/simplify-geometry-of-multiple-features-without-losing-shape">here</a> and <a href="https://gis.stackexchange.com/questions/325766/geopandas-simplify-results-in-gaps-between-polygons">here</a>.) My colleague Paul Ramsey sometimes calls it the "killer app" for polygonal coverages. Earlier this century there was no easily-available software implementing this capability. Users often had to resort to the <a href="https://gis.stackexchange.com/a/705/14766">complex approach</a> of extracting the linework from the dataset, dissolving it, simplifying the lines (with a tool which would not cause further overlaps), re-polygonizing, and re-attaching feature attribution to the result geometries. </p><p>More recently tooling has emerged to provide this functionality. Simplification is the <i>raison-d'etre</i> of the widely-used and cited <a href="https://mapshaper.org/">MapShaper</a> tool. GRASS has the <a href="https://grass.osgeo.org/grass82/manuals/v.generalize.html"><span style="font-family: courier;">v.generalize</span></a> module. And good old OpenJUMP added <a href="https://ojwiki.soldin.de/index.php?title=Tools#Generalization">Simplify Polygon Coverage</a> a while back. </p><p>JTS has provided the <a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/simplify/TopologyPreservingSimplifier.html"><span style="font-family: courier;">TopologyPreservingSimplifier</span></a> algorithm for many years, but this only operates on Polygons and MultiPolygons, not on polygonal coverages. (Attempting to use it on polygonal coverages can introduce gaps and overlaps, resulting in complaints like <a href="https://gis.stackexchange.com/questions/325766/geopandas-simplify-results-in-gaps-between-polygons">this</a>.) But coverage simplification has been lacking in JTS/GEOS - until now.</p><h3 style="text-align: left;">JTS Coverage Simplification</h3><p>Recent work on <a href="http://lin-ear-th-inking.blogspot.com/2022/04/outer-and-inner-concave-polygon-hulls.html">Polygon Hulls</a> provided ideas for an implementation of coverage simplification. The <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java"><span style="font-family: courier;">CoverageSimplifier</span></a> class uses an area-based simplification approach similar to the well-known <a href="https://en.wikipedia.org/wiki/Visvalingam%E2%80%93Whyatt_algorithm">Visvalingam-Whyatt</a> algorithm. This provides good results for simplifying areal features (as opposed to linear ones). It's possible to use a <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Douglas-Peucker</a> based approach as well, so this may be a future option.</p><p>The degree of simplification is determined by a tolerance value. The value is equivalent roughly to the maximum distance a simplified edge can change (technically speaking, it is the square root of the area tolerance for the Visvalingam-Whyatt algorithm). </p><p>The algorithm progressively simplifies all coverage edges, while ensuring that no edges cross another edge, or touch at endpoints. This provides the maximum amount of simplification (up to the tolerance) while still maintaining the coverage topology.</p><p>The coverage of course should be valid according to the JTS <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageValidator.java"><span style="font-family: courier;">CoverageValidator</span></a> class. Invalid coverages can still be simplified, but only edges with valid topology will have it maintained.</p><p><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmx1iDR1pv44rG5YZ5IeoJhKDX-gSOqK9953_US9MutrV6iG75Sri8eCt1rqpfgEmo41JpCZbihNHjWvhmrcvUDtcPO1omSKX8eCF_gKEYIa5zHbYrp3HhnUKos3CrV4ndIF1KAghcDB0HX9D9YmVB8C58nznBXweFacBIa5RlPVZlYhWcOgRhxzV7/s697/polyy-cov-simp-fra-0p1.png" style="margin-left: 1em; margin-right: 1em; text-align: center;"><img border="0" data-original-height="458" data-original-width="697" height="263" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgmx1iDR1pv44rG5YZ5IeoJhKDX-gSOqK9953_US9MutrV6iG75Sri8eCt1rqpfgEmo41JpCZbihNHjWvhmrcvUDtcPO1omSKX8eCF_gKEYIa5zHbYrp3HhnUKos3CrV4ndIF1KAghcDB0HX9D9YmVB8C58nznBXweFacBIa5RlPVZlYhWcOgRhxzV7/w400-h263/polyy-cov-simp-fra-0p1.png" width="400" /></a></p><div class="separator" style="clear: both; text-align: center;"><i>France coverage simplified with tolerance = 0.1 ( 1,928 vertices)</i></div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsQE1oWQSmDZY1aGzCJDUSuDNL4lfzs94c2Ef0J-U7rTUqve57fY78D3DpWbZfcofgy02xl0yFhOCmHhfgPy4kYUe3GASXfvVxjBTXADjTYRh_y-Pejod0i35gOjoS8m_I7UW0RfGiLSm1baObypa5lSbxuPlsUnkwuyt10RWpXkFpfiumDfxVhtkk/s659/poly-cov-simp-fra-0p5.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="458" data-original-width="659" height="278" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsQE1oWQSmDZY1aGzCJDUSuDNL4lfzs94c2Ef0J-U7rTUqve57fY78D3DpWbZfcofgy02xl0yFhOCmHhfgPy4kYUe3GASXfvVxjBTXADjTYRh_y-Pejod0i35gOjoS8m_I7UW0RfGiLSm1baObypa5lSbxuPlsUnkwuyt10RWpXkFpfiumDfxVhtkk/w400-h278/poly-cov-simp-fra-0p5.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>France coverage simplified with tolerance = 0.5 ( 1,527 vertices)</i></div><div class="separator" style="clear: both; text-align: center;"><i>The coverage topology (adjacency relationship) is always preserved.</i></div><p><br /></p><h3 style="text-align: left;">Inner Simplification</h3><p>The implementation also provides the interesting option of <b>Inner Simplification</b>. This mode simplifies only inner, shared edges, leaving the outer boundary edges unchanged. This allows portions of a coverage to be simplified, by ensuring that the simplified polygons will fit exactly into the original coverage. (This GIS-SE <a href="https://gis.stackexchange.com/questions/364360/simplifying-adjacent-polygons-on-subset-of-vertices-only">question</a> is an example of how this can be used.)</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnqpfWN3ZVVKFo8aP_BOBVGNtCDJuT3ncNeorcouwnNwQ7WFoK62un74ALMh-5OYdqYiafKIW1DxVja0LAgAU1Ga08JGjd_PD4XLEyBkreDP4bwPsBG4lLL-n30E5ZZTCRvQ2dUskCbM-_r1Ae43IEUfhyyHrAw0tBeg5I484S8_0ek-9RQDQ7jdhg/s579/poly-cov-simp-inner-france.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="384" data-original-width="579" height="265" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnqpfWN3ZVVKFo8aP_BOBVGNtCDJuT3ncNeorcouwnNwQ7WFoK62un74ALMh-5OYdqYiafKIW1DxVja0LAgAU1Ga08JGjd_PD4XLEyBkreDP4bwPsBG4lLL-n30E5ZZTCRvQ2dUskCbM-_r1Ae43IEUfhyyHrAw0tBeg5I484S8_0ek-9RQDQ7jdhg/w400-h265/poly-cov-simp-inner-france.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Inner Simplification of France coverage</i></div><div style="text-align: left;"><br /></div><div style="text-align: left;">It would also be possible to provide <b>Outer Simplification</b>, where only outer edges are simplified. It's not clear what the use case would be for this - if you have ideas, leave a comment!</div><h3 style="text-align: left;">GEOS and PostGIS</h3><p>As usual, this algorithm will be ported to <a href="http://libgeos.org/">GEOS</a>, from where it will be available to downstream projects such as <a href="http://www.postgis.org/">PostGIS</a> and <a href="https://github.com/shapely/shapely">Shapely</a>. </p><p>For PostGIS, the intention is to expose this as a <b>window function</b> (perhaps <span style="font-family: courier;">ST_CoverageSimplify</span>). That will make it easy to process a set of features (records) and maintain the attributes for each feature.</p><p><br /></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com3tag:blogger.com,1999:blog-2420860529344694449.post-29423865466886101072023-03-06T12:57:00.003-08:002023-03-06T12:57:38.282-08:00Fast Coverage Union in JTS<p>The next operation delivered in the build-out of <a href="https://lin-ear-th-inking.blogspot.com/2022/07/polygonal-coverages-and-operations-in.html">Simple Polygonal Coverages</a> in the <a href="https://github.com/locationtech/jts"><b>JTS Topology Suite</b></a> is <b>Coverage Union</b>. This is simply the topological union of a set of polygons in a polygonal coverage, producing one or more polygons as the result. (This is sometimes called "dissolve" in the context of polygonal coverages.)</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVpP6TSiPQcqMJNXb_bliXPNZjrQRjCA65JPmbDN8ltPRobxDPrN3PzNEsHb3tS2PcqSEoTvqiPxnLMGnHAVVNdM_XAz_HNfPdiVVkahrp7_7lxdZnWF0xDBvoPFeOmO3jU6T8P3xmObvw5Z-Oa5rXr1hWeMOKv6c3h4dqZPBc_PAWXbfiTvT83ZK5/s528/poly-cov-union-us.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="220" data-original-width="528" height="166" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjVpP6TSiPQcqMJNXb_bliXPNZjrQRjCA65JPmbDN8ltPRobxDPrN3PzNEsHb3tS2PcqSEoTvqiPxnLMGnHAVVNdM_XAz_HNfPdiVVkahrp7_7lxdZnWF0xDBvoPFeOmO3jU6T8P3xmObvw5Z-Oa5rXr1hWeMOKv6c3h4dqZPBc_PAWXbfiTvT83ZK5/w400-h166/poly-cov-union-us.png" width="400" /></a></div><br /><p>Union of polygons has long been available in JTS, most recently (and robustly) as the <a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/operation/overlayng/UnaryUnionNG.html"><span style="font-family: courier;">UnaryUnion</span></a> capability of <a href="https://lin-ear-th-inking.blogspot.com/2020/05/jts-overlay-next-generation.html">OverlayNG</a>. This makes use of the <a href="https://lin-ear-th-inking.blogspot.com/2007/11/fast-polygon-merging-in-jts-using.html">Cascaded Union</a> technique to provide good performance for large sets of polygons. But the constrained structure of polygonal coverages means unions can be computed <b>much</b> faster than even Cascaded Union. Essentially, the duplicated inner edges of adjacent polygons are identified and discarded, leaving only the outer boundary of the unioned area. Because a valid polygonal coverage has edges which match exactly, identifying duplicate segments can be done with a fast equality test. Also, there is no need for computationally-expensive techniques to ensure geometric robustness.</p><p>This is now available in JTS as the <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageUnion.java"><span style="font-family: courier;">CoverageUnion</span></a> class.</p><h3 style="text-align: left;">Performance</h3><p style="text-align: left;">To test the performance of Coverage Union we need some large clean polygonal coverages. These are nicely provided by the <a href="https://gadm.org/data.html">GADM</a> repository of worldwide administrative areas. </p><p style="text-align: left;">Here is some metrics comparing the performance of Coverage Union against OverlayNG Unary Union (which uses the Cascaded Union technique). Coverage Union is much faster for all sizes of dataset.</p><div><br /></div>
<table border="1">
<tbody><tr><th>Dataset</th><th>Polygons</th><th>Vertices</th><th>Coverage Union</th><th>OverlayNG Union</th><th>Times Faster</th></tr>
<tr><td>France level 4</td><td>3,728</td><td>407,102</td><td>0.3 s</td><td>8.5 s</td><td>28 x</td></tr>
<tr><td>France level 5</td><td>36,612</td><td>729,573</td><td>1.07 s</td><td>13.9 s</td><td>13 x</td></tr>
<tr><td>Germany level 4</td><td>11,302</td><td>2,162,184</td><td>0.68 s</td><td>27.3 s</td><td>40 x</td></tr></tbody></table><div style="text-align: left;"><br /></div><h3 style="text-align: left;">Union by Attribute</h3><p>It's worth noting that unioning a set of polygons in a coverage leaves the <b>boundary</b> of the unioned area perfectly <b>unchanged</b>. So subsets of a coverage can be unioned, and the result still forms a valid polygonal coverage. This provides a fast Union by Attribute capability, which is a <a href="https://gis.stackexchange.com/questions/31895/joining-lots-of-small-polygons-to-form-larger-polygon-using-postgis">common</a> <a href="https://medium.com/@michellemho/merging-together-polygons-on-a-common-attribute-5b0b04545601">spatial</a> <a href="https://gis.stackexchange.com/questions/185393/what-is-the-best-way-to-merge-lots-of-small-adjacents-polygons-postgis">requirement</a>. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqcxbihozuRpdkM1Tynz1u6n79QI0JPrabXKVyQSlxe5y55e3F-qoSt9x0I8hNDY2Za_vp9cM1umqHMxmNP7zM-3t7RJeElAC54E6eUSKvpI8ST1NNVcQnYJPrplnSqnvhUCB-IfgarXqLzelspDQXBVUEE0NEgKc6rdowUcCefJgy-CdSEgrTjaai/s477/poly-cov-uion-france.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="336" data-original-width="477" height="281" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhqcxbihozuRpdkM1Tynz1u6n79QI0JPrabXKVyQSlxe5y55e3F-qoSt9x0I8hNDY2Za_vp9cM1umqHMxmNP7zM-3t7RJeElAC54E6eUSKvpI8ST1NNVcQnYJPrplnSqnvhUCB-IfgarXqLzelspDQXBVUEE0NEgKc6rdowUcCefJgy-CdSEgrTjaai/w400-h281/poly-cov-uion-france.png" width="400" /></a></div><h3 style="text-align: left;">GEOS and PostGIS</h3><p><a href="https://libgeos.org/">GEOS</a> already supports a <span style="font-family: courier;"><a href="http://libgeos.org/doxygen/3.11/geos__c_8h.html#ab295260c1abeb373f91b9724e918bcfd">GEOSCoverageUnion</a></span> operation. At some point it would be nice to expose this in <a href="https://postgis.net/">PostGIS</a>, most likely as a new aggregate function (perhaps <span style="font-family: courier;">ST_UnionCoverage</span>).</p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-11855110438240172732023-01-27T13:02:00.002-08:002023-01-27T13:02:42.168-08:00Alpha Shapes in JTS<p>Recently JTS gained the ability to compute <a href="http://lin-ear-th-inking.blogspot.com/2022/01/concave-hulls-in-jts.html">Concave Hulls</a> of point sets. The algorithm used is based the <b>Chi-shapes</b> approach described by <a href="http://www.geosensor.net/papers/duckham08.PR.pdf">Duckham <i>et al</i>.</a> It works by eroding border triangles from the Delaunay Triangulation of the input points, in order of longest triangle edge length, down to a threshold length provided as a parameter value. </p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjOq5sEvrZfdZrYOy_q_ud01Jy0gOx954Roe-RIktqQsgL85Ok0VNBAudIvV0d3j3jREUSExonmCgI9I9mGNiN634Y3UK1uikRx9bUvCXmoyJ6Rgp23hKqZolIvK-cI-pRSDDFJ_MPNbApV-t8olPfy2zz3f2MkMACYLcS-jKLcSID-v1Ao1gbo30jA" style="margin-left: 1em; margin-right: 1em;"></a><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjOq5sEvrZfdZrYOy_q_ud01Jy0gOx954Roe-RIktqQsgL85Ok0VNBAudIvV0d3j3jREUSExonmCgI9I9mGNiN634Y3UK1uikRx9bUvCXmoyJ6Rgp23hKqZolIvK-cI-pRSDDFJ_MPNbApV-t8olPfy2zz3f2MkMACYLcS-jKLcSID-v1Ao1gbo30jA" style="margin-left: 1em; margin-right: 1em;"></a><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjC6SWhmaBtitz-k_ihViMNDYlqgn_9gEkMwQ8Tg06B9i9sEcP4iEuiwwnylOq9TdKYPg4MAeRwyX60TiM8DQB21PIdnTwrKQb_hHxbdlXSXQUDLYEGK-OGTUE_ZF162O2LJTtvsZUJD4DmVPRkeAgLVSFbL6THOe9-WeodcOKEBRzcQ5wEndAF7cPS" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="448" data-original-width="672" height="266" src="https://blogger.googleusercontent.com/img/a/AVvXsEjC6SWhmaBtitz-k_ihViMNDYlqgn_9gEkMwQ8Tg06B9i9sEcP4iEuiwwnylOq9TdKYPg4MAeRwyX60TiM8DQB21PIdnTwrKQb_hHxbdlXSXQUDLYEGK-OGTUE_ZF162O2LJTtvsZUJD4DmVPRkeAgLVSFbL6THOe9-WeodcOKEBRzcQ5wEndAF7cPS=w400-h266" width="400" /></a></div></div><div class="separator" style="clear: both; text-align: center;"><i>Concave Hull of Ukraine (Edge-length = 10)</i></div><p></p><h3 style="text-align: left;">Alpha-Shapes</h3><p>Another popular approach for computing concave hulls is the <b>Alpha-shape</b> construction originally proposed by <a href="https://www.cs.jhu.edu/~misha/Fall13b/Papers/Edelsbrunner93.pdf">Edelsbrunner et al</a>. <a href="https://graphics.stanford.edu/courses/cs268-11-spring/handouts/AlphaShapes/as_fisher.pdf">Papers</a> describing alpha-shapes tend to be lengthy and somewhat opaque theoretical expositions, but the actual algorithm is fairly simple. It is also based on eroding Delaunay triangles, but uses the circumradius of the triangles as the criteria for removal. Border triangles whose circumradius is greater than alpha are removed. </p><p>Another way of understanding this is to imagine triangles on the border of the triangulation being "scooped out" by a disc of radius alpha:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIxA7h8-o5Z-w3dVAVitCUpFe9R5e3Z1Ya_2p85BRaahIbCeThc64JpnCmYDk4DO5zWKNbohfHCcL3hA1PqPc1hpHpO9jo82LaygKZtWjdx1nFjhQO-R5XFewzZVBOEvkNdexcN-CijQwhvzSf2I-oAK3VH-otnwwrX25pYepfI9hDGrE_fmia3AQH/s300/alpha-shape-construction.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="252" data-original-width="300" height="252" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIxA7h8-o5Z-w3dVAVitCUpFe9R5e3Z1Ya_2p85BRaahIbCeThc64JpnCmYDk4DO5zWKNbohfHCcL3hA1PqPc1hpHpO9jo82LaygKZtWjdx1nFjhQO-R5XFewzZVBOEvkNdexcN-CijQwhvzSf2I-oAK3VH-otnwwrX25pYepfI9hDGrE_fmia3AQH/s1600/alpha-shape-construction.png" width="300" /></a></div><div style="text-align: center;"><i>Construction of an Alpha-shape via "scooping" with an alpha-radius disc</i></div><p>Given the similar basis of both algorithms, it was easy to generalize the JTS <span style="font-family: courier;">ConcaveHull</span> class to be able to compute alpha-shapes as well. This is now available in JTS via the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java#L159">ConcaveHull.alphaShape</a></span> function.</p><h3 style="text-align: left;">Seeking Alpha</h3><p>An issue that became apparent was: <b>what is the definition of alpha?</b> There are at least three alternatives in use:</p><p></p><ul style="text-align: left;"><li>The original Edelsbrunner 1983 <a href="https://www.cs.jhu.edu/~misha/Fall13b/Papers/Edelsbrunner93.pdf">paper</a> defines alpha as the <b>inverse of the radius</b> of the eroding disc.</li><li>In a later 1994 <a href="https://www.cs.jhu.edu/~misha/Fall05/Papers/edelsbrunner94.pdf">paper</a> Edelsbrunner defines alpha as the <b>radius</b> of the eroding disc. The same definition is used in his 2010 <a href="http://graphics.stanford.edu/courses/cs268-14-fall/Handouts/AlphaShapes/2010-B-01-AlphaShapes.pdf">survey</a> of alpha shape research.</li><li>The CGAL geometry library <a href="https://doc.cgal.org/latest/Alpha_shapes_2/index.html#I1_SectAlpha_Shape_2">implements</a> alpha to be the <b>square of the radius</b> of the eroding disc. (The derivation of this definition is not obvious to me; perhaps it is intended to make the parameter have areal units?)</li></ul><p></p><p>The simplest option is to define <b>alpha to be the radius of the eroding disc</b>. This has the additional benefit of being congruent with the edge-length parameter. For both parameters, 0 produces a result with maximum concaveness, and for values larger than some (data-dependent) value the result is the convex hull of the input.</p><h3 style="text-align: left;">Alpha-Shape VS Concave Hull</h3><p>An obvious question is: <b>how do alpha-shapes differ to edge-length-based concave hulls?</b> Having both in JTS makes it easy to compare the hulls on various datasets. For comparison purposes the alpha and edge-length parameters are chosen to produce hulls with the same area (as close as possible).</p><p>Here's an illustration of an Alpha-shape and a Concave Hull generated for the same dataset, with essentially identical area.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjBpjmUPse_3T1BBXkLREn0NCSOyxB6Bm0dtCCmym_nO2a8pfD3TMMU1_GiMuck8hTE9zsq8vW4VKWEPkwwW7MaDwnXk59VSDa5rEdhZ4ZWsLNbr34Yl4twwOZm8N1ntEGFC4uChqg012bZUdMBN3sXNDgJbh7sloLJIdnJZtMfgrMB9kPbaF0JS_q7" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="307" data-original-width="611" height="201" src="https://blogger.googleusercontent.com/img/a/AVvXsEjBpjmUPse_3T1BBXkLREn0NCSOyxB6Bm0dtCCmym_nO2a8pfD3TMMU1_GiMuck8hTE9zsq8vW4VKWEPkwwW7MaDwnXk59VSDa5rEdhZ4ZWsLNbr34Yl4twwOZm8N1ntEGFC4uChqg012bZUdMBN3sXNDgJbh7sloLJIdnJZtMfgrMB9kPbaF0JS_q7=w400-h201" width="400" /></a></div>Overall the shapes are fairly similar. Alpha-shapes tend to follow the data points more closely in "shallow bays", whereas the concave hull tends to include them. Conversely, the concave hull can sometimes erode deeper bays. The effect of keeping shallow bays is to make the Concave Hull slightly smoother than the Alpha-shape.<p></p><h3 style="text-align: left;">Avoiding Cavities</h3><p>However, there is one way in which alpha-shapes can be less well-formed than concave hulls. The measurement of a circumradius is independent of the orientation of the triangle. This means it is not sensitive to whether the triangle has a long or short edge on the border of the triangulation. This can result in what I am calling "cavitation". Cavitation occurs where narrow triangles reaching deep into the interior of the dataset are removed. This in turn exposes more interior triangles to being eroded. This is visible in the example below, where the Alpha-shape contains a large cavity which is obviously not desirable. </p><p></p><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEilpagTxHmz47wa2Q-CkMP2R5lqzOkVEyel8C0ehnC_O3abpmrnrcA7XKvLW418k-0qXF2aYHGyx7JR2LjYYL64NCKtSduSkKqZUNY-ZngSorDNKtsirObDUF2NMXNPU8TdUPglCTOn_LWNd0kJKgISeohdB1vW1iUoML5-aBX_piFwZdCD18ZZ8qhU" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="285" data-original-width="502" height="228" src="https://blogger.googleusercontent.com/img/a/AVvXsEilpagTxHmz47wa2Q-CkMP2R5lqzOkVEyel8C0ehnC_O3abpmrnrcA7XKvLW418k-0qXF2aYHGyx7JR2LjYYL64NCKtSduSkKqZUNY-ZngSorDNKtsirObDUF2NMXNPU8TdUPglCTOn_LWNd0kJKgISeohdB1vW1iUoML5-aBX_piFwZdCD18ZZ8qhU=w400-h228" width="400" /></a></div><br /><div><span style="text-align: left;"><div class="separator" style="clear: both; text-align: center;"><i>Alpha-shape with a "cavity"</i></div><div style="text-align: justify;"><br /></div><div style="text-align: justify;">This is shown in more detail below. The highlighted triangle is removed due to its large circumradius, even though it has a relatively small border edge. This in turn exposes more triangles to being removed, forming the undesirable cavity.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEiK-Iqq4pEPqfrTAPW5aQAUw47tX6bKQEy9TCgIUhXdXR00Bqymf9OXmRwlVEZoteRXfSclHBBWjafhmCAdnc7i91JL5dvvTLj56_ardfiJH5WSwB5YN2a0DzW0ds7fXuWudGUD0l_ey4IoCgmBQywW3Za9EF27qCCHXDwzP0e3bf2ZWjXw-UNuDL8O" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="430" data-original-width="557" height="309" src="https://blogger.googleusercontent.com/img/a/AVvXsEiK-Iqq4pEPqfrTAPW5aQAUw47tX6bKQEy9TCgIUhXdXR00Bqymf9OXmRwlVEZoteRXfSclHBBWjafhmCAdnc7i91JL5dvvTLj56_ardfiJH5WSwB5YN2a0DzW0ds7fXuWudGUD0l_ey4IoCgmBQywW3Za9EF27qCCHXDwzP0e3bf2ZWjXw-UNuDL8O=w400-h309" width="400" /></a></div><i>Alpha-shape with Delaunay triangle forming a "cavity"</i><br /><br /></span></div><div style="text-align: justify;"><span style="text-align: left;">The edge-length metric does not have this problem, since it considers only the edges along the (current) border of the triangulation. (In fact, the presence of the cavity in the example above means that the equal-area comparison strategy causes more erosion artifacts in the Concave Hull than might otherwise be present.)</span></div><div style="text-align: justify;"><span style="text-align: left;"><br /></span></div><div style="text-align: justify;"><span style="text-align: left;">For an even more egregious example of this issue, here is an Alpha-shape of the Ukraine dataset:</span></div><div style="text-align: justify;"><span style="text-align: left;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgpQryNULzcx494-wC-lADEx2jyUIpaCAd6n-scaoltI34fTTUvnuhuGSMVJCXjEa7djL_YxBLUWlumCCjKOOJsFnirMoU0elsXeCBj9UrcYheHJciPuGwYX2ZMmHUDwgc5DS3OQGeH5aSYE454ozLALjrUazCKksMOSyDec7mDyrY7pzqa-LupUP-O" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="448" data-original-width="673" height="266" src="https://blogger.googleusercontent.com/img/a/AVvXsEgpQryNULzcx494-wC-lADEx2jyUIpaCAd6n-scaoltI34fTTUvnuhuGSMVJCXjEa7djL_YxBLUWlumCCjKOOJsFnirMoU0elsXeCBj9UrcYheHJciPuGwYX2ZMmHUDwgc5DS3OQGeH5aSYE454ozLALjrUazCKksMOSyDec7mDyrY7pzqa-LupUP-O=w400-h266" width="400" /></a></div><div style="text-align: center;"><i>Alpha-shape of Ukraine (alpha = 7)</i></div></span></div><div style="text-align: justify;"><span style="text-align: left;"><br /></span></div><div style="text-align: left;">The effect of cavitation is obvious. Although it can be eliminated by increasing the alpha radius, note that Crimean Peninsula is already less well-delineated than in the Concave Hull, and increasing alpha would make this worse. </div><h3 style="text-align: justify;"><span style="text-align: left;">Recommendation</span></h3><div style="text-align: justify;"><span style="text-align: left;">Given the risk of undesired cavitation, the safest option for computing Concave Hulls is to use the Edge-Length approach, rather than Alpha-shapes. </span></div><h3 style="text-align: justify;"><span style="text-align: left;">Future Work</span></h3><div style="text-align: justify;"><ul><li style="text-align: left;"><b>Best of Both?</b> - perhaps there is a strategy which combines aspects of both Alpha-shapes and Concave Hulls, to erode shallow bays but avoid the phenomenon of cavitation?</li><li style="text-align: left;"><b>Disconnected Hulls - t</b>he "classic" Alpha-shape definition allows disconnected MultiPolygons as a result. Currently, the JTS algorithm always produces a result which is a connected single polygon. This seems like the most commonly desired behaviour, but there is a possibility to extend the implementation to optionally allow a disconnected result . CGAL provides a <a href="https://doc.cgal.org/latest/Alpha_shapes_2/classCGAL_1_1Alpha__shape__2.html#aec257cfd1bb47e49366f3ca589a56959">parameter</a> to control the number of polygons in the result, which could be implemented as well.</li></ul></div></div><p></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-23498439384977093852022-10-13T15:03:00.003-07:002022-10-13T15:03:16.140-07:00Relational Properties of DE-9IM spatial predicates<p>There is an elegant mathematical theory of <b><a href="https://en.wikipedia.org/wiki/Binary_relation">binary relations</a></b>. <b><a href="https://en.wikipedia.org/wiki/Homogeneous_relation">Homogeneous relations</a></b> are an important subclass of binary relations in which both domains are the same. A homogeneous relation R is a subset of all ordered pairs <i>(x,y)</i> with x and y elements of the domain. This can be thought of as a boolean-valued function <i>R(x,y)</i>, which is <i>true</i> if the pair has the relationship and <i>false</i> if not.</p><p>The restricted structure of homogeneous relations allows describing them by various properties, including:</p><p><b>Reflexive</b> - <i>R(x, x)</i> is always true</p><p><b>Irreflexive</b> - <i>R(x, x)</i> is never true</p><p><b>Symmetric</b> - if <i>R(x, y),</i> then <i>R(y, x)</i></p><p><b>Antisymmetric</b> - if <i>R(x, y) </i>and <i>R(y, x)</i>, then <i>x = y</i></p><p><b>Transitive</b> - if <i>R(x, y)</i> and <i>R(y, z)</i>, then <i>R(x, z)</i></p><p>The <b><a href="https://en.wikipedia.org/wiki/DE-9IM">Dimensionally Extended 9-Intersection Model (DE-9IM)</a></b> represents the topological relationship between two geometries. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgivqP-tC0-E_rmzGDzZNTD5q9ywnGTyzd1BbEZflXEn_2c9JpD3QOGhHHmotWikoCtQzBWleHyl-Q5KYYoj1LDKD5yfbvPMy8_ONmdeOjgkoN1q_7B9ziUmsOnwJrU-7IACr26eU_1oFFkZMZUF3imZVIs_MzpEsrbCYh2I8rMaV6_9DhdjJiUsZLB/s566/Screen%20Shot%202022-10-13%20at%202.25.05%20PM.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="81" data-original-width="566" height="57" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgivqP-tC0-E_rmzGDzZNTD5q9ywnGTyzd1BbEZflXEn_2c9JpD3QOGhHHmotWikoCtQzBWleHyl-Q5KYYoj1LDKD5yfbvPMy8_ONmdeOjgkoN1q_7B9ziUmsOnwJrU-7IACr26eU_1oFFkZMZUF3imZVIs_MzpEsrbCYh2I8rMaV6_9DhdjJiUsZLB/w400-h57/Screen%20Shot%202022-10-13%20at%202.25.05%20PM.png" width="400" /></a></div><p>Various useful subsets of spatial relationships are specified by <b><a href="https://en.wikipedia.org/wiki/DE-9IM#Spatial_predicates">named spatial predicates</a></b>. These are the basis for spatial querying in many spatial systems including the <a href="https://github.com/locationtech/jts">JTS Topology Suite</a>, <a href="http://libgeos.org/">GEOS</a> and <a href="https://postgis.net/">PostGIS</a>.</p><p>The spatial predicates are homogeneous binary relations over the Geometry domain They can thus be categorized in terms of the relational properties above. The following table shows the properties of the standard predicates:</p><h4 style="text-align: left;"><table border="1">
<tbody><tr><th>Predicate</th><th>Reflexive /<br />Irreflexive</th><th>Symmetric /<br />Antisymmetric</th><th>Transitive<br /><br /></th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Equals</span></th><th>R</th><th>S</th><th>T</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Intersects</span></th><th>R</th><th>S</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Disjoint</span></th><th>R</th><th>S</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Contains</span></th><th>R</th><th>A</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Within</span></th><th>R</th><th>A</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Covers</span></th><th>R</th><th>A</th><th>T</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">CoveredBy</span></th><th>R</th><th>A</th><th>T</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Crosses</span></th><th>I</th><th>S</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Overlaps</span></th><th>I</th><th>S</th><th>-</th></tr>
<tr><th><span style="font-family: courier; font-weight: normal;">Touches</span></th><th>I</th><th>S</th><th>-</th></tr>
</tbody></table><br /></h4><h4 style="text-align: left;">Notes</h4><p></p><ul style="text-align: left;"><li><span style="font-family: courier;">Contains</span> and <span style="font-family: courier;">Within</span> are not Transitive because of the quirk that "Polygons do not contain their Boundary" (explained in <a href="http://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html">this post</a>). A counterexample is a Polygon that contains a LineString lying in its boundary and interior, with the LineString containing a Point that lies in the Polygon boundary. The Polygon does <i>not</i> contain the Point. So <span style="font-family: courier;">C</span><span style="font-family: courier;">ontains(poly, line) = true</span> and <span style="font-family: courier;">C</span><span style="font-family: courier;">ontains(line, pt) = true</span>, but <span style="font-family: courier;">C</span><span style="font-family: courier;">ontains(poly, pt) = false</span>. The predicates <span style="font-family: courier;">Covers</span> and <span style="font-family: courier;">CoveredBy</span> do not have this idiosyncrasy, and thus <i>are</i> transitive.</li></ul><p></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com2tag:blogger.com,1999:blog-2420860529344694449.post-9569059740561257172022-08-24T15:19:00.002-07:002022-08-24T16:42:19.779-07:00Validating Polygonal Coverages in JTS <p>The <a href="http://lin-ear-th-inking.blogspot.com/2022/07/polygonal-coverages-and-operations-in.html">previous post</a> discussed <b>polygonal coverages</b> and outlined the plan to support them in the <b><a href="https://github.com/locationtech/jts">JTS Topology Suite</a></b>. This post presents the first step of the plan: algorithms to validate polygonal coverages. This capability is essential, since coverage algorithms rely on valid input to provide correct results. And as will be seen below, coverage validity is usually not obvious, and cannot be taken for granted. </p><p>As described previously, a polygonal coverage is a set of polygons which fulfils a specific set of geometric conditions. Specifically, a set of polygons is <b>coverage-valid</b> if and only if it satisfies the following conditions:</p><p></p><ul style="text-align: left;"><li><b>Non-Overlapping</b> - polygon interiors do not intersect</li><li><b><b>Edge-Matched</b> (</b>also called<b> Vector-Clean </b>and<b> <b>Fully-Noded</b>)</b> - the shared boundaries of adjacent polygons has the same set of vertices in both polygons</li></ul><div>The <b>Non-Overlapping</b> condition ensures that no point is covered by more than one polygon. The <b>Edge-Matched</b> condition ensures that coverage topology is stable under transformations such as reprojection, simplification and precision reduction (since even if vertices are coincident with a line segment in the original dataset, this is very unlikely to be the case when the data is transformed). </div><div><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhfnrtJ63G17ct-hXLiblMCsu3g_gWENSA_Z_XnCbBCJ3pKjHJdWozw6U6OS6-scmey3RbSzG7L94biWsLOb3RQW8UiLfBOeZRiApn5t55z2sIYn5C3xBWr5DhFQwT8TK2Y-Cp3qoyD4nOanLJhZRTdPupcioLTxbEl54IETcPtCmSTgcK5oESsnwme" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="355" data-original-width="560" height="203" src="https://blogger.googleusercontent.com/img/a/AVvXsEhfnrtJ63G17ct-hXLiblMCsu3g_gWENSA_Z_XnCbBCJ3pKjHJdWozw6U6OS6-scmey3RbSzG7L94biWsLOb3RQW8UiLfBOeZRiApn5t55z2sIYn5C3xBWr5DhFQwT8TK2Y-Cp3qoyD4nOanLJhZRTdPupcioLTxbEl54IETcPtCmSTgcK5oESsnwme" width="320" /></a></div></div><div style="text-align: center;"><i>An invalid coverage which violates both (L) Non-Overlapping and (R) Edge-Matched conditions (note the different vertices in the shared boundary of the right-hand pair)</i></div></div><div><br /></div><div>Note that these rules allow a polygonal coverage to cover disjoint areas. They also allow internal gaps to occur between polygons. Gaps may be intentional holes, or unwanted narrow gaps caused by mismatched boundaries of otherwise adjacent polygons. The difference is purely one of size. In the same way, unwanted narrow "gores" may occur in valid coverages. Detecting undesirable gaps and gores will be discussed further in a subsequent post. </div><h4 style="text-align: left;">Computing Coverage Validation</h4><div>Coverage validity is a global property of a set of polygons, but it can be evaluated in a local and piecewise fashion. To confirm a coverage is valid, it is sufficient to check every polygon against each adjacent (intersecting) polygon to determine if any of the following <b>invalid</b> situations occur:</div><div><ul style="text-align: left;"><li><b>Interiors Overlap:</b></li><ul><li>the polygon linework crosses the boundary of the adjacent polygon</li><li>a polygon vertex lies within the adjacent polygon</li><li>the polygon is a duplicate of the adjacent polygon</li></ul><li><b>Edges do not Match:</b></li><ul><li>two segments in the boundaries of the polygons intersect and are collinear, but are not equal </li></ul></ul></div><div>If neither of these situations are present, then the target polygon is <b>coverage-valid</b> with respect to the adjacent polygon. If all polygons are coverage-valid against every adjacent polygon then the coverage as a whole is valid.</div><div><br /></div><div>For a given polygon it is more efficient to check all adjacent polygons together, since this allows faster checking of valid polygon boundary segments. When validation is used on datasets which are already clean, or mostly so, this improves the overall performance of the algorithm. </div><div><br /></div><div>Evaluating coverage validity in a piecewise way allows the validation process to be parallelized easily, and executed incrementally if required.</div><h4 style="text-align: left;">JTS Coverage Validation</h4><div>Validation of a single coverage polygon is provided by the JTS <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java"><span style="font-family: courier;">CoveragePolygonValidator</span> </a>class. If a polygon is coverage-invalid due to one or more of the above situations, the class computes the portion(s) of the polygon boundary which cause the failure(s). This allows the locations and number of invalidities to be determined and visualized.</div><div><br /></div><div>The class <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageValidator.java"><span style="font-family: courier;">CoverageValidator</span> </a>computes coverage-validity for an entire set of polygons. It reports the invalid locations for all polygons which are not coverage-valid (if any). </div><div><br /></div><div>Using spatial indexing makes checking coverage validity quite performant. For example, a coverage containing 91,384 polygons with 10,474,336 vertices took only 6.4 seconds to validate. In this case the coverage is <i>nearly </i>valid, since only one invalid polygon was found. The invalid boundary linework returned by <span style="font-family: courier;">CoverageValidator </span>allows easily visualizing the location of the issue.</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgPzizIfU3Eb6-fA4TFVN4OEPKX5gJtUXzD5_aaLrdjX8iT9ysm1mtHFGNfMHQPJTUnWmxUxV1RIeT7XwgnYKrUCNU-JFHl5OyQPLebWtR8EnjrEpHSHOBUaaPBY4dEoNOV32yQxWhwtliwksGSeg9cyIb9UUo7LBaGkH0XzXexl8ihA4hWnvqTHwBH" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="524" data-original-width="385" height="320" src="https://blogger.googleusercontent.com/img/a/AVvXsEgPzizIfU3Eb6-fA4TFVN4OEPKX5gJtUXzD5_aaLrdjX8iT9ysm1mtHFGNfMHQPJTUnWmxUxV1RIeT7XwgnYKrUCNU-JFHl5OyQPLebWtR8EnjrEpHSHOBUaaPBY4dEoNOV32yQxWhwtliwksGSeg9cyIb9UUo7LBaGkH0XzXexl8ihA4hWnvqTHwBH=w235-h320" width="235" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>A polygonal dataset of 91,384 polygons, containing a single coverage-invalid polygon</i></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjqKGlFy773vAB6PcPxi0YT3we9NJjv1qCDzTSl3Qn1XTLTUvv5rZluUeCosJ0QPf-GIQoRxKEORWW6YFbJi_FzNV0Z9ZD8LLnzuobzwieIAXgQ_xHFEkKzup0lIVaF-l2Z5M6H15TUG2dgpRT6zNw1a2W3blzmp3yplPTgD3rRnGUy5Vxdj24kxXiR" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="332" data-original-width="390" height="240" src="https://blogger.googleusercontent.com/img/a/AVvXsEjqKGlFy773vAB6PcPxi0YT3we9NJjv1qCDzTSl3Qn1XTLTUvv5rZluUeCosJ0QPf-GIQoRxKEORWW6YFbJi_FzNV0Z9ZD8LLnzuobzwieIAXgQ_xHFEkKzup0lIVaF-l2Z5M6H15TUG2dgpRT6zNw1a2W3blzmp3yplPTgD3rRnGUy5Vxdj24kxXiR" width="282" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>The invalid polygon is a tiny sliver, with a single vertex lying a very small distance inside an adjacent polygon. </i><i>The discrepancy is only visible using t</i><i>he JTS TestBuilder Reveal Topology mode.</i></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: justify;">The size of the discrepancy is very small. The vertex causing the overlap is only 0.0000000001 units away from being valid:</div><div class="separator" style="clear: both;"><div class="separator" style="clear: both; text-align: left;"><span style="font-family: courier; font-size: x-small;"><br /></span></div><div class="separator" style="clear: both; text-align: left;"><span style="font-family: courier; font-size: x-small;">[921] POLYGON(632)</span></div><div class="separator" style="clear: both; text-align: left;"><span style="font-family: courier; font-size: x-small;">[922:4] POLYGON(5)</span></div><div class="separator" style="clear: both; text-align: left;"><span style="font-family: courier; font-size: x-small;">Ring-CW Vert[921:0 514] POINT ( 960703.3910000008 884733.1892000008 )</span></div><div class="separator" style="clear: both; text-align: left;"><span style="font-family: courier; font-size: x-small;">Ring-CW Vert[922:4:0 3] POINT ( 960703.3910000008 884733.1893000007 )</span></div><br /><div style="text-align: justify;">This illustrates the importance of having fast, robust automated validity checking for polygonal coverages, and providing information about the exact location of errors.</div></div></div></div><h4 style="text-align: left;">Real-world testing</h4><div>With coverage validation now available in JTS, it's interesting to apply it to publicly available datasets which (should) have coverage topology. It is surprising how many contain validity errors. Here are a few examples:</div><div><br /></div><div><table><tbody><tr><td><b>Source</b></td><td><span style="font-size: medium;">City of Vancouver</span></td></tr><tr><td><b>Dataset</b></td><td><a href="https://opendata.vancouver.ca/explore/dataset/zoning-districts-and-labels">Zoning Districts</a></td></tr><tr><td></td><td><br /></td></tr></tbody></table></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjcp1OHQZmf7jAELYaJpraBuch1PouHyg_NLSk-R80oOH6BSq45shYs1MYbYI2p3TEB-hPCrSN1Lx5FEAGkNAwzRVL9ZG8lPayccWj9DSiXm1_fdMIO8bFl3GyNF-crqOk_dGdrPdecn8XXfbK7fXK0OWU1cMRokn9u_8y55D3Qo6VMLhnOEB6m7p7Z" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="384" data-original-width="665" height="185" src="https://blogger.googleusercontent.com/img/a/AVvXsEjcp1OHQZmf7jAELYaJpraBuch1PouHyg_NLSk-R80oOH6BSq45shYs1MYbYI2p3TEB-hPCrSN1Lx5FEAGkNAwzRVL9ZG8lPayccWj9DSiXm1_fdMIO8bFl3GyNF-crqOk_dGdrPdecn8XXfbK7fXK0OWU1cMRokn9u_8y55D3Qo6VMLhnOEB6m7p7Z" width="320" /></a></div><br />This dataset contains 1,498 polygons with 57,632 vertices. There are 379 errors identified, which mainly consist of very small discrepancies between vertices of adjacent polygons.</div><div><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgGhMBKgQUgOsOWtn7G_uTPrKP5Gi3rBDDUUQVVVSCXyNX4xa3t4-75k5m77rLFtprcQn4lpbIP9EtP9QOz_iagbVBmOcyJoGfV5kmp4NeFSRO_APgEDbnYLySfR7-O9EWHwIoANnIqQGHb9wygguGmAh6G5J2Ki3dUpBjzTLbio-5SKKGVfxotjZDY" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="326" data-original-width="426" height="240" src="https://blogger.googleusercontent.com/img/a/AVvXsEgGhMBKgQUgOsOWtn7G_uTPrKP5Gi3rBDDUUQVVVSCXyNX4xa3t4-75k5m77rLFtprcQn4lpbIP9EtP9QOz_iagbVBmOcyJoGfV5kmp4NeFSRO_APgEDbnYLySfR7-O9EWHwIoANnIqQGHb9wygguGmAh6G5J2Ki3dUpBjzTLbio-5SKKGVfxotjZDY" width="314" /></a></div><i>Example of a discrepant vertex in a polygon</i></div><br /><br /></div>
<div>
<table><tbody><tr>
<td><b>Source</b></td><td><span style="font-size: medium;">British Ordnance Survey OpenData</span></td></tr>
<tr><td><b>Dataset</b></td><td><a href="https://osdatahub.os.uk/downloads/open/BoundaryLine">Boundary-Line</a></td></tr>
<tr><td><b>File</b></td><td><span style="font-family: courier;">unitary_electoral_division_region.shp</span></td></tr>
</tbody></table><br /><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyIqSBGAwNTubgYRt-k4tKhgaxTgTk-hnayddsCHLFz1NDwcqe82gA1e9UV5aDq6iEqnAPDAVaTrbUWEr-OaVhNwL98dGV_SoxCkS4odfTY8lvZ_MzksUwG935RcO-CBSHsa4rlmpeuwAldB5fpgWObQvUyIZSURL3-XdFccuoPEUm1Z9ZdXCrJcOn/s417/cov-valid-os-bl.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="417" data-original-width="240" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyIqSBGAwNTubgYRt-k4tKhgaxTgTk-hnayddsCHLFz1NDwcqe82gA1e9UV5aDq6iEqnAPDAVaTrbUWEr-OaVhNwL98dGV_SoxCkS4odfTY8lvZ_MzksUwG935RcO-CBSHsa4rlmpeuwAldB5fpgWObQvUyIZSURL3-XdFccuoPEUm1Z9ZdXCrJcOn/s320/cov-valid-os-bl.png" width="184" /></a></div><div><br /></div><div>This dataset contains 1,178 polygons with 2,174,787 vertices. There are 51 errors identified, which mainly consist of slight discrepancies between vertices of adjacent polygons. (Note that this does not include gaps, which are not detected by <span style="font-family: courier;">CoverageValidator</span>. There are about 100 gaps in the dataset as well.)</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfByQ5n59g7elBi0V9aLmhE1aYNgFLFRuA1UYDnhBGfcN1jxD4-vLvCasP0JvGRbh9hQCkMLJ_nRA56qAKxeU4onnxl7bYD7jq4UfV5FXBKfFlPHxhRD-4qPyTBAIZYROvx_RGYCqj5awsGWNtXehcq_YUcGpKgwJkOpkcLZ_sKXXqAv03cwvziC1Z/s355/cov-valid-os-bl-error.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="355" data-original-width="326" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhfByQ5n59g7elBi0V9aLmhE1aYNgFLFRuA1UYDnhBGfcN1jxD4-vLvCasP0JvGRbh9hQCkMLJ_nRA56qAKxeU4onnxl7bYD7jq4UfV5FXBKfFlPHxhRD-4qPyTBAIZYROvx_RGYCqj5awsGWNtXehcq_YUcGpKgwJkOpkcLZ_sKXXqAv03cwvziC1Z/s320/cov-valid-os-bl-error.png" width="294" /></a></div><div style="text-align: center;"><i>An example of overlapping polygons in the Electoral Division dataset</i></div><div><div><br /><br />
<table><tbody><tr>
<td><b>Source</b></td><td><span style="font-size: medium;">Hamburg Open Data Platform</span></td></tr>
<tr><td><b>Dataset</b></td><td><a href="https://api.hamburg.de/datasets/v1/alkis_vereinfacht/collections/VerwaltungsEinheit">VerwaltungsEinheit</a> (Administrative Units)</td></tr>
</tbody></table><br />
</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEic2DHp4oyzC_5CCOT80xN_QTjgMtvGZ8DlLMx1sB8lzeBi-XPcDvHreYgAty2-bvhJ_CijqHq5-68_nk5DJM-E6nERRxqnJIo9Oq0GVGOxq3O-isNNMZiPfIoZ3NRcd-Dt1yb_PHhlEMBoe4-YxzORH-dTtqbGX-6osir4v8624vF1XRxq1ifvsCuh/s389/cov-valid-hodp-ve.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="239" data-original-width="389" height="197" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEic2DHp4oyzC_5CCOT80xN_QTjgMtvGZ8DlLMx1sB8lzeBi-XPcDvHreYgAty2-bvhJ_CijqHq5-68_nk5DJM-E6nERRxqnJIo9Oq0GVGOxq3O-isNNMZiPfIoZ3NRcd-Dt1yb_PHhlEMBoe4-YxzORH-dTtqbGX-6osir4v8624vF1XRxq1ifvsCuh/s320/cov-valid-hodp-ve.png" width="320" /></a></div><div><br /></div><div>The dataset (slightly reduced) contains 7 polygons with 18,254 vertices. Coverage validation produces 64 error locations. The errors are generally small vertex discrepancies producing overlaps. Gaps exist as well, but are not detected by the default <span style="font-family: courier;">CoverageValidator </span>usage.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWuFIh0yzWVqayQ5XRzVezBIGleVXmMUipumdsdFcJJkO-cYNN24psVaXqIgDaoj464tIJCDqEEMQ8l0u_zyM6lkVsMMQ9WvOHE4fCeBZzmrhjSI3MiGFcqoaNV4V4PGSuHZVnEzcZ_-y-eeWkY-GA86MFSqZrwmmTI37YDi8lBMVY3uv4Qtxmeszb/s389/cov-valid-hodp-ve-error.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="317" data-original-width="389" height="261" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgWuFIh0yzWVqayQ5XRzVezBIGleVXmMUipumdsdFcJJkO-cYNN24psVaXqIgDaoj464tIJCDqEEMQ8l0u_zyM6lkVsMMQ9WvOHE4fCeBZzmrhjSI3MiGFcqoaNV4V4PGSuHZVnEzcZ_-y-eeWkY-GA86MFSqZrwmmTI37YDi8lBMVY3uv4Qtxmeszb/s320/cov-valid-hodp-ve-error.png" width="320" /></a></div><i style="text-align: center;"><div style="text-align: center;"><i>An example of overlapping polygons (and a gap) in the </i>VerwaltungsEinheit<i> dataset</i></div></i><div><br /></div><div><br /></div></div><div>As always, this code will be ported to <a href="http://libgeos.org/">GEOS</a>. A further goal is to provide this capability in <a href="https://postgis.net/">PostGIS</a>, since there are likely many datasets which could benefit from this checking. The piecewise implementation of the algorithm should mesh well with the nature of SQL query execution.</div><div><br /></div><div>And of course the next logical step is to provide the ability to fix errors detected by coverage validation. This is a research project for the near term.</div><div><br /></div><div><b>UPDATE</b>: my colleague Paul Ramsey pointed out that he has already <a href="https://github.com/libgeos/geos/commit/62c928c9f37957c62fab8db69e6c8efd26ce4085">ported</a> this code to GEOS. Now for some performance testing!</div>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-24077530309612507832022-07-24T09:14:00.001-07:002022-07-24T09:14:05.865-07:00Polygonal Coverages and Operations in JTS<p>An important concept in <a href="https://en.wikipedia.org/wiki/Data_model_(GIS)">spatial data modelling</a> is that of a <b><a href="https://en.wikipedia.org/wiki/Coverage_data">coverage</a></b>. A coverage models a two-dimensional region in which every point has a value out of a range (which may be defined over one or a set of attributes). Coverages can be represented in both of the main physical spatial data models: <b>raster</b> and <b>vector</b>. In the <a href="https://en.wikipedia.org/wiki/Data_model_(GIS)#Raster_data_model">raster data model</a> a coverage is represented by a grid of cells with varying values. In the <a href="https://en.wikipedia.org/wiki/Data_model_(GIS)#Vector_data_model">vector data model</a> a coverage is a set of non-overlapping polygons (which usually, but not always, cover a contiguous area). </p><p>This post is about the vector data coverage model, which is termed (naturally) a <b>polygonal coverage</b>. These are used to model regions which are occupied by discrete sub-regions with varying sets of attribute values. The sub-regions are modelled by simple polygons. The coverage may contain gaps between polygons, and may cover multiple disjoint areas. The essential characteristics are:</p><p></p><ul style="text-align: left;"><li>polygon interiors do not overlap</li><li>the common boundary of adjacent polygons has the same set of vertices in both polygons.</li></ul><p></p><p>There are many types of data which are naturally modelled by polygonal coverages. Classic examples include:</p><p></p><ul style="text-align: left;"><li>Man-made boundaries</li><ul><li>parcel fabrics</li><li>political jurisdictions</li></ul><li>Natural boundaries</li><ul><li>vegetation cover</li><li>land use</li></ul></ul><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgtT1khRQu_1sWboeMvu5oVgLu-3Hu40tZWJp9yERssoseLWeiwKOWHTr_1mBkmLx1GBZ8Ls7mC5umcTTJbritEbCW3kuF9Rc--m0hEioIhg2MV4NMp5XWv53lWGSv7dHtMQLUA6tdbhAN9-tCwfh4Ook53-HOMaQItCmWZaCMtO5t5xX5IlFm-vrO_/s630/coverage-france.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="413" data-original-width="630" height="263" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgtT1khRQu_1sWboeMvu5oVgLu-3Hu40tZWJp9yERssoseLWeiwKOWHTr_1mBkmLx1GBZ8Ls7mC5umcTTJbritEbCW3kuF9Rc--m0hEioIhg2MV4NMp5XWv53lWGSv7dHtMQLUA6tdbhAN9-tCwfh4Ook53-HOMaQItCmWZaCMtO5t5xX5IlFm-vrO_/w400-h263/coverage-france.png" width="400" /></a></div><div style="text-align: center;"><i>A polygonal coverage of regions of France</i></div><h3 style="text-align: left;">Topological and Discrete Polygonal Coverages</h3><div><br /></div><div>There are two ways to represent polygonal coverages: as a <b>topological data structure</b>, or as a <b>set of discrete polygons</b>. </div><div>A <b>coverage topology</b> consists of linked faces, edges and nodes. The edges between two nodes form the shared boundary between two faces. The coverage polygons can be reconstituted from the edges delimiting each face. </div><div>The <b>discrete polygon</b> representation is simpler, and aligns better with the OGC Simple Features model. It is simply a collection of polygons which satisfy the coverage validity criteria given above.</div><div>Most common spatial data formats support only a discrete polygon model, and many coverage datasets are provided in this form. However, the lack of inherent topology means that datasets must be carefully constructed to ensure they have valid coverage topology. In fact, many available datasets contain coverage invalidities. A current focus of JTS development is to provide algorithms to detect this situation and provide the locations where the polygons fail to form a valid coverage.</div><h3 style="text-align: left;">Polygonal Coverage Operations</h3><p>Operations which can be performed on polygonal coverages include:</p><p></p><ul style="text-align: left;"><li><b>Validation</b> - check that a set of discrete polygons forms a valid coverage</li><li><b>Gap Detection</b> - check if a polygonal coverage contains narrow gaps (using a given distance tolerance)</li><li><b>Cleaning </b>- fix errors such as gaps, overlaps and slivers in a polygonal dataset to ensure that it forms a clean, valid coverage</li><li><b>Simplification</b> - simplify (generalize) polygon boundary linework, ensuring coverage topology is preserved</li><li><b>Precision Reduction</b> - reduce precision of polygon coordinates, ensuring coverage topology is preserved</li><li><b>Union</b> - merge all or portions of the coverage polygons into a single polygon (or multipolygon, if the input contains disjoint regions)</li><li><b>Overlay</b> - compute the intersection of two coverages, producing a coverage of resultant polygons </li></ul><p></p><p>Implementing polygonal coverage operations is a current focus for development in the <a href="https://github.com/locationtech/jts"><b>JTS Topology Suite</b></a>. Since most operations require a valid coverage as input, the first goal is to provide Coverage Validation. Cleaning and Simplification are priority targets as well. Coverage Union is already available, as is Overlay (in a slightly sub-optimal way). In addition, a Topology data structure will be provided to support the edge-node representation. (Yes, the Topology Suite will provide topology at last!). Stay tuned for further blog posts as functionality is rolled out.</p><p>As usual, the coverage algorithms developed in JTS will be ported to <a href="http://libgeos.org/"><b>GEOS</b></a>, and will thus be available to <a href="http://libgeos.org/usage/bindings/">downstream projects</a> like <a href="http://www.postgis.org/"><b>PostGIS</b></a>.</p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-77260873154963677092022-06-21T15:34:00.000-07:002022-06-21T15:34:06.026-07:00JTS 1.19 Released<p>JTS 1.19 has just been released! There is a great deal of new, improved and fixed functionality in this release - see the <a href="https://github.com/locationtech/jts/releases/tag/1.19.0">GitHub release page</a> or the <a href="https://github.com/locationtech/jts/blob/1.19.0/doc/JTS_Version_History.md">Version History</a> for full details.</p><p>This blog has several posts describing new functionality in JTS 1.19:</p><h4 style="text-align: left;">New Functionality</h4><p></p><ul style="text-align: left;"><li><a href="http://lin-ear-th-inking.blogspot.com/2022/05/concave-hulls-of-polygons.html">Concave Hull of Polygons</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/05/algorithm-for-concave-hull-of-polygons.html">Algorithm for Concave Hull of Polygons</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/04/outer-and-inner-concave-polygon-hulls.html">Outer and Inner Concave Polygon Hulls</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/05/using-outer-hulls-for-smoothing.html">Using Outer Hulls for Smoothing Vectorized Polygons</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/01/concave-hulls-in-jts.html">Concave Hulls</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2021/11/jts-polygon-triangulation-at-last.html">Polygon Triangulation</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/01/cubic-bezier-curves-in-jts.html">Cubic Bezier Curves</a></li><li><a href="http://lin-ear-th-inking.blogspot.com/2022/01/jts-offset-curves.html">Offset Curves</a></li></ul><h4 style="text-align: left;">Improvements</h4><div><ul style="text-align: left;"><li><a href="http://lin-ear-th-inking.blogspot.com/2021/10/query-kd-trees-100x-faster-with-this.html">KD-tree query performance improvement with randomized insertion</a></li></ul><div><br /></div><div>Many of these improvements have been ported to GEOS, and will appear in the soon-to-appear <a href="https://github.com/libgeos/geos/releases/tag/3.11.0beta2">version 3.11</a>. In turn this has provided the basis for new and enhanced functions in the next PostGIS release, and will likely be available in other platforms via the many GEOS <a href="http://libgeos.org/usage/bindings/">bindings and applications</a>.</div></div><p></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-68424405302465092532022-05-30T12:24:00.001-07:002022-05-30T12:24:23.689-07:00Algorithm for Concave Hull of Polygons<p>The <a href="http://lin-ear-th-inking.blogspot.com/2022/05/concave-hulls-of-polygons.html">previous post</a> introduced the new <a href="https://github.com/locationtech/jts/pull/870"><span style="font-family: courier;">ConcaveHullOfPolygons</span></a> class in the <a href="https://github.com/locationtech/jts"><b>JTS Topology Suite</b></a>. This allows computing a <a href="http://lin-ear-th-inking.blogspot.com/2022/01/concave-hulls-in-jts.html">concave hull</a> which is <b>constrained by a set of polygonal geometries</b>. This supports use cases including:</p><p></p><ul style="text-align: left;"><li><b>generalization</b> of groups of polygon</li><li><b>joining</b> polygons</li><li><b>filling gaps</b> between polygons</li></ul><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjKg0FhegVG0lweG269Sp7OcjTJzudWhSdroBt89UHxKrAmrSWQUx89SubDaIo9NyHJSDnV2pFFeaZ46RN6U4D-ucHEiDRZrRDW_TePj1bB92ILWg5q2kfcm3dhzFB8vSNNmxwecRRlpFd-21zKwoAlMYl0QBXoS7x7_2uD46w53Ptr0NsvacE8twZf/s438/chpolygons-uk.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="390" data-original-width="438" height="285" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjKg0FhegVG0lweG269Sp7OcjTJzudWhSdroBt89UHxKrAmrSWQUx89SubDaIo9NyHJSDnV2pFFeaZ46RN6U4D-ucHEiDRZrRDW_TePj1bB92ILWg5q2kfcm3dhzFB8vSNNmxwecRRlpFd-21zKwoAlMYl0QBXoS7x7_2uD46w53Ptr0NsvacE8twZf/s320/chpolygons-uk.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>A concave hull of complex polygons</i></div><p>The algorithm developed for <span style="font-family: courier;">ConcaveHullOfPolygons</span> is a novel one (as far as I know). It uses several features recently developed for JTS, including a neat trick for constrained triangulation. This post describes the algorithm in detail.</p><p>The construction of a concave hull for a set of polygons uses the same approach as the existing JTS <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java"><span style="font-family: courier;">ConcaveHull</span></a> implementation. The space to be filled by a concave hull is triangulated with a <a href="https://en.wikipedia.org/wiki/Delaunay_triangulation">Delaunay triangulation</a>. Triangles are then "eroded" from the outside of the triangulation, until a criteria for termination is achieved. A useful termination criteria is that of <b>maximum outside edge length</b>, specified as either an absolute length or a fraction of the range of edge lengths.</p><p>For a concave hull of points, the underlying triangulation is easily obtained via the <a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/triangulate/DelaunayTriangulationBuilder.html">Delaunay Triangulation</a> of the point set. However, for a <b>concave hull of polygons</b> the triangulation required is for the space <i>between</i> the constraint polygons. A simple Delaunay triangulation of the polygon vertices will not suffice, because the triangulation may not respect the edges of the polygons. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh60Mv3yXnhh986H31Ppgn2HbdmBkOAV7qCo2566bArFwl0d6Q4s3Pz9QPoc-FWxhbR5C2fSkxlGOtzFaDsNrT5S4qdW3R0zvSOadypLxhdTohWISS7Jouv94zQhx8bhs5dkwx8ERjJ_uKX5Qqjxqcr1Xok4cQsJAuNSOQoAoHol8pzrXbfPugo-RCX/s374/chpoly-delaunay-crossing.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="342" data-original-width="374" height="293" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh60Mv3yXnhh986H31Ppgn2HbdmBkOAV7qCo2566bArFwl0d6Q4s3Pz9QPoc-FWxhbR5C2fSkxlGOtzFaDsNrT5S4qdW3R0zvSOadypLxhdTohWISS7Jouv94zQhx8bhs5dkwx8ERjJ_uKX5Qqjxqcr1Xok4cQsJAuNSOQoAoHol8pzrXbfPugo-RCX/s320/chpoly-delaunay-crossing.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Delaunay Triangulation of polygon vertices crosses polygon edges</i></div><p>What is needed is a <a href="https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation"><b>Constrained Delaunay Triangulation</b></a>, with the edge segments of the polygons as constraints (i.e. the polygon edge segments are present as triangle edges, which ensures that other edges in the triangulation do not cross them). There are several <a href="https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation#Algorithms">algorithms</a> for Constrained Delaunay Triangulations - but a simpler alternative presented itself. JTS recently added an algorithm for computing <a href="http://lin-ear-th-inking.blogspot.com/2021/11/jts-polygon-triangulation-at-last.html">Delaunay Triangulations for polygons</a>. This algorithm supports triangulating polygons with holes (via <b>hole joining</b>). So to generate a triangulation of the space between the input polygons, they can be inserted as holes in a larger "frame" polygon. This can be triangulated, and then the frame triangles removed. Given a sufficiently large frame, this leaves the triangulation of the "fill" space between the polygons, out to their convex hull. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3zUQrNdDZ-6XsrytB3ZRAXcQrTVoqLWGIGQTwYhDg9oICVctrD_m7JDBfJmcKqZscAKQVZvo6z-vY2SkoBVSaPykR4b_MCu3y2sYcfniZDSf3inE2I8m00dG_OjXCuZkzmj6UfMcOML6kzAwwNkLqNVpHL0jRUXrzMQ1cd_g8B8bN12bZ1Oevu9Ma/s415/chpoly-frame.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="388" data-original-width="415" height="299" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3zUQrNdDZ-6XsrytB3ZRAXcQrTVoqLWGIGQTwYhDg9oICVctrD_m7JDBfJmcKqZscAKQVZvo6z-vY2SkoBVSaPykR4b_MCu3y2sYcfniZDSf3inE2I8m00dG_OjXCuZkzmj6UfMcOML6kzAwwNkLqNVpHL0jRUXrzMQ1cd_g8B8bN12bZ1Oevu9Ma/s320/chpoly-frame.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Triangulation of frame with polygons as holes</i></div><p>The triangulation can then be eroded using similar logic to the non-constrained Concave Hull algorithm. The implementations all use the JTS <a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/triangulate/tri"><span style="font-family: courier;">Tri</span></a> data structure, so it is easy and efficient to share the triangulation model between them. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEKqTzmsLyamVN73WbW3tkZFltFpWscNPVzkZ-jhGYIjBZI6nigqlX0FoQpc5n8ETfuqPsgRMzA9-JUsRQBR1t4P__EnX_huSlQaYeQhhn6z9lDm1yEXFLYD94gkJu9k3hOcqFb64n3ICGbgek4mZ0Wm0EDAwsbrfpoZhgzrqzcNqeevnEALmMnjNo/s376/chpoly-eroded.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="348" data-original-width="376" height="296" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiEKqTzmsLyamVN73WbW3tkZFltFpWscNPVzkZ-jhGYIjBZI6nigqlX0FoQpc5n8ETfuqPsgRMzA9-JUsRQBR1t4P__EnX_huSlQaYeQhhn6z9lDm1yEXFLYD94gkJu9k3hOcqFb64n3ICGbgek4mZ0Wm0EDAwsbrfpoZhgzrqzcNqeevnEALmMnjNo/s320/chpoly-eroded.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Triangulation after removing frame and eroding triangles</i></div><p>The triangles that remain after erosion can be combined with the input polygons to provide the result concave hull. The triangulation and the input polygons form a polygonal coverage, so the union can be computed very efficiently using the JTS <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/operation/overlayng/CoverageUnion.html">CoverageUnion</a></span> class. If required, the fill area alone can be returned as a result, simply by omitting the input polygons from the union.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnOrvb1V7Nt06TacMcErSIlS3_o6CCWmQML_MOhN6uHREKSZrQxJWRLMdkZMT0XabifXgQMDJ6gqmWS34_JHG1_x2UazGiRGn_T7IdJZP2kS_nIrbbXARFnUvYoLy7eIFn29hkjjX5PTjGJHt_M3P8SaO-HLcOeiwCVP6BT_lnj9O-dOels17VBUwE/s613/chpoly-hull-fill.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="269" data-original-width="613" height="175" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjnOrvb1V7Nt06TacMcErSIlS3_o6CCWmQML_MOhN6uHREKSZrQxJWRLMdkZMT0XabifXgQMDJ6gqmWS34_JHG1_x2UazGiRGn_T7IdJZP2kS_nIrbbXARFnUvYoLy7eIFn29hkjjX5PTjGJHt_M3P8SaO-HLcOeiwCVP6BT_lnj9O-dOels17VBUwE/w400-h175/chpoly-hull-fill.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Concave Hull and Concave Fill</i></div><p>A useful option is to compute a "tight" concave hull to the outer boundary of the input polygons. This is easily accomplished by removing triangles which touch only a single polygon. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY8YGJewkMe5ZDu_dR8z8E5Dil3-DEYNinAtPmbuzvcLB7jX4atqev8G-0QQYYx7yJHWeL14Jq0Mg_O7Le3rIX4bvWS2VxhA41i7GepmCOizOugCnfecVBH-Hy4HMR3d1qiMVchs_r8QHuXo5aEz85R77lX31Oy847XE122BmkZMKSN1BmAnT-jOYs/s373/chpoly-tight.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="338" data-original-width="373" height="290" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiY8YGJewkMe5ZDu_dR8z8E5Dil3-DEYNinAtPmbuzvcLB7jX4atqev8G-0QQYYx7yJHWeL14Jq0Mg_O7Le3rIX4bvWS2VxhA41i7GepmCOizOugCnfecVBH-Hy4HMR3d1qiMVchs_r8QHuXo5aEz85R77lX31Oy847XE122BmkZMKSN1BmAnT-jOYs/s320/chpoly-tight.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Concave Hull tight to outer edges</i></div><p><br /></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjx6a_SHytFCRnC_Fh8qq9OhRjNC2_S9j8pCYf-Y_mPKWssmbPlXs9ZSUvfTzldJgzO4r0HdIQmpN5X7vTAueW4_QV6ypsp_nS5JeqrPbkUGUPucNZBMJ4oWXVVL0MjRhpBcwzlrxXY3qM1tV8EKN7gJ21Cre2ZDI0oPbM_klusGD6Xp-_VIPCyzXKz/s408/chpolygons-uk-tight.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="393" data-original-width="408" height="308" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjx6a_SHytFCRnC_Fh8qq9OhRjNC2_S9j8pCYf-Y_mPKWssmbPlXs9ZSUvfTzldJgzO4r0HdIQmpN5X7vTAueW4_QV6ypsp_nS5JeqrPbkUGUPucNZBMJ4oWXVVL0MjRhpBcwzlrxXY3qM1tV8EKN7gJ21Cre2ZDI0oPbM_klusGD6Xp-_VIPCyzXKz/s320/chpolygons-uk-tight.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Concave Hull of complex polygons, tight to outer edges.</i></div><p>Like the Concave Hull of Points algorithm, holes are easily supported by allowing erosion of interior triangles.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg65yDpBTvk1i-PjyMmDy2Hp9Kks4L3b-zxe12lf7xZsqqeHe00UE_56sQOccSvKELhV0o1Ql1-nPVyn-cKDVAbrip1EPd58ZOig4bo9cABVF60oDZneeeb5TvjAGsZEh1PdsBXDFvMki3YsucqLOZeoZaU8mEt8IV2PsJ1aH9avW9lb6YM91jbkvnA/s364/chpoly2-holes.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="319" data-original-width="364" height="280" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg65yDpBTvk1i-PjyMmDy2Hp9Kks4L3b-zxe12lf7xZsqqeHe00UE_56sQOccSvKELhV0o1Ql1-nPVyn-cKDVAbrip1EPd58ZOig4bo9cABVF60oDZneeeb5TvjAGsZEh1PdsBXDFvMki3YsucqLOZeoZaU8mEt8IV2PsJ1aH9avW9lb6YM91jbkvnA/s320/chpoly2-holes.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Concave Hull of Polygons, allowing holes</i></div><p>The algorithm performance is determined by the cost of the initial polygon triangulation. This is quite efficient, so the overall performance is very good.</p><p>As mentioned, this seems to be a new approach to this geometric problem. The only comparable implementation I have found is the ArcGIS tool called <a href="https://desktop.arcgis.com/en/arcmap/latest/tools/cartography-toolbox/aggregate-polygons.htm">Aggregate Polygons</a>, which appears to provide similar functionality (including producing a tight outer boundary). But of course algorithm details are not published and the code is not available. It's much better to have an open source implementation, so it can be used in spatial tools like PostGIS, Shapely and QGIS (based on the <a href="https://github.com/libgeos/geos/pull/617">port to GEOS</a>). Also, this provides the ability to add options and enhanced functionality for use cases which may emerge once this gets some real-world use.</p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-82697030142432146962022-05-20T14:10:00.001-07:002022-05-20T14:10:10.566-07:00Concave Hulls of Polygons<p>A common spatial need is to compute a polygon which contains another set of polygons. There are numerous use cases for this; for example:</p><p style="text-align: left;"></p><ul style="text-align: left;"><li>Generalizing groups of building outlines (questions: <a href="https://gis.stackexchange.com/questions/164350/strategies-for-grouping-polygons">1</a>, <a href="https://gis.stackexchange.com/questions/291093/dissolving-neighbour-bordering-polygon-features-in-qgis">2</a>) </li><li>Creating "district" polygons around block polygons (questions: <a href="https://gis.stackexchange.com/questions/218958/creating-bounding-extent-of-existing-polygon-with-same-geometry-using-qgis">1</a>)</li><li>Removing gaps between sets of polygons (questions: <a href="https://gis.stackexchange.com/questions/356480/enclose-polygons-that-are-not-overlapped-and-remove-gaps-between-them">1</a>, <a href="https://gis.stackexchange.com/questions/316000/using-st-union-to-combine-several-polygons-to-one-multipolygon-using-postgis">2</a>, <a href="https://gis.stackexchange.com/questions/229790/dissolve-adjacent-parts-of-multipolygon-into-a-single-polygon">3</a>, <a href="https://gis.stackexchange.com/questions/168999/postgis-how-to-dissolve-vector-data">4</a>)</li><li>Joining two polygons by filling the space between them (questions: <a href="https://gis.stackexchange.com/questions/352884/how-can-i-get-a-polygon-of-everything-between-two-polygons-in-postgis">1</a>, <a href="https://gis.stackexchange.com/questions/49726/postgis-algorithm-to-unite-points-of-two-geometries-that-are-within-specified-ra">2</a>) </li></ul><div>This post describes a new approach for solving these problems, via an algorithm for computing a concave hull with polygonal constraints. The algorithm builds on recent work on <a href="http://lin-ear-th-inking.blogspot.com/2021/11/jts-polygon-triangulation-at-last.html">polygon triangulation in JTS</a>, and uses a neat trick which I'll describe in a subsequent post. </div><p></p><h3 style="text-align: left;">Approach: Convex Hull</h3><p>The simplest way to compute an area enclosing a set of geometries is to compute their <b><a href="https://en.wikipedia.org/wiki/Convex_hull">convex hull</a></b>. But the convex hull is a fairly coarse approximation of the area occupied by the polygons, and in most cases a better representation is required. </p><p>Here's an example of gap removal between two polygons. Obviously, the convex hull does not provide anything close to the desired result: </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmkaCBjIEmAMWvzKq-arNEGLzr6WA-3MjVEzoZGp1CSNcr-bjcFqjvwGHXwb-yuZqEigpy51PY6r_NGnU4PCw77zRzfzdFobMqXoBASHCwVxe9AASaC71WgsQWsfr-8GmuTZhRrslwdsXZjiqJhNtfC1QAmcGXKfvBnJeqKllN6oAR9r_CsoAODBQR/s468/convex-hull.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="246" data-original-width="468" height="168" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjmkaCBjIEmAMWvzKq-arNEGLzr6WA-3MjVEzoZGp1CSNcr-bjcFqjvwGHXwb-yuZqEigpy51PY6r_NGnU4PCw77zRzfzdFobMqXoBASHCwVxe9AASaC71WgsQWsfr-8GmuTZhRrslwdsXZjiqJhNtfC1QAmcGXKfvBnJeqKllN6oAR9r_CsoAODBQR/s320/convex-hull.png" width="320" /></a></div><h3 style="text-align: left;">Approach: Buffer and Unbuffer</h3><p>A popular suggestion is to buffer the polygon set by a distance sufficient to "bridge the gaps", and then "un-buffer" the result inwards by the same (negative) distance. But the buffer computation can "round off" corners, which usually produces a poor match to the input polygons. It also fills in the outer boundary of the original polygons.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqPihIu6XtNYEIYuREaHUcsq_4n1F18ADxinDQTHpEF2u8VyvqVZ9jgY83VmPdpxj6nf1I3S6d2kLFhFN_F7UlzYfYEtbvs3NmqmA-8J1CsOwwzFQyDIsBMnm3ojBvbYqtWO8j98fuJtl5zcVjdUu9I16DX3J2IWN52PG9YIDdDesiiZRvdv9WJ3y-/s476/buffer-unbuffer.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="233" data-original-width="476" height="157" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgqPihIu6XtNYEIYuREaHUcsq_4n1F18ADxinDQTHpEF2u8VyvqVZ9jgY83VmPdpxj6nf1I3S6d2kLFhFN_F7UlzYfYEtbvs3NmqmA-8J1CsOwwzFQyDIsBMnm3ojBvbYqtWO8j98fuJtl5zcVjdUu9I16DX3J2IWN52PG9YIDdDesiiZRvdv9WJ3y-/s320/buffer-unbuffer.png" width="320" /></a></div><h3 style="text-align: left;">Approach: Concave Hull of Points</h3><p>A more sophisticated approach is to use a <a href="http://lin-ear-th-inking.blogspot.com/2022/01/concave-hulls-in-jts.html">concave hull algorithm</a>. But most (or all?) available concave hull algorithms use <b>points</b> as the input constraints. The vertices of the polygons <i>could</i> be used as the constraint points, but since the polygon boundaries are not respected, the computed hull may cross the polygon edges and hence not cover the polygons. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVOqzAPmSlsKmOkdJbfv2I-T0uMT79Vg2qCT3fkWtqhKIuZKcjl5AR1TA3sC-FZk332S5drtYcqr2yupYnGnbe8ZSmsv6Vuo3Fc-Og7erH30Euz27fcn3n3zwBMDhCfwQe8ql8mhJJIjG_q9zLuu3f0gOFpecKSzTE0ompUddexnlcy8AZAVXigx2S/s508/concave-hull-points.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="250" data-original-width="508" height="157" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiVOqzAPmSlsKmOkdJbfv2I-T0uMT79Vg2qCT3fkWtqhKIuZKcjl5AR1TA3sC-FZk332S5drtYcqr2yupYnGnbe8ZSmsv6Vuo3Fc-Og7erH30Euz27fcn3n3zwBMDhCfwQe8ql8mhJJIjG_q9zLuu3f0gOFpecKSzTE0ompUddexnlcy8AZAVXigx2S/s320/concave-hull-points.png" width="320" /></a></div><p>Densifying the polygon boundaries helps, but introduces another problem - the computed hull can extend beyond the outer boundaries of individual polygons. And it introduces new vertices not present in the original data.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaRMmJu9dcAegw41NmBJob-c8nbuvFmQv3rB4Obu9FCIBk9qinQjgbhYu1mKwnQo6MVMzc_IQhRQWoneFWg1fnmdtobAlK8zPoyep-jC9x1bVsQzPBmdMqHKMTcCaf89WMV_gpEfSeMLRP_hf2I2wcThGQr87w2ANhcaFjH7bSw6C1C1X9YH8gxoVk/s473/densify-concave-hull-points.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="230" data-original-width="473" height="156" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiaRMmJu9dcAegw41NmBJob-c8nbuvFmQv3rB4Obu9FCIBk9qinQjgbhYu1mKwnQo6MVMzc_IQhRQWoneFWg1fnmdtobAlK8zPoyep-jC9x1bVsQzPBmdMqHKMTcCaf89WMV_gpEfSeMLRP_hf2I2wcThGQr87w2ANhcaFjH7bSw6C1C1X9YH8gxoVk/s320/densify-concave-hull-points.png" width="320" /></a></div><h3 style="text-align: left;">Solution: Concave Hull of Polygons</h3><p>What is needed is a concave hull algorithm that <b>accepts polygons as constraints</b>, and thus respects their boundaries. The <b><a href="https://github.com/locationtech/jts">JTS Topology Suite</a></b> now provides this capability in a class called <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/pull/870">ConcaveHullOfPolygons</a></span> (not a cute name, but descriptive). It provides exactly the solution desired for the gap removal example: </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzoY5tNtJU-9yfiubsSN5AswdeUnuA1C7_LAYkLWFV9mui_PkM9tF8raO6kILeg3LWLsc8ZotEb1UPNYwXheEOUeT2DIDWVm2ce8qB7aNJ9epRQz1Ml9WC3iJIic3bex5_uCky2Z74ItH2KNV9OPJNA-QhTuI0ozyZaIAFasgaF5yV2MIW3nLD3ZpK/s467/concavehull-poly-gap.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="237" data-original-width="467" height="162" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzoY5tNtJU-9yfiubsSN5AswdeUnuA1C7_LAYkLWFV9mui_PkM9tF8raO6kILeg3LWLsc8ZotEb1UPNYwXheEOUeT2DIDWVm2ce8qB7aNJ9epRQz1Ml9WC3iJIic3bex5_uCky2Z74ItH2KNV9OPJNA-QhTuI0ozyZaIAFasgaF5yV2MIW3nLD3ZpK/s320/concavehull-poly-gap.png" width="320" /></a></div><h3 style="text-align: left;">The Concave Hull of Polygons API</h3><p>Like concave hulls of point sets, concave hulls of polygons form a sequence of hulls, with the amount of concaveness determined by a numeric parameter. <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHullOfPolygons.java">ConcaveHullOfPolygons</a></span> uses the same parameters as the JTS <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/pull/823">ConcaveHull</a></span> algorithm. The control parameter determines the maximum line length in the triangulation underlying the hull. This can be specified as an <b>absolute length</b>, or as a <b>ratio between the longest and shortest lines</b>. </p><p>Further options are:</p><p></p><ul style="text-align: left;"><li>The computed hull can be kept "<b>tight</b>" to the outer boundaries of the individual polygons. This allows filling gaps between polygons without distorting their original outer boundaries. Otherwise, the concaveness of the outer boundary will be decreased to match the distance parameter specified (which may be desirable in some situations).</li><li><b>Holes</b> can be allowed to be present in the computed hull</li><li>Instead of the hull, the <b>fill area</b> between the input polygons can be computed. </li></ul><div>As usual, this code will be ported to <a href="https://github.com/libgeos/geos">GEOS</a>, and from there it can be exposed in the downstream <a href="https://libgeos.org/usage/bindings/">libraries</a> and <a href="https://trac.osgeo.org/geos/wiki/Applications">projects</a>.</div><p></p><h3 style="text-align: left;">Examples of Concave Hulls of Polygons</h3><p>Here are examples of using <span style="font-family: courier;">ConcaveHullOfPolygons</span> for the use cases above:</p><h4 style="text-align: left;">Example: Generalizing Building Groups</h4><p>Using the "tight" option allows following the outer building outlines.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQdpX2Bjut8NQ4BHhQEwEiJ5lggjfQONODtU81U0JRS8IYNsFRqc0uDJJV56pGAT4m8few4qMoJMf467uvR8mC0TNSXLCuD87G7DgM3y0sHdwAzBUktuseOPO_VdEf1XjQqQOkhrxZnLzxZdBodRN99bDdwhjsj8ZiqFgyT58CiNREbAifDegQ5qbK/s657/concavehullpoly-buildings.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="455" data-original-width="657" height="278" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQdpX2Bjut8NQ4BHhQEwEiJ5lggjfQONODtU81U0JRS8IYNsFRqc0uDJJV56pGAT4m8few4qMoJMf467uvR8mC0TNSXLCuD87G7DgM3y0sHdwAzBUktuseOPO_VdEf1XjQqQOkhrxZnLzxZdBodRN99bDdwhjsj8ZiqFgyT58CiNREbAifDegQ5qbK/w400-h278/concavehullpoly-buildings.png" width="400" /></a></div><br /><p></p><h4 style="text-align: left;">Example: Aggregating Block Polygons</h4><div>The concave hull of a set of block polygons for an oceanside suburb. Note how the "tight" option allows the hull to follow the convoluted, fine-grained coastline on the right side.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFTtjjFXtEHuhq7EicC1JVdpl_CRZY-vuSXqJImnZqxJBO5oh3mLyNv_zo_nb-8B4ZwQZMJgXpIlSrWchGgG1q0iERWX_Cr_abAn7IQ9DQQcfabWDPWdXKJpiDnzR0Bjn0srx3ZJ-NdWpXXddncUUhXYSZZ3y_rSguenAxxm5EJCrwN_bNnGCVy-e1/s497/concavehull-poly-blocks.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="425" data-original-width="497" height="343" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhFTtjjFXtEHuhq7EicC1JVdpl_CRZY-vuSXqJImnZqxJBO5oh3mLyNv_zo_nb-8B4ZwQZMJgXpIlSrWchGgG1q0iERWX_Cr_abAn7IQ9DQQcfabWDPWdXKJpiDnzR0Bjn0srx3ZJ-NdWpXXddncUUhXYSZZ3y_rSguenAxxm5EJCrwN_bNnGCVy-e1/w400-h343/concavehull-poly-blocks.png" width="400" /></a></div><h4 style="text-align: left;">Example: Removing Gaps to Merge Polygons</h4><div>Polygons separated by a narrow gap can be merged by computing their concave hull using a small distance and keeping the boundary tight.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnC32rkRL_PM7Kjp11iWXtowsEAEkqvgfz4-YGnrEVXts1WzAKN49HjUXUW6oG8l6MxekT7M_W4Mc5iuAXi1_bwQjkoC37FuV0z_POFeL5oM8W9eYxBhzLXG2GDU4WaiqSNKlBwmA32g6ofhWk8ebWS1aeNAkZi3ojbGqydiOXKx4_dX3nNLoFJyOf/s467/concavehull-poly-gap.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="237" data-original-width="467" height="203" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgnC32rkRL_PM7Kjp11iWXtowsEAEkqvgfz4-YGnrEVXts1WzAKN49HjUXUW6oG8l6MxekT7M_W4Mc5iuAXi1_bwQjkoC37FuV0z_POFeL5oM8W9eYxBhzLXG2GDU4WaiqSNKlBwmA32g6ofhWk8ebWS1aeNAkZi3ojbGqydiOXKx4_dX3nNLoFJyOf/w400-h203/concavehull-poly-gap.png" width="400" /></a></div><div><br /></div><h4 style="text-align: left;">Example: Fill Area Between Polygons</h4><div>The "fill area" portion of the hull between two polygons can be computed as a separate polygon. This could be used to provide an "Extend to Meet" construction by unioning the fill polygon with one of the input polygons. It can also be used to determine the "visible boundary", provided by the intersection of the fill polygon with the input polygon(s).</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwNlpdj6ptKl_VSC_Vf_vMSIHSXaL6ohnpT03wjWOhHMpldkws_MNuzxZ9AQWMQMhoGiVYy94vejvVfqBH6kP6wBLwRRaHQpfcggglXFr7WclV87-XvM9qDB582kJFbR9nXNFSv3APjgB6YruaLRuInV_tOOeHcdSFgLl3Qo9DbWFi7fq9RZa6T6RV/s335/concavehull-poly-fill.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="247" data-original-width="335" height="295" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhwNlpdj6ptKl_VSC_Vf_vMSIHSXaL6ohnpT03wjWOhHMpldkws_MNuzxZ9AQWMQMhoGiVYy94vejvVfqBH6kP6wBLwRRaHQpfcggglXFr7WclV87-XvM9qDB582kJFbR9nXNFSv3APjgB6YruaLRuInV_tOOeHcdSFgLl3Qo9DbWFi7fq9RZa6T6RV/w400-h295/concavehull-poly-fill.png" width="400" /></a></div><br /><div><br /></div><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-91507028702275627472022-05-04T17:15:00.003-07:002022-05-04T17:15:30.459-07:00Using Outer Hulls for Smoothing Vectorized Polygons<p>The electrons were hardly dry on the <a href="http://lin-ear-th-inking.blogspot.com/2022/04/outer-and-inner-concave-polygon-hulls.html">JTS Outer and Inner Polygon Hull</a> post when another interesting use case popped up on <b>GIS StackExchange</b>. The <a href="https://gis.stackexchange.com/questions/429747/simplification-by-constrained-anti-aliasing-in-postgis">question</a> was how to remove <a href="https://en.wikipedia.org/wiki/Aliasing">aliasing</a> artifacts (AKA "<a href="https://en.wikipedia.org/wiki/Jaggies">jaggies</a>") from polygons created by vectorizing raster data, with the condition that the <b>result should contain the original polygon</b>. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkGfHEeQFvM30J9sosFJvUk8H9whQkk6gM_yEtB8M-lW5kiZDojrwRSxn7aFk05LTWULwPMOuP9CXLqk9ecgsgU7543Krri9XloCVGjfvlYyqg2TwmBUGf8VDgiIpTji8D76Qgdrwas8pJufPyUFmE4kGk6wZOh-rhyg2SsT-RjntRLaXprA2DvECN/s462/vanisle-gridded.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="261" data-original-width="462" height="181" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgkGfHEeQFvM30J9sosFJvUk8H9whQkk6gM_yEtB8M-lW5kiZDojrwRSxn7aFk05LTWULwPMOuP9CXLqk9ecgsgU7543Krri9XloCVGjfvlYyqg2TwmBUGf8VDgiIpTji8D76Qgdrwas8pJufPyUFmE4kGk6wZOh-rhyg2SsT-RjntRLaXprA2DvECN/s320/vanisle-gridded.png" width="320" /></a></div><p style="text-align: center;"><i>A polygon for Vancouver Island vectorized from a coarse raster dataset. </i><i>Aliasing artifacts are obvious.</i></p><p>This problem is often handled by applying a simplification or smoothing process to the "jaggy" polygon boundary. This works, as long as the process preserves polygonal topology (e.g. such as the JTS <a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/simplify/TopologyPreservingSimplifier.html"><span style="font-family: courier;">TopologyPreservingSimplifier</span></a>). But generally this output of this process does <i>not</i> contain the input polygon, since the simplification/smoothing can alter the boundary inwards as well as outwards. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJYhMOLEatwdAkaQ9YFH5OuVmxrgpk6Lo8TqX0gBp8V4uZ1SDx5pPKuxjTrUDZy-xQlMgbOPT2w78Fb7HjgpMhdJ6UNIgABjs0OPUgvHbPBckvkEfap0r9ejysKMb9JhnPwVrJpwx5nrBufFZTuL91QF731sbKQAtg_VClop3kacGemfQKJ5oc5qYM/s455/vanisle-tps.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="257" data-original-width="455" height="181" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhJYhMOLEatwdAkaQ9YFH5OuVmxrgpk6Lo8TqX0gBp8V4uZ1SDx5pPKuxjTrUDZy-xQlMgbOPT2w78Fb7HjgpMhdJ6UNIgABjs0OPUgvHbPBckvkEfap0r9ejysKMb9JhnPwVrJpwx5nrBufFZTuL91QF731sbKQAtg_VClop3kacGemfQKJ5oc5qYM/s320/vanisle-tps.png" width="320" /></a></div><i><div style="text-align: center;"><i>Simplification using TopologyPreservingSimplifier with distance = 0.1. Artifacts are removed, but the simplified polygon does not fully contain the original.</i></div></i><p>In contrast, the JTS <b>Polygon Outer Hull</b> algorithm is designed to do exactly what is required: it reduces the number of vertices, while guaranteeing that the input polygon is contained in the result. It is essentially a simplification method which also preserves polygonal topology (using an area-based approach similar to the <a href="https://en.wikipedia.org/wiki/Visvalingam%E2%80%93Whyatt_algorithm">Visvalingham-Whyatt algorithm</a>).</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUjvCYk1zQvuDKw8eP1pC49ULlDAelXP2_rKZcYNVOft4jaE0gS2XNznp_he2i2UHI7hyjZgLWB8HZGskej9_ndnBGJ4l8MsCPyFaqGQAxjO9-W_u2V-ZVw7OfgFkhEW0MbcBha6qA_5S01PBO8JcG4KqgeE-_OAZToz-kzWDLqj_p4z-cY9JHszee/s464/vanisle-outer-hull.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="272" data-original-width="464" height="188" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhUjvCYk1zQvuDKw8eP1pC49ULlDAelXP2_rKZcYNVOft4jaE0gS2XNznp_he2i2UHI7hyjZgLWB8HZGskej9_ndnBGJ4l8MsCPyFaqGQAxjO9-W_u2V-ZVw7OfgFkhEW0MbcBha6qA_5S01PBO8JcG4KqgeE-_OAZToz-kzWDLqj_p4z-cY9JHszee/s320/vanisle-outer-hull.png" width="320" /></a></div><i><div style="text-align: center;"><i>Outer Hull using vertex ratio = 0.25. Artifacts are removed, and the original polygon is contained in the hull polygon.</i></div></i><p>Here's a real-world example, taken from the <a href="https://geodata.ucdavis.edu/gadm/gadm4.0/shp/gadm40_DEU_shp.zip">GADM dataset for administrative areas of Germany</a>. The coastline of the state of Mecklenburg-Vorpommern appears to have been derived from a raster, and thus exhibits aliasing artifacts. Computing the outer hull with a fairly conservative parameter eliminates most of the artifacts, and ensures polygonal topology is preserved.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP5ePPfukjMaQ_LJMhJzwiElfHF51TdHnhzBUy8XvrQsxe4YBkP8BaMQS8flKJyQ_NpBiQhTkfkmP3-PAB3IDMKYTkwekOYMPvy-rlvXcNS-o7qcvilTBdGVO-EUAKx_s9N-rTeAt5Lplunsazn18kGkKsGFP9XCVJYOflXMIfy8qSokb3lGa9Ly7V/s505/gadm-deu1-outer-hull.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="433" data-original-width="505" height="274" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiP5ePPfukjMaQ_LJMhJzwiElfHF51TdHnhzBUy8XvrQsxe4YBkP8BaMQS8flKJyQ_NpBiQhTkfkmP3-PAB3IDMKYTkwekOYMPvy-rlvXcNS-o7qcvilTBdGVO-EUAKx_s9N-rTeAt5Lplunsazn18kGkKsGFP9XCVJYOflXMIfy8qSokb3lGa9Ly7V/s320/gadm-deu1-outer-hull.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>A portion of the coastline of <span style="text-align: left;">Mecklenburg-Vorpommern showing aliasing artifacts. The Outer Hull computed with vertex ratio = 0.25 eliminates most artifacts, and preserves the coastline topology.</span></i></div><h3 style="text-align: left;">Future Work</h3><p>A potential issue for using Outer Hull as a smoothing technique is the choice of parameter value controlling the amount of change. The algorithm provides two options: the ratio of reduction in the number of vertices, or the fraction of change in area allowed. Both of these are scale-independent, and reflect natural goals for controlling simplification. But neither relate directly to the goal of removing "stairstep" artifacts along the boundary. This might be better specified via a distance-based parameter. The parameter value could then be determined based on the known artifact size (i.e. the resolution of the underlying grid). Since the algorithm for Outer Hull is quite flexible, this should be feasible to implement. </p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-78035609751039106882022-04-25T14:20:00.002-07:002022-08-25T11:03:35.348-07:00Outer and Inner Polygon Hulls in JTS<p>The <a href="https://github.com/locationtech/jts"><b>JTS Topology Suite</b></a> recently gained the ability to compute <b><a href="http://lin-ear-th-inking.blogspot.com/2022/01/concave-hulls-in-jts.html">concave hulls</a></b>. The Concave Hull algorithm computes a polygon enclosing a set of points using a parameter to determine the "tightness". However, for polygonal inputs the computed concave hull is built only using the polygon vertices, and so does not always respect the polygon boundaries. This means the concave hull may not contain the input polygon.</p><p>It would be useful to be able to compute the <b>"outer hull"</b> of a polygon. This is a <b>valid polygon</b> formed by a <b>subset of the vertices</b> of the input polygon which <b>fully contains</b> the input polygon. Vertices can be eliminated as long as the resulting boundary does not self-intersect, and does not cross into the original polygon.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFpu4OASprcjEP_CqZctEC11WDw9WOcKMF27PDwz5NaODmCNUAG-Locuqm6NQkrFHqsOWXfIJWvd6ldKh_hwHR0OoEWeHm-3mIf1fUsjyALgrhbcATdG5DlSlL7ODOQw82hZLI3GNLyQgDfGFVGnS7zTz7rP65e7RjRviZD37vieU-J-8AIZrtUeaG/s533/polygon-hull-switz.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="203" data-original-width="533" height="153" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiFpu4OASprcjEP_CqZctEC11WDw9WOcKMF27PDwz5NaODmCNUAG-Locuqm6NQkrFHqsOWXfIJWvd6ldKh_hwHR0OoEWeHm-3mIf1fUsjyALgrhbcATdG5DlSlL7ODOQw82hZLI3GNLyQgDfGFVGnS7zTz7rP65e7RjRviZD37vieU-J-8AIZrtUeaG/w400-h153/polygon-hull-switz.png" width="400" /></a></div><p style="text-align: center;"><i>An outer hull of a polygon representing Switzerland</i></p>As with point-set concave hulls, the vertex reduction is controlled by a numeric parameter. This creates a sequence of hulls of increasingly larger area with smaller vertex counts. At an extreme value of the parameter, the outer hull is the same as the convex hull of the input.<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMj3LdLikSfrD5jsVzLk6YWY5D66sQucZW7JtqHpGemEVJxBtXOK4Dk8tpeF-M2fGik2ePsqXk8Gf5r73qbFxHIaUOWzqAAXEXP77B66ELYYNFQ-11wmsx_JSwoRhVaOmcLP4xgEqZFXMJ7HgIm04asXQ-xT2_elxnDI3e86og6fbMiA3-31PuwRlF/s716/polygon-hull-nz-sequence.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="399" data-original-width="716" height="223" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjMj3LdLikSfrD5jsVzLk6YWY5D66sQucZW7JtqHpGemEVJxBtXOK4Dk8tpeF-M2fGik2ePsqXk8Gf5r73qbFxHIaUOWzqAAXEXP77B66ELYYNFQ-11wmsx_JSwoRhVaOmcLP4xgEqZFXMJ7HgIm04asXQ-xT2_elxnDI3e86og6fbMiA3-31PuwRlF/w400-h223/polygon-hull-nz-sequence.png" width="400" /></a></div><div style="text-align: center;"><i>A sequence of outer hulls of New Zealand's North Island</i></div><p>The outer hull concept extends to handle holes and MultiPolygons. In all cases the hull boundaries are constructed so that they do not cross each other, thus ensuring the validity of the result.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDSo5Cuq_W3CcLhIbkCLI6uZysFguC-1GLkEvM0eVLBBCbJ2JpVkhxFwPNyHYmuoid2I7pEPyeqCMG_2XANZpGgGBWnfOcD0oBZtIdNKlX8p7ZWUv_PcAuNisOhZShKyGyoQFGT83-pcVchZLet-WToeB5gBNoMvDnvfk-ovCmZxLFjL7-lemnHSap/s371/polygon-hull-multi.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="344" data-original-width="371" height="297" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDSo5Cuq_W3CcLhIbkCLI6uZysFguC-1GLkEvM0eVLBBCbJ2JpVkhxFwPNyHYmuoid2I7pEPyeqCMG_2XANZpGgGBWnfOcD0oBZtIdNKlX8p7ZWUv_PcAuNisOhZShKyGyoQFGT83-pcVchZLet-WToeB5gBNoMvDnvfk-ovCmZxLFjL7-lemnHSap/s320/polygon-hull-multi.png" width="320" /></a></div><p style="text-align: center;"><i>An outer hull of a MultiPolygon for the coast of Denmark. The hull polygons do not cross.</i></p><p>It's also possible to construct <b>inner hulls</b> of polygons, where the constructed hull is fully <b>within</b> the original polygon.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDHxwmuGKO9A16dN0lrADExT8HGsOWvE6qOrF3kEGPZfIp7va1j1xCyfghrQuSDhrtDVqFUCGjfS9D-OWH94QqGLS21cyvcW0gGDdlbyM6r_dco-3YosfgNs4H_ugn1NYXCs6n40X1L55lg4k2sCRKOER2rMKPSD11mu3ZjI3vnnhaqJ823R0WWWQR/s508/polygon-hull-inner.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="217" data-original-width="508" height="171" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhDHxwmuGKO9A16dN0lrADExT8HGsOWvE6qOrF3kEGPZfIp7va1j1xCyfghrQuSDhrtDVqFUCGjfS9D-OWH94QqGLS21cyvcW0gGDdlbyM6r_dco-3YosfgNs4H_ugn1NYXCs6n40X1L55lg4k2sCRKOER2rMKPSD11mu3ZjI3vnnhaqJ823R0WWWQR/w400-h171/polygon-hull-inner.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>An inner hull of Switzerland</i></div><p>Inner hulls also support holes and MultiPolygons. At an extreme value of the control parameter, holes become convex hulls, and a polygon shell reduces to a triangle (unless blocked by the presence of holes).</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3q5ljk7RQV6-ImHzltkiuWjahE1mlPjNi8xdkx46TazpxUAFUerCIFfwmQJdxMOzIELGf2gMhoxiSIMr5SxDO2T_uemFeVIvao0H4KDhLo1zWbViioeAfzHdFjng354OVKtZ31EyFq0wjxabLsVaiOVWB21waztiCGc5AVYQNd14Caq6xCXEdnfDz/s448/polygon-hull-inner-lake.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="448" data-original-width="358" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3q5ljk7RQV6-ImHzltkiuWjahE1mlPjNi8xdkx46TazpxUAFUerCIFfwmQJdxMOzIELGf2gMhoxiSIMr5SxDO2T_uemFeVIvao0H4KDhLo1zWbViioeAfzHdFjng354OVKtZ31EyFq0wjxabLsVaiOVWB21waztiCGc5AVYQNd14Caq6xCXEdnfDz/w320-h400/polygon-hull-inner-lake.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>An inner hull of a lake with islands. The island holes become convex hulls, and prevent the outer shell from reducing fully to a triangle</i></div><p>A hull can provide a significant reduction in the vertex size of a polygon for a minimal change in area. This could allow faster evaluation of spatial predicates, by pre-filtering with smaller hulls of polygons.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyIl8swJc-7Y-aYi3SMUGddcCOIgvKiWls5sb30FYwqe1rnyzTl5ygsZKkJBDhlqK5e6JWXrHocqAcUvAAZ2HIPd47vuwvEFl3rJuG8GQYbD2_Gjr5IJMDO27JVMIknbRtyfpMpoCDdjqQuJJjgvQuEWTHuebDIgMtv9U-PHOi6Ns3gSbp74e8Ejpo/s496/polygon-hull-brazil.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="496" data-original-width="448" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgyIl8swJc-7Y-aYi3SMUGddcCOIgvKiWls5sb30FYwqe1rnyzTl5ygsZKkJBDhlqK5e6JWXrHocqAcUvAAZ2HIPd47vuwvEFl3rJuG8GQYbD2_Gjr5IJMDO27JVMIknbRtyfpMpoCDdjqQuJJjgvQuEWTHuebDIgMtv9U-PHOi6Ns3gSbp74e8Ejpo/w361-h400/polygon-hull-brazil.png" width="361" /></a></div><p style="text-align: center;"><i>An outer hull of Brazil provides a 10x reduction in vertex size, with only ~1% change in area.</i></p><p>This has been on the JTS To-Do list for a while (I first <a href="https://www.mail-archive.com/jts-devel@lists.jump-project.org/msg01098.html">proposed it</a> back in 2009). At that time it was presented as a way of simplifying polygonal geometry. Of course JTS has had the <a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/simplify/TopologyPreservingSimplifier.html"><span style="font-family: courier;">TopologyPreservingSimplifier</span></a> for many years. But it doesn't compute a strictly outer hull. Also, it's based on <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm">Douglas-Peucker simplification</a>, which isn't ideal for polygons. </p><p>It seems there's quite a need for this functionality, as shown in these GIS-StackExchange posts (<a href="https://gis.stackexchange.com/questions/334119/original-polygon-is-completely-contained-by-new-after-simplifying">1</a>, <a href="https://gis.stackexchange.com/questions/217328/simplify-a-shapefile-geojson-polygon-without-uncovering-territory">2</a>, <a href="https://gis.stackexchange.com/questions/75551/lossless-polygon-simplification">3</a>, <a href="https://gis.stackexchange.com/questions/338331/simplify-polygon-no-loss-allowed-looking-for-a-concave-hull-arcmap">4</a>). There's even existing implementations on Github: <a href="https://github.com/prakol16/rdp-expansion-only"><span style="font-family: courier;">rdp-expansion-only</span></a> and <a href="https://github.com/albertferras/simplipy"><span style="font-family: courier;">simplipy</span></a> (both in Python) - but both of these sound like they have some significant issues. </p><p>Recent JTS R&D (on concave hulls and polygon triangulation) has provided the basis for an effective, performant polygonal concave hull algorithm. This is now released as the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/simplify/PolygonHullSimplifier.java"><span>PolygonHull</span>Simplifier</a></span> class in JTS.</p><h3 style="text-align: left;">The <span style="font-family: courier;">PolygonHullSimplifier</span> API</h3><p>Polygon hulls have the following characteristics:</p><p></p><ul><li>Hulls can be constructed for Polygons and MultiPolygons, including holes.</li><li>Hull geometries have the same structure as the input. There is a one-to-one correspondence for elements, shells and holes.</li><li>Hulls are valid polygonal geometries.</li><li>The hull vertices are a subset of the input vertices.</li></ul><p></p><p>The PolygonHullSimplifier algorithm supports computing both <b>outer</b> and <b>inner</b> hulls. </p><p></p><ul><li><b>Outer hulls</b> <i>contain</i> the input geometry. Vertices forming concave corners (convex for holes) are removed. The maximum outer hull is the convex hull(s) of the input polygon(s), with holes reduced to triangles.</li><li><b>Inner hulls</b> are contained <i>within</i> the input geometry. Vertices forming convex corners (concave for holes) are removed. The minimum inner hull is a triangle contained in (each) polygon, with holes expanded to their convex hulls. </li></ul><p>The number of vertices removed is controlled by a numeric parameter. Two different parameters are provided:</p><p></p><ul style="text-align: left;"><li>the <b>Vertex Number Fraction</b> specifies the desired result vertex count as a fraction of the number of input vertices. The value 1 produces the original geometry. Smaller values produce simpler hulls. The value 0 produces the maximum outer or minimum inner hull.</li><li>the <b>Area Delta Ratio</b> specifies the desired maximum change in the ratio of the result area to the input area. The value 0 produces the original geometry. Larger values produce simpler hulls. </li></ul>Defining the parameters as ratios means they are independent of the size of the input geometry, and thus easier to specify for a range of inputs. Both parameters are targets rather than absolutes; the validity constraint means the result hull may not attain the specified value in some cases. <h3 style="text-align: left;">Algorithm Description</h3><p>The algorithm removes vertices via "corner clipping". Corners are triangles formed by three consecutive vertices in a (current) boundary ring of a polygon. Corners are removed when they meet certain criteria. For an <b>outer hull</b>, a corner can be removed if it is <b>concave</b> (for shell rings) or <b>convex</b> (for hole rings). For an <b>inner hull</b> the removable corner orientations are reversed. </p><p>In both variants, corners are removed only if the triangle they form does <i>not</i> contain other vertices of the (current) boundary rings. This condition prevents self-intersections from occurring within or between rings. This ensures the resulting hull geometry is topologically valid. Detecting triangle-vertex intersections is made performant by maintaining a spatial index on the vertices in the rings. This is supported by an index structure called a <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/index/VertexSequencePackedRtree.java"><span style="font-family: courier;">VertexSequencePackedRtree</span></a>. This is a <b>semi-static R-tree</b> built on the list of vertices of each polygon boundary ring. Vertex lists typically have a high degree of <b>spatial coherency</b>, so the constructed R-tree generally provides good <b>space utilization</b>. It provides <b>fast bounding-box search</b>, and supports <b>item removal</b> (allowing the index to stay consistent as ring vertices are removed).</p><p>Corners that are candidates for removal are kept in a <b>priority queue ordered by area</b>. Corners are removed in order of smallest area first. This minimizes the amount of change for a given vertex count, and produces a better quality result. Removing a corner may create new corners, which are inserted in the priority queue for processing. Corners in the queue may be invalidated if one of the corner side vertices has previously been removed; invalid corners are discarded. </p><p>This algorithm uses techniques originated for the Ear-Clipping approach used in the JTS <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/triangulate/polygon/PolygonTriangulator.java">PolgyonTriangulator</a></span> implementation. It also has a similarity to the Visvalingham-Whyatt simplification algorithm. But as far as I know using this approach for computing outer and inner hulls is novel. (After the fact I found a recent paper about a similar construction called a Shortcut Hull [<a href="https://arxiv.org/abs/2106.13620">Bonerath et al 2020</a>], but it uses a different approach).</p><h3 style="text-align: left;">Further Work</h3><p>It should be straightforward to use this same approach to implement a variant of Topology-Preserving Simplifier using the corner-area-removal approach (as in <b>Visvalingham-Whyatt simplification</b>). The result would be a simplified, topologically-valid polygonal geometry. The simplification parameter limits the number of result vertices, or the net change in area. The resulting shape would be a good approximation of the input, but is not necessarily be either wholly inside or outside.</p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-84057575253433533452022-01-27T09:30:00.000-08:002022-01-27T09:30:01.791-08:00Cubic Bezier Curves in JTS<p>As the title of this blog indicates, I'm a fan of linearity. But sometimes a little non-linearity makes things more interesting. A convenient way to generate non-linear curved lines is to use <b><a href="https://en.wikipedia.org/wiki/B%C3%A9zier_curve">Bezier Curves</a></b>. Bezier Curves are curves defined by polynomials. Bezier curves can be defined for polynomials of any degree, but a popular choice is to use <b>cubic Bezier curves</b> defined by polynomials of degree 3. These are relatively easy to implement, visually pleasing, and versatile since they can model <a href="https://en.wikipedia.org/wiki/Ogee">ogee</a> or <a href="https://en.wikipedia.org/wiki/Sigmoid_function">sigmoid</a> ("S"-shaped) curves.</p><p>A single cubic Bezier curve is specified by four points: two endpoints forming the baseline, and two control points. The curve shape lies within the quadrilateral convex hull of these points.</p><p><i>Note: the images in this post are created using the JTS TestBuilder.</i></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjEJ5-iOb0I3J75E1-z71sv04ZE-UKtkzXW1ffH1VTftyD-7YBY7shAFi0jKClNToZu_oBFQ076Uydrl-magY3kIioxKrGojE_mjveh3XwfmNT4DIAtjIR5nRWwnCMHCMPFDiknDjQWBh_Jzw0P2uK97GczSCxQTANVHqEE2obxsm2-_jt4DdMhXWCK=s376" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="376" data-original-width="296" height="320" src="https://blogger.googleusercontent.com/img/a/AVvXsEjEJ5-iOb0I3J75E1-z71sv04ZE-UKtkzXW1ffH1VTftyD-7YBY7shAFi0jKClNToZu_oBFQ076Uydrl-magY3kIioxKrGojE_mjveh3XwfmNT4DIAtjIR5nRWwnCMHCMPFDiknDjQWBh_Jzw0P2uK97GczSCxQTANVHqEE2obxsm2-_jt4DdMhXWCK=s320" width="252" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Cubic Bezier Curves, showing endpoints and control points</i></div><p>A sequence of Bezier curves can be chained together to form a curved path of any required shape. There are several ways to join <b><a href="https://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve">composite Bezier curves</a></b>. The simplest join constraint is <b>C0-continuity</b>: the curves touch at endpoints, but the join may be a sharp angle. <b>C1-continuity</b> (differentiable) makes the join smooth. This requires the control vectors at a join point to be collinear and opposite. If the control vectors are of different lengths there will be a different radius of curvature on either side. The most visually appealing join is provided by <b>C2-continuity</b> (twice-differentiable), where the curvature is identical on both sides of the join. To provide this the control vectors at a vertex must be collinear, opposite <i>and</i> have the same length.</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjIL6JAPavWK_pQnL8WDIZhP4qlUvJMbivc01EUKjBzUWpX_rjjOG0Grn3MJ2VTssS6yn7cCWwDf7qgGHZsMEDOC37ZHVgY8HXXLqGRPU0S8cp78xg6jmHxJjNNOTWVVg02dKf87AndBQCJKenmXkIHifys0laihlPpJRJc72U8kvO3iAI9JQ0Q1Qn9=s440" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="366" data-original-width="440" height="266" src="https://blogger.googleusercontent.com/img/a/AVvXsEjIL6JAPavWK_pQnL8WDIZhP4qlUvJMbivc01EUKjBzUWpX_rjjOG0Grn3MJ2VTssS6yn7cCWwDf7qgGHZsMEDOC37ZHVgY8HXXLqGRPU0S8cp78xg6jmHxJjNNOTWVVg02dKf87AndBQCJKenmXkIHifys0laihlPpJRJc72U8kvO3iAI9JQ0Q1Qn9=s320" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Bezier Curve with C2-continuity</i></div><div class="separator" style="clear: both; text-align: center;"><br /></div>A recent addition to the <a href="https://github.com/locationtech/jts">JTS Topology Suite</a> is the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/shape/CubicBezierCurve.java">CubicBezierCurve</a></span> class, which supports constructing Bezier Curves from LineStrings and Polygons. JTS only supports representing linear geometries, so curves must be approximated by sequences of line segments. (The buffer algorithm uses this technique to approximate the circular arcs required by round joins.) <div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjsCZaj9lGqs3eV-nA6mvicJEqiRz2c-5YZ9wKBhaY-yGWVIPRMnJ6Mc3qayY8X1KE12hXgGt-lA65Gu3k-98tUQCKdxjWspge95Or7zvt_bp-o0FccO7PVrYE1a0TyhrQ6IsPWgG87E8WT-Eo0kDZw4CDbJJbjLN_EgrzHoGo94_4eG2shKbxWiIrP=s373" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="167" data-original-width="373" height="143" src="https://blogger.googleusercontent.com/img/a/AVvXsEjsCZaj9lGqs3eV-nA6mvicJEqiRz2c-5YZ9wKBhaY-yGWVIPRMnJ6Mc3qayY8X1KE12hXgGt-lA65Gu3k-98tUQCKdxjWspge95Or7zvt_bp-o0FccO7PVrYE1a0TyhrQ6IsPWgG87E8WT-Eo0kDZw4CDbJJbjLN_EgrzHoGo94_4eG2shKbxWiIrP=s320" width="320" /></a></div><br /><div><div style="text-align: center;"><i>Bezier Curve approximated by line segments</i></div><div><br /></div><div>Bezier curves can be generated on both lines and polygons (including holes):<br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjWZCMUtzKYUO4NOoysmGa2jo9mv1yw0WnpZuLOsVpQKGEDV5gJ2-S1RuwbkZWZ773AZNb4pAo1iUEEG4dVHsxZFrP4xeWtQwYpn8yFy9O35pqHySh7ZgNc47qi5-ARwIyEw3EgdtMQYgBwWI5iLJmaa8CmBUY6Z2snxKgVM4gS_BfkHpedWesax9gI=s472" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="401" data-original-width="472" height="272" src="https://blogger.googleusercontent.com/img/a/AVvXsEjWZCMUtzKYUO4NOoysmGa2jo9mv1yw0WnpZuLOsVpQKGEDV5gJ2-S1RuwbkZWZ773AZNb4pAo1iUEEG4dVHsxZFrP4xeWtQwYpn8yFy9O35pqHySh7ZgNc47qi5-ARwIyEw3EgdtMQYgBwWI5iLJmaa8CmBUY6Z2snxKgVM4gS_BfkHpedWesax9gI=s320" width="320" /></a></div><div style="text-align: center;"><i>Bezier Curve on a polygon</i></div><p>There are two ways of specifying the control points needed to define the curve: </p><h4 style="text-align: left;">Alpha (Curvedness) Parameter</h4><p>The easiest way to define the shape of a curve is via the parameter <b>alpha</b>, which indicates the "curvedness". This value is used to automatically generate the control points at each vertex of the baseline. A value of 1 creates a roughly circular curve at right angles. Higher values of alpha make the result more curved; lower values (down to 0) make the curve flatter.</p><p>Alpha is used to determine the length of the control vectors at each vertex. The control vectors on either side of the vertex are collinear and of equal length, which provides C2-continuity. The angle of the control vectors is perpendicular to the bisector of the vertex angle, to make the curve symmetrical. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEh3a5QKTvF2pm6BayvKT6BhkQW-kM7_Xp3p3o-9Jm1mlbWE_cyA33Hzf1by7PEg-zegHsb3Gwro-wvyFlQnecMpKRRDwQp6d70h-0svtL1UydVPPGcIwYzoZjWTOT6q5ot9wfVtJl3FIjZkDDIebrPCsv4xevjuEg2T91zaYs4Ygg4phiAACUwCSjJ4=s387" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="281" data-original-width="387" height="232" src="https://blogger.googleusercontent.com/img/a/AVvXsEh3a5QKTvF2pm6BayvKT6BhkQW-kM7_Xp3p3o-9Jm1mlbWE_cyA33Hzf1by7PEg-zegHsb3Gwro-wvyFlQnecMpKRRDwQp6d70h-0svtL1UydVPPGcIwYzoZjWTOT6q5ot9wfVtJl3FIjZkDDIebrPCsv4xevjuEg2T91zaYs4Ygg4phiAACUwCSjJ4=s320" width="320" /></a></div><div style="text-align: center;"><i>Bezier Curve for alpha = 1</i></div><div style="text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEiX9X-iZcluyzkrEUbYXsk3miRcGiWBMcV6meESQIZ-E-X6KT3QisxZ7E7m5-666O4ZUqYqt6cps1ZuwwdCPXbcNut5vBiV-2IWVHwIkyBheGWy4Rcl2cw-0OdYrIUwphUhVFJNz40xKTQnF4L_PhshE4ZhzrEEzWTgy73ttWvHWU_RBTsHJ9jJ2XcR=s397" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="279" data-original-width="397" height="225" src="https://blogger.googleusercontent.com/img/a/AVvXsEiX9X-iZcluyzkrEUbYXsk3miRcGiWBMcV6meESQIZ-E-X6KT3QisxZ7E7m5-666O4ZUqYqt6cps1ZuwwdCPXbcNut5vBiV-2IWVHwIkyBheGWy4Rcl2cw-0OdYrIUwphUhVFJNz40xKTQnF4L_PhshE4ZhzrEEzWTgy73ttWvHWU_RBTsHJ9jJ2XcR=s320" width="320" /></a></div><i>Bezier Curve for alpha = 1.3</i></div><div style="text-align: center;"><i><br /></i><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjDSJp3quTYuBMNXVSzEiAXxk5iDCHqpw-rRkvuZM40-rSes6QPsJugrUpROUAuSUEh3KbrGYAqRYxaQNQkGupvrA59sMSpCVJOJx27-ORvGPfRQgFzo3KI2SjnmmPuNVaxMJINbW_YPYPp_NM4erANhWMZmKRAl9GT7IJYdr3GpcnBEpJLWR8bTuMa=s381" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="279" data-original-width="381" height="234" src="https://blogger.googleusercontent.com/img/a/AVvXsEjDSJp3quTYuBMNXVSzEiAXxk5iDCHqpw-rRkvuZM40-rSes6QPsJugrUpROUAuSUEh3KbrGYAqRYxaQNQkGupvrA59sMSpCVJOJx27-ORvGPfRQgFzo3KI2SjnmmPuNVaxMJINbW_YPYPp_NM4erANhWMZmKRAl9GT7IJYdr3GpcnBEpJLWR8bTuMa=s320" width="320" /></a></div><i>Bezier Curve for alpha = 0.3</i></div><p><br /></p><h4 style="text-align: left;">Explicit Control Points</h4><p>Alternatively, the Bezier curve control points can be provided explicitly. This gives complete control over the shape of the generated curve. Two control points are required for each line segment of the baseline geometry, in the same order. A convenient way to provide these is as a <span style="font-family: courier;">LineString</span> (or <span style="font-family: courier;">MultiLineString</span> for composite geometries) containing the required number of vertices.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjzUmRb_Kik1yRNg6LJBUZy0GiTCXBvbkozxjQ7TobCmZJ0aVIGAZgOxRoGPvxqXzrf6YqeJI3Cb_znJynvicpKS5dA51z9dremwbgK5_JTDx8KLYcoztdy4paI7b0r6qr0KDMo5em6qkmN9wdQatb_PUEkyUm4mdJA4NMXTipYdwmKGuHZLlPlzOor=s404" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="322" data-original-width="404" height="319" src="https://blogger.googleusercontent.com/img/a/AVvXsEjzUmRb_Kik1yRNg6LJBUZy0GiTCXBvbkozxjQ7TobCmZJ0aVIGAZgOxRoGPvxqXzrf6YqeJI3Cb_znJynvicpKS5dA51z9dremwbgK5_JTDx8KLYcoztdy4paI7b0r6qr0KDMo5em6qkmN9wdQatb_PUEkyUm4mdJA4NMXTipYdwmKGuHZLlPlzOor=w400-h319" width="400" /></a></div><br /><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><i>Bezier Curve defined by control points, with C2 continuity</i></div><p>When using this approach only C0-continuity is provided automatically. The caller must enforce C1 or C2-continuity via suitable positioning of the control points.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEg04ZShXFnXDS1L2v-2YoIBKB4AH0SyCvhQZpJ5VhY6O5hRajzR93DeFIS09vymhOQE-bLPFrR6ZTJGRneiVzB6nF5pU8hKhMGYlPloGQeltLGO72EKeKEp2Y-ZgU9DQV8qfG_ev98qfElGw4zT8-B-mGTNIPNabWQMjzpwEDdgzTYShByO1ror7Q9z=s437" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="274" data-original-width="437" height="201" src="https://blogger.googleusercontent.com/img/a/AVvXsEg04ZShXFnXDS1L2v-2YoIBKB4AH0SyCvhQZpJ5VhY6O5hRajzR93DeFIS09vymhOQE-bLPFrR6ZTJGRneiVzB6nF5pU8hKhMGYlPloGQeltLGO72EKeKEp2Y-ZgU9DQV8qfG_ev98qfElGw4zT8-B-mGTNIPNabWQMjzpwEDdgzTYShByO1ror7Q9z=s320" width="320" /></a></div><p style="text-align: center;"><i>Bezier Curve defined by control points showing C0 and C1 continuity</i></p></div><h3 style="clear: both; text-align: left;">Further Ideas</h3><div class="separator" style="clear: both; text-align: left;"><ul style="text-align: left;"><li>Allow specifying the number of vertices used to approximate each curve</li><li>Add a function to return the constructed control vectors (e.g. for display and analysis purposes)</li><li>Make specifying explicit control points easier by generating C2-continuous control vectors from a single control point at each vertex</li></ul></div><br /></div></div>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com3tag:blogger.com,1999:blog-2420860529344694449.post-84192023717308058572022-01-18T06:53:00.005-08:002022-01-18T06:53:56.789-08:00Concave Hulls in JTS<p>A common spatial need is to find a polygon that accurately represents a set of points. The convex hull of the points often does not provide this, since it can enclose large areas which contain no points. What is required is a <i>non-convex</i> hull, often termed the <b>concave hull.</b> </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhTsfTJAsSKAscvZpIr8bVlodUAyTswzjzdCpat2TswDnV6AYdkYDly3T934T8SHlfaBowVpNwlgyY2jMNCMwc_FTiNrJQFvH2JnXWBClRDeuLqYwb4zZKpCrNugxVQBAwK_E2Q27jLtVOn1O9asYOYja8feDScJVC8nv8vzcJkhQlNsSkD7jYQ0Kg3=s263" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="263" data-original-width="261" src="https://blogger.googleusercontent.com/img/a/AVvXsEhTsfTJAsSKAscvZpIr8bVlodUAyTswzjzdCpat2TswDnV6AYdkYDly3T934T8SHlfaBowVpNwlgyY2jMNCMwc_FTiNrJQFvH2JnXWBClRDeuLqYwb4zZKpCrNugxVQBAwK_E2Q27jLtVOn1O9asYOYja8feDScJVC8nv8vzcJkhQlNsSkD7jYQ0Kg3=s16000" /></a></div><i><div style="text-align: center;"><i>The Convex Hull and a Concave Hull of a point set</i></div></i><p>A concave hull is generally considered to have some or all of the following properties:</p><p></p><ul style="text-align: left;"><li>The hull is a simply connected polygon</li><li>It contains all the input points</li><li>The vertices in the hull polygon boundary are all input points</li><li>The hull may or may not contain holes</li></ul><div>For a typical point set there are many polygons which meet these criteria, with varying degrees of <b>concaveness</b>. Concave Hull algorithms provide a numeric parameter which controls the amount of concaveness in the result. The nature of this parameter is particularly important, since it affects the ease-of-use in practical scenarios. Ideally it has the following characteristics:</div><p></p><ul style="text-align: left;"><li><b>Simple geometric basis</b>: this allows the user to understand the effect of the parameter and aids in determining an effective value</li><li><b>Scale-free</b> (dimensionless): this allows a single parameter value to be effective on varying sizes of geometry, which is essential for batch or automated processing</li><li><b>Local</b> (as opposed to global): A local property (such as edge length) gives the algorithm latitude to determine the concave shape of the points. A global property (such as area) over-constrains the possible result. </li><li><b>Monotonic area: </b> larger (or smaller) values produce a sequence of more concave areas</li><li><b>Monotonic containment</b> :the sequence of hulls produced are topologically nested</li><li><b>Convex-bounded:</b> an extremal value produces the convex hull</li></ul><p>This is a well-studied problem, and many different approaches have been proposed. Some notable ones are:</p><p></p><ul style="text-align: left;"><li><b>Alpha shapes</b> - Edelsbrunner, Kirkpatrick, Seidel (1983): <i><a href="https://www.cs.jhu.edu/~misha/Fall13b/Papers/Edelsbrunner93.pdf">On the shape of a set of points in</a> </i><i><a href="https://www.cs.jhu.edu/~misha/Fall13b/Papers/Edelsbrunner93.pdf">the plane</a></i></li><li><b>KNN</b> - Moreira and Santos (2007):<i> <a href="http://repositorium.sdum.uminho.pt/bitstream/1822/6429/1/ConcaveHull_ACM_MYS.pdf">CONCAVE HULL: A K-NEAREST NEIGHBOURS APPROACH FOR THE COMPUTATION OF THE REGION OCCUPIED BY A SET OF POINTS.</a></i></li><li><b>Delaunay Erosion (Chi-shapes) </b>- Duckham, Kulik, Worboys, Galton (2008) <i><a href="http://www.geosensor.net/papers/duckham08.PR.pdf">Efficient generation of simple polygons for characterizing the shape of a set of points in the plane</a></i></li><li><b>Nearest-Point Digging</b> - Park and Oh (2012) <a href="https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.676.6258&rep=rep1&type=pdf"><i>A New Concave Hull Algorithm and Concaveness Measure for n-dimensional Datasets</i></a></li></ul><p>Of these, <b>Delaunay Erosion</b> (Chi-shapes) offers the best set of features. It is straightforward to code and is performant. It uses the control parameter of <b>Edge Length Ratio, </b>a fraction of the difference between the longest and shortest edges in the underlying Delaunay triangulation. This is easy to reason about, since it is scale-free and corresponds to a simple property of the point set (that of distance between vertices). It can be extended to support holes. And it has a track record of use, notably in <a href="https://docs.oracle.com/database/121/SPATL/sdo_geom-sdo_concavehull_boundary.htm#SPATL1420">Oracle Spatial</a>. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgGSpBpB8ZdEZwTv5EaJ8uKTihuq9argt8TH7eqQXQvzL_9tX4R928Ztng6OL2-YHOuzIsdFPfnL3h4j07Qsl7GTGkKhSmClN7MGHYhcp9cQ4UJtCOgf8OXm2u-c3Re5kMy41rbjI9GRg6x5FF_Ecv-tWwN2Bab7Q8BCPcGYxVstNqy0fKrnVyT9rtk=s261" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="261" data-original-width="230" height="261" src="https://blogger.googleusercontent.com/img/a/AVvXsEgGSpBpB8ZdEZwTv5EaJ8uKTihuq9argt8TH7eqQXQvzL_9tX4R928Ztng6OL2-YHOuzIsdFPfnL3h4j07Qsl7GTGkKhSmClN7MGHYhcp9cQ4UJtCOgf8OXm2u-c3Re5kMy41rbjI9GRg6x5FF_Ecv-tWwN2Bab7Q8BCPcGYxVstNqy0fKrnVyT9rtk" width="230" /></a></div><i style="text-align: center;"><div style="text-align: center;"><i>ConcaveHull generated by Delaunay Erosion with Edge Length Ratio = 0.3</i></div></i><p>Recently the <b>Park-Oh</b> algorithm has become popular, thanks to a high-quality implementation in <a href="https://github.com/mapbox/concaveman">Concaveman</a> project (which has spawned numerous ports). However, it has some drawbacks. It can't support holes (and likely not disconnected regions and discarding outlier points). As the paper points out and experiment confirms, it produces rougher outlines than the Delaunay-based algorithm. Finally, the control parameter for Delaunay Erosion has a simpler geometrical basis which makes it easier to use.</p><p>Given these considerations, the new JTS <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/algorithm/hull/ConcaveHull.java">ConcaveHull</a></span> algorithm utilizes Delaunay Erosion. The algorithm ensures that the computed hull is simply connected, and contains all the input points. The <b>Edge Length Ratio</b> is used as the control parameter. A value of 1 produces the convex hull; 0 produces a concave hull of minimal size. Alternatively the maximum edge length can be specified directly. This allows alternative strategies to determine an appropriate length value; for instance, another possibility is to use a fraction of the longest edge in the Minimum Spanning Tree of the input points. </p><p>The recently-added <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/triangulate/tri">Tri</a></span> data structure provides a convenient basis for the implementation,. It operates as follows:</p><p></p><ol style="text-align: left;"><li>The Delaunay Triangulation of the input points is computed and represented as a set of of Tris</li><li>The Tris on the border of the triangulation are inserted in a priority queue, sorted by longest boundary edge</li><li>While the queue is non-empty, the head Tri is popped from the queue. It is removed from the triangulation if it does not disconnect the area. Insert new border Tris into the queue if they have a boundary edge length than the target length</li><li>The Tris left in the triangulation form the area of the Concave Hull </li></ol><p></p><p>Thanks to the efficiency of the JTS Delaunay Triangulation the implementation is quite performant, approaching the performance of a Java port of Concaveman. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEg8xazkPDuzZyTZfJP7a6KryHVPuQgw_ueL60cQJbw2MsrP-JCYxn-wnQHpJjk_wTPG8mkT1wNHCkFM_-S-TpwW0ICYzO-4-M-031CVNCRI8F4uR1xP1DhdmFkkGkikq00liLEbh5VJ3KN_iUrxUUorainNdkhHQ13M4Z8UcGV-iMzOhrSNr5PjUVP5=s665" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="456" data-original-width="665" height="219" src="https://blogger.googleusercontent.com/img/a/AVvXsEg8xazkPDuzZyTZfJP7a6KryHVPuQgw_ueL60cQJbw2MsrP-JCYxn-wnQHpJjk_wTPG8mkT1wNHCkFM_-S-TpwW0ICYzO-4-M-031CVNCRI8F4uR1xP1DhdmFkkGkikq00liLEbh5VJ3KN_iUrxUUorainNdkhHQ13M4Z8UcGV-iMzOhrSNr5PjUVP5=s320" width="320" /></a></div><i style="text-align: center;"><div style="text-align: center;"><i>Concave Hull of Ukraine dataset; Edge Length Ratio = 0.1</i></div></i><p>Optionally holes can be allowed to be present in the hull polygon (while maintaining a simply connected result). A classic demonstration of this is to generate hulls for text font glyphs:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjCez0yh-w1MpntpcRyXPztMZWDgZNL4-BfXnZ9SAr3s3c9dAK8GZtNQAiZPF9sU75r2IMrVvzQycDxKrbeIoFnLFHQqjcC7u5J5ApemldV770k9kqb1RA4QQQLa3IsfZ16mXkWROuwiqzkYw35Pm-bPupJdlsVIJzIvt7ppgPj-Xx3hRcHYjA6xNVs=s502" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="502" data-original-width="380" height="200" src="https://blogger.googleusercontent.com/img/a/AVvXsEjCez0yh-w1MpntpcRyXPztMZWDgZNL4-BfXnZ9SAr3s3c9dAK8GZtNQAiZPF9sU75r2IMrVvzQycDxKrbeIoFnLFHQqjcC7u5J5ApemldV770k9kqb1RA4QQQLa3IsfZ16mXkWROuwiqzkYw35Pm-bPupJdlsVIJzIvt7ppgPj-Xx3hRcHYjA6xNVs=w151-h200" width="151" /></a><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhgXwGUnFQlSuIuKmOdLFnIy8GADl6LneG3u8y-RgaPRHsHxpvpZ3zQp3sbMB9hbb1JfybIjFkZpKUXUBkNzhNYwZV93ozpR2t-aCr90P5IQFWKQdQ_ho0bvYNUxDbddgxkejuEjnSFeCFfUdgxJLT0ZHmT-AYm9oIsFPRqXl0jVzavSG2L6EcNTq_F=s450" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="450" data-original-width="305" height="200" src="https://blogger.googleusercontent.com/img/a/AVvXsEhgXwGUnFQlSuIuKmOdLFnIy8GADl6LneG3u8y-RgaPRHsHxpvpZ3zQp3sbMB9hbb1JfybIjFkZpKUXUBkNzhNYwZV93ozpR2t-aCr90P5IQFWKQdQ_ho0bvYNUxDbddgxkejuEjnSFeCFfUdgxJLT0ZHmT-AYm9oIsFPRqXl0jVzavSG2L6EcNTq_F=w136-h200" width="136" /></a></div><div class="separator" style="clear: both; text-align: left;"><br /></div><div class="separator" style="clear: both; text-align: left;">This algorithm is in the process of being ported to <a href="http://libgeos.org/">GEOS</a>. The intention is to use it to enhance the PostGIS <span style="font-family: courier;"><a href="https://postgis.net/docs/ST_ConcaveHull.html">ST_ConcaveHull</a></span> function, which has known issues and has proven difficult to use.</div><h3 style="text-align: left;">Further Ideas</h3><p></p><ul style="text-align: left;"><li><b>Disconnected Result</b> - It is straightforward to extend the algorithm to allow a disconnected result (i.e. a MultiPolygon). This could be provided as an option.</li><li><b>Outlier Points</b> - It is also straightforward to support discarding "outlier" points.</li><li><b>Polygon Concave Hull</b> - computing a concave "outer hull" for a polygon can be used to simplify the polygon while guaranteeing the hull contains the original polygon. Additionally, an "inner hull" can be computed which is fully contained in the original. The implementation of a Polygon Concave Hull algorithm is well under way and will be released in JTS soon. </li></ul><p></p><p><br /></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com6tag:blogger.com,1999:blog-2420860529344694449.post-55426893304488989762022-01-03T20:33:00.002-08:002022-01-03T20:34:54.886-08:00JTS Offset Curves<p>Offset curves (also known as <a href="https://en.wikipedia.org/wiki/Parallel_curve">parallel curves</a>) are an oft-requested feature in JTS. They are a natural extension to the concept of buffering, and are useful for things like placing labels along rivers. As far as I know there is no hard-and-fast definition for how an offset curve should be constructed, but a reasonable semantic would seem to be <i>"a line lying on one side of another line at a given offset distance"</i>. </p><p>Here's an image of offset curves on both sides of a river reach from a <a href="https://www.weather.gov/gis/Rivers">US rivers dataset</a>:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhaaAF2JJHmiMO16CHgcfsEE9OaVKH_XQV73dgh5at96fbpuSyd7On5iqfk-rttloUx4Fh3r9LTCkYCMBVapM8mh4XYdV6_b8ypQIzwwghWTJuhKqgWcqXPW8-6kxN4DAX6c8duzFy7oyEp1oQLObmYxgiUnM9OyLnj-axJ6NJdCkw39vEWfY6Bw9Co=s391" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="150" data-original-width="391" height="154" src="https://blogger.googleusercontent.com/img/a/AVvXsEhaaAF2JJHmiMO16CHgcfsEE9OaVKH_XQV73dgh5at96fbpuSyd7On5iqfk-rttloUx4Fh3r9LTCkYCMBVapM8mh4XYdV6_b8ypQIzwwghWTJuhKqgWcqXPW8-6kxN4DAX6c8duzFy7oyEp1oQLObmYxgiUnM9OyLnj-axJ6NJdCkw39vEWfY6Bw9Co=w400-h154" width="400" /></a></div><h3 style="text-align: left;">GEOS Implementation</h3><p><a href="https://github.com/libgeos/geos">GEOS</a> acquired an <a href="https://github.com/libgeos/geos/blob/main/src/operation/buffer/BufferBuilder.cpp#L131">implementation</a> to produce offset curves a few years ago. However, it has some known bugs (such as <a href="https://github.com/libgeos/geos/issues/477">producing disconnected curves</a>, and <a href="https://github.com/libgeos/geos/issues/507">including extraneous segments</a>). It also has the feature (or quirk?) of potentially producing a result with multiple disjoint curves for a single input line. </p><p>The GEOS implementation is based on the concept that an offset curve is just a portion of the corresponding buffer boundary. So to generate an offset curve the algorithm extracts a portion of the buffer polygon boundary. The trick is deciding which portion! </p><p>The GEOS implementation generates a <b>raw offset curve</b> (potentially full of self-intersections and unwanted linework) and then determines the <b>intersection</b> of that curve with the buffer boundary. However, the use of intersection on linework is always tricky. The buffer geometry is necessarily only a (close) approximation, and the buffer algorithm takes advantage of this to use various heuristics to improve the quality and robustness of the generated buffer linework. This can cause the buffer linework to diverge from the raw offset curve. The divergence makes the intersection result susceptible to errors caused by slight differences between the generated curves. The two issues above are caused by this limitation. </p><h3 style="text-align: left;">JTS Implementation</h3><p>Instead of using intersection, an effective technique is to match geometry linework using a <b>distance tolerance</b>. This is the approach taken in the new<b> <a href="https://github.com/locationtech/jts/pull/810">JTS Offset Curve algorithm</a></b>. The high-level design is</p><p></p><ol style="text-align: left;"><li>The buffer is generated at the offset distance</li><li>The raw offset curve is generated at the same distance</li><li>The raw curve segments are matched to the buffer boundary using a distance tolerance</li><li>The offset curve is the section of the buffer boundary between the first and last matching points.</li></ol><div>To make the matching efficient a spatial index is created on the buffer curve segments. </div><div><br /></div><div>This algorithm provides the following semantics:</div><div><ul style="text-align: left;"><li>The offset curve of a line is a <b>single</b> LineString</li><li>The offset curve lies on <b>one side</b> of the input line (to the left if the offset distance is positive, and to the right if negative)</li><li>The offset curve has the <b>same direction</b> as the input line</li><li>The <b>distance between the input line and the offset curve</b> equals the offset distance (up to the limits of curve quantization and numerical precision)</li></ul></div><div>This algorithm does a fine job at generating offset curves for typical simple linework. The image below shows a family of offset curves on both sides of a convoluted line. </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEj1QuJJhLLUwkUExwEHozCpl9es7iiwMMzikLPdNjWLu-KFrU7urVvHc-9c1xu7BVERP_u1OTfAo4cnzomueAyWSU05VGyWml8QvkF26QEaz7KFHEn6UMZa69QLvgxJLRIu-9YooYfRRJrgM36AAGJqM2b_rrRvz1QwNPPmFCRS2OByu1ROUh9RD5P0=s452" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="349" data-original-width="452" height="309" src="https://blogger.googleusercontent.com/img/a/AVvXsEj1QuJJhLLUwkUExwEHozCpl9es7iiwMMzikLPdNjWLu-KFrU7urVvHc-9c1xu7BVERP_u1OTfAo4cnzomueAyWSU05VGyWml8QvkF26QEaz7KFHEn6UMZa69QLvgxJLRIu-9YooYfRRJrgM36AAGJqM2b_rrRvz1QwNPPmFCRS2OByu1ROUh9RD5P0=w400-h309" width="400" /></a></div><div>This resolves both of the GEOS code issues. It also supports parameters for <b>join style</b> (round, bevel, and mitre), <b>number of quadrant segments</b> (for round joins) and <b>mitre limit</b>:</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhAkgLKFZQ983X7DRTlN1NwZR6bsemVgMDm7bJCu0Hf1aha_CQeAE7sDJBmeUuhgyTb1b37tyjkBy2feyEjf5DiJ910R_t_N_oHcEpS085PvUX-5149tKu459yZGj3C5yh9KEe_8gGYr0OklOi1kB74t3xKtwkLtF0thWS3YJCkSPymaIreiVSBHg8N=s246" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="246" data-original-width="234" height="320" src="https://blogger.googleusercontent.com/img/a/AVvXsEhAkgLKFZQ983X7DRTlN1NwZR6bsemVgMDm7bJCu0Hf1aha_CQeAE7sDJBmeUuhgyTb1b37tyjkBy2feyEjf5DiJ910R_t_N_oHcEpS085PvUX-5149tKu459yZGj3C5yh9KEe_8gGYr0OklOi1kB74t3xKtwkLtF0thWS3YJCkSPymaIreiVSBHg8N=w304-h320" width="304" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Join Style = MITRE</i><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgfKKf7kN4YHuoxP4NqB0e7hegKpQ3wDmtBay8wO9E-0nDZCaiaP-0t4BvTFiFk1fgg2bXgNljS69ZL3hH-a0HqBpmgVDHYQ0y8KY2oTVkc15btJI80rSwPqyoqMqdim8ghunDFC4iSQUS7xp9-1Uuq_yakAHNSklfRnPKYu4I4p_ICsB602N1ux-eE=s326" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="263" data-original-width="326" height="258" src="https://blogger.googleusercontent.com/img/a/AVvXsEgfKKf7kN4YHuoxP4NqB0e7hegKpQ3wDmtBay8wO9E-0nDZCaiaP-0t4BvTFiFk1fgg2bXgNljS69ZL3hH-a0HqBpmgVDHYQ0y8KY2oTVkc15btJI80rSwPqyoqMqdim8ghunDFC4iSQUS7xp9-1Uuq_yakAHNSklfRnPKYu4I4p_ICsB602N1ux-eE=s320" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Join Style = ROUND; Quadrant Segments = 2</i></div></div><div><br /></div><div>There are also a few nuances required to handle some tricky situations; in particular, cases when the input line string curves back on itself and when it self-intersects. These do not have a clear definition for what the result should be. Moreover, the algorithm itself imposes some constraints on how these cases can be handled. The images below show how the algorithm behaves in these cases.</div><div><br /></div><div>"Loopbacks"produce offset curves representing only the "exposed" sections of the input linework: </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEjPNDyd5wVgkZ1szi0NBDAuP1OfE5VDSfFovQlw2nMxf100qJ9P55v1WjlXjq8U_tSOqh08RpfM5md0WinDOmfF_kPjanPF44yuXBQC98FdwhUN7_gFrWq2_9QGml649tr34oBRLbQuBQdF8vdAcmH5psv7UPjYsexA5L8sGCUb1o50a1eSMm7PclA0=s472" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="263" data-original-width="472" height="223" src="https://blogger.googleusercontent.com/img/a/AVvXsEjPNDyd5wVgkZ1szi0NBDAuP1OfE5VDSfFovQlw2nMxf100qJ9P55v1WjlXjq8U_tSOqh08RpfM5md0WinDOmfF_kPjanPF44yuXBQC98FdwhUN7_gFrWq2_9QGml649tr34oBRLbQuBQdF8vdAcmH5psv7UPjYsexA5L8sGCUb1o50a1eSMm7PclA0=w400-h223" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Offset Curves of a Line with "loop-backs"</i></div><br /><div class="separator" style="clear: both; text-align: left;">For a self-intersecting input line, the offset curve starts at the beginning of the input line on the specified side, and continues only up to where the line side changes due to the crossing. The length of the offset curve is also reduced by the requirement that it be no closer than specified offset distance to the input line: </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEgCjb3CpYUFYUcKFiKko9YIIf2HSy_EzFmY7vmxHPSlazXhx8tSC2MIKvO5MlvGMG9qoZvgEkMI_paRtHXR13SGtCMjsf8YacZbPoMAD25AahyLd0V_ajNkEgFiXGBvANqMjybkJeh0tn9B6q5mYwNGH2LLSeYhmWjL3Bi1rNdU5VamJel6ALWvaL4Q=s550" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="412" data-original-width="550" height="300" src="https://blogger.googleusercontent.com/img/a/AVvXsEgCjb3CpYUFYUcKFiKko9YIIf2HSy_EzFmY7vmxHPSlazXhx8tSC2MIKvO5MlvGMG9qoZvgEkMI_paRtHXR13SGtCMjsf8YacZbPoMAD25AahyLd0V_ajNkEgFiXGBvANqMjybkJeh0tn9B6q5mYwNGH2LLSeYhmWjL3Bi1rNdU5VamJel6ALWvaL4Q=w400-h300" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Offset Curves of a Line with a self-intersection</i></div><br /><div>This algorithm is now in JTS, and has been ported to GEOS.</div><div><br /></div><div><br /></div><p></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-38790179789093303932021-12-22T16:08:00.003-08:002021-12-22T16:08:29.244-08:00Christmas Wrapping<p> Every so often I produce an image in the JTS TestBuilder which strikes me as worthy of capture. Here's one that seems pretty seasonal:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEiNzyFQkyhgyWrxPfR97frESMwCEdTFmaG3kyhDp6ai5g4jxI5djtP8wIXBv4W_2KKYSTklzD91-dandHsg-HobXV0bdrg57fbdQFcFENLmNWjnfcWk1oAcWw7VpC2M3bwzUOO3k5LzvWi9mWyLvSvjktq3VBM-6HbEqsCy3D3c8ZHH1jOkWZFj3SN7=s540" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="535" data-original-width="540" height="396" src="https://blogger.googleusercontent.com/img/a/AVvXsEiNzyFQkyhgyWrxPfR97frESMwCEdTFmaG3kyhDp6ai5g4jxI5djtP8wIXBv4W_2KKYSTklzD91-dandHsg-HobXV0bdrg57fbdQFcFENLmNWjnfcWk1oAcWw7VpC2M3bwzUOO3k5LzvWi9mWyLvSvjktq3VBM-6HbEqsCy3D3c8ZHH1jOkWZFj3SN7=w400-h396" width="400" /></a></div><div class="separator" style="clear: both; text-align: justify;">It is generated like this:</div><div class="separator" style="clear: both; text-align: justify;"><ol><li>Produce two sets of 1000 random points roughly aligned with a grid</li><li>Compute their fully-eroded Convex Hulls</li><li>Compute the intersection of the two hulls</li><li>Theme the intersection with random fill</li></ol></div><div class="separator" style="clear: both; text-align: center;"><br /></div><br /><p><br /></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-36005841183538160612021-11-01T12:54:00.002-07:002021-11-01T12:54:54.120-07:00JTS Polygon Triangulation, at last<p>A (long) while ago I <a href="https://lin-ear-th-inking.blogspot.com/2011/04/polygon-triangulation-via-ear-clipping.html">posted</a> about "soon-to-be-released" JTS code for polygon triangulation using <a href="https://en.wikipedia.org/wiki/Polygon_triangulation#Ear_clipping_method">Ear Clipping</a>. It turned out it was actually in the category of "never-to-be-released". However, later I worked with a student, Dan Tong, on a coding exercise sponsored by Facebook. We decided to tackle polygon triangulation. By the end of the project he implemented a functional algorithm, including handling holes (via the hole-joining technique described by <a href="http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf">Eberly</a>). To make it release-ready the code needed performance and structural improvements. This has finally happened and the code is out! </p><h3 style="text-align: left;">Polygon Triangulation</h3><p>The new <a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/triangulate/polygon">codebase</a> has been substantially rewritten to improve modularity and the API. A new data structure to represent triangulations has been introduced, called a <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java">Tri</a></span> It is simpler, more memory efficient, and easier to work with than the <span style="font-family: courier;">QuadEdge</span> data structure previously used in JTS. It provides an excellent basis for developing further algorithms based on triangulations (of which more below)</p><p>Most of the reworking involved implementing key performance optimizations, utilizing existing and new JTS spatial index structures: </p><ul style="text-align: left;"><li>the <b>Hole Joining</b> phase now uses a spatial index to optimize the test for whether a join line is internal to a polygon</li><li>the <b>Ear-Clipping</b> phase has to check whether a candidate ear contains any vertices of the polygon shell being clipped. This has been made performant by using a new spatial index structure called the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/triangulate/polygon/VertexSequencePackedRtree.java">VertexSequencePackedRtree</a></span></li></ul><p></p><p>Performance must be benchmarked against an existing implementation. Geometry <i>maestro</i> Vladimir Agafonkin has developed an Ear Clipping algorithm in Javascript called <b><a href="https://github.com/mapbox/earcut">Earcut</a></b>. It's claimed to be the fastest implementation in Javascript. Indeed the <a href="https://github.com/mapbox/earcut#why-another-triangulation-library">performance comparison</a> shows Earcut to be over 4 times faster than some other Javascript triangulation implementations. Earcut has been ported to Java as <a href="https://github.com/earcut4j/earcut4j"><b>earcut4j</b></a>, which allows comparing JTS to it. </p><p>
</p><table border="1" cellpadding="4">
<tbody><tr><th>Dataset</th><th>Size</th><th>JTS Time</th><th>Earcut4j Time</th></tr>
<tr><td>Lake Superior</td><td>3,478 vertices / 28 holes</td><td>18 ms</td><td>13 ms</td></tr>
<tr><td>Canada</td><td>10,522 vertices</td><td>35 ms</td><td>26 ms</td></tr>
<tr><td>Complex polygon</td><td>44,051 vertices / 125 holes</td><td>1.1 s</td><td>0.64 s</td></tr>
<tr><td>North America with Lakes</td><td>115,206 vertices / 1,660 holes</td><td>13.3 s</td><td>5.4 s</td></tr>
</tbody></table>
<p></p><h3 style="text-align: left;"><span style="font-size: medium; font-weight: 400;">So JTS ear clipping is not quite as fast as Earcut4j. But it's still plenty fast enough for production use!</span></h3><div><p>The triangulation algorithm is exposed as the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/triangulate/polygon/PolygonTriangulator.java">PolygonTriangulator</a></span> class (deliberately named after the effect, rather than the algorithm producing it). Here's an example of the output:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCPgm3x9y0NFss6qn0-KV0yFmiI0J5EUF4zwn4vY34xuD51H2bgcWlc_w4QpPPacP7hKwsBd1CD8WrMxb2z7xP7EzWOfqQmf2s2MoGVFDYsXCS7-RogesSo9KIYTRSdSaP3ZujSwujokc/s448/superior-tri.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="239" data-original-width="448" height="214" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiCPgm3x9y0NFss6qn0-KV0yFmiI0J5EUF4zwn4vY34xuD51H2bgcWlc_w4QpPPacP7hKwsBd1CD8WrMxb2z7xP7EzWOfqQmf2s2MoGVFDYsXCS7-RogesSo9KIYTRSdSaP3ZujSwujokc/w400-h214/superior-tri.png" width="400" /></a></div><i><div style="text-align: center;"><i>Ear-Clipping Triangulation of Lake Superior</i></div></i></div><h3 style="text-align: left;">Constrained Delaunay Triangulation</h3><p>The output of <span style="font-family: courier;">PolygonTriangulator</span> is guaranteed to be a <b>valid</b> triangulation, but in the interests of performance it does not attempt to produce a <b>high-quality</b> one. Triangulation quality is measured in terms of minimizing the number of "skinny" triangles (or equivalently, maximizing the sum of the interior angles). Optimal quality is provided by <b><a href="https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation">Constrained Delaunay Triangulation</a></b> (CDT). In addition to being optimal in terms of triangle shape, the CDT has the nice properties of being (essentially) unique, and of accurately representing the structure of the input polygon. It also removes the influence that Hole Joining holes has on the raw triangulation provided by Ear-Clipping.</p><p>I originally proposed that a CDT can be achieved by a <b>Delaunay Improvement</b> algorithm based on iterated triangle flipping. It turned out that this technique is not only effective, but also pleasingly performant. This is implemented in the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/triangulate/polygon/ConstrainedDelaunayTriangulator.java">ConstrainedDelaunayTriangulator</a></span> class. Running it on the example above shows the improvement obtained. Note how there are fewer narrow triangles, and their arrangement more closely represents the structure of the polygon.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-Pg5KBhd1fEqyeMer_92GHebJfBJy8pyWQjSW0gfS0zxQ0GdOezUKoYXDqN4tt2uiYxzS8xwW_R78T_sXLCJdtwaYLPJNGGlpigzfAdUg2SCHkVGRU8Oes4E1EtiIW3s44IQ9GGvO1yM/s575/superior-cdt.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="304" data-original-width="575" height="211" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi-Pg5KBhd1fEqyeMer_92GHebJfBJy8pyWQjSW0gfS0zxQ0GdOezUKoYXDqN4tt2uiYxzS8xwW_R78T_sXLCJdtwaYLPJNGGlpigzfAdUg2SCHkVGRU8Oes4E1EtiIW3s44IQ9GGvO1yM/w400-h211/superior-cdt.png" width="400" /></a></div><i><div style="text-align: center;"><i>Constrained Delaunay Triangulation of Lake Superior</i></div></i><p>This code will appear in the next JTS version (1.18.3 or 1.19), and has already been <a href="https://git.osgeo.org/gitea/geos/geos/src/branch/main/src/triangulate/polygon">released</a> in GEOS 3.10.</p><h3 style="text-align: left;">Future Work</h3><p>It seems possible to adapt the machinery for Ear Clipping to provide two other useful constructions:</p><p></p><ul style="text-align: left;"><li><b>Polygon Concave Hull</b> ("Outer Hull") - this requires a triangulation of the area between the polygon and its convex hull. That can be obtained by running Ear Clipping <i>outwards</i>, rather than inwards. A Concave Hull can be constructed by limiting the number of triangles generated based on some criteria such as area, longest edge, or number of vertices. The result is a simplified polygon with fewer vertices which is guaranteed to contain the original polygon. </li><li><b>Polygon Inner Hull</b> - the triangulation constructed by Ear Clipping can be "eroded" to provide a simpler polygon which is guaranteed to be contained in the original.</li></ul><div>The property of the Constrained Delaunay Triangulation of representing the structure of the input polygon could provide a basis for computing the following constructions: </div><ul style="text-align: left;"><li><b>Approximate Skeleton</b> construction (also known as the Medial Axis) by creating edges through the CDT triangles</li><li><b>Equal-Area Polygon Subdivision</b>, by partitioning the graph induced by the CDT</li></ul><div>The <span style="font-family: courier;"><b>Tri</b></span> data structure provides a simple representation of a triangulation. I expect that it will facilitate the development of further JTS triangulation algorithms, such as:</div><div><ul><li><b>Concave Hull of Point Set</b></li><li><b>Constrained Delaunay Triangulation of Lines and Points</b></li></ul></div><p></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com4tag:blogger.com,1999:blog-2420860529344694449.post-90284694719771859162021-10-09T20:27:00.002-07:002021-10-09T20:27:36.866-07:00Query KD-trees 100x faster with this one weird trick!<p>Recently a GEOS <a href="https://github.com/libgeos/geos/pull/481">patch</a> was contributed to change the <span style="font-family: courier;"><a href="https://git.osgeo.org/gitea/geos/geos/src/branch/main/src/index/kdtree/KdTree.cpp">KdTree</a></span> query implementation to use an explicit stack rather than recursion. This has been ported to JTS as <a href="https://github.com/locationtech/jts/pull/779">PR #779</a> (along with some refactoring).</p><p>The change was motivated by a <a href="https://github.com/qgis/QGIS/issues/45226">QGIS issue</a> in which a union of some large polygons caused a stack overflow during a <span style="font-family: courier;">KdTree</span> query. The reason is that the poor vertex alignment of the input polygons causes the union overlay process to invoke the <span style="font-family: courier;">SnappingNoder</span>. (For background about why this occurs see the post <a href="http://lin-ear-th-inking.blogspot.com/2020/06/jts-overlayng-noding-strategies.html">OverlayNG - Noding Strategies</a>). The snapping noder snaps vertices using a <span style="font-family: courier;">KdTree</span> with a tolerance. The polygon boundaries contain runs of coherent (nearly-monotonic) vertices (which is typical for high-resolution polygons delineating natural areas). When these are loaded directly into a <span style="font-family: courier;">KdTree</span> the tree can become <a href="https://en.wikipedia.org/wiki/K-d_tree#Adding_elements">unbalanced</a>. </p><p>An unbalanced tree has a relatively large depth for its size. The diagrams below shows the graph of the KdTree for a polygon containing 10,552 vertices. The unbalanced nature of the tree is evident from the few subtrees extending much deeper than the rest of the tree. The tree depth is 282, but most of that occurs in a single subtree.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgq35p2TTqTj_AhfCDJzzF-J9Vto_k1IiYdfZ7yBJlIqY66L1NtF91sN6P4PbH9r9xvsC4VRQSaV6ZHoMeT9PVufMMxSfAbcXK1kM8hvacBomQEh1Z0shVOSAY0JYXkJuywcGmMpOjnLhc/s337/kdtree-unbalanced-full.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="337" data-original-width="113" height="200" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgq35p2TTqTj_AhfCDJzzF-J9Vto_k1IiYdfZ7yBJlIqY66L1NtF91sN6P4PbH9r9xvsC4VRQSaV6ZHoMeT9PVufMMxSfAbcXK1kM8hvacBomQEh1Z0shVOSAY0JYXkJuywcGmMpOjnLhc/w67-h200/kdtree-unbalanced-full.png" width="67" /></a><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBS2GC3A-X0bRgUNL9etZHFodokvDqSUq9ZSi9A_8Ywmk1vZNY6n5O5jnN9HycZ96sedhE_r8buxPlbfgtDVx1vyZ-1o7TnWbmT_X50wLkNWEjCGwK2QSd50HydFHHyW38iuRYAGVOA48/s393/kdtree-unbalanced.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="254" data-original-width="393" height="207" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBS2GC3A-X0bRgUNL9etZHFodokvDqSUq9ZSi9A_8Ywmk1vZNY6n5O5jnN9HycZ96sedhE_r8buxPlbfgtDVx1vyZ-1o7TnWbmT_X50wLkNWEjCGwK2QSd50HydFHHyW38iuRYAGVOA48/w320-h207/kdtree-unbalanced.png" width="320" /></a></div><p style="text-align: center;"><i>An unbalanced KD-tree (size = 10,552, depth = 282)</i></p><p style="text-align: center;"><i>Left: full graph. Right: close-up of graph structure</i></p><p>For large inputs querying such a deep subtree causes the recursive tree traversal to exceed the available call stack. Switching to an iterative implementation eliminates the stack overflow error, since the memory-based stack can grow to whatever size is needed.</p><p>However, stack overflow is not the only problem! Unbalanced KD-trees also exhibit poor query performance. This is because deep tree traversals make the search runtime more linear than logarithmic. Increasing the size of the query stack increases robustness, but does not improve the performance problem. The best solution is to build the KD-tree in a balanced way in the first place. </p><p>One way to do this is to to break up monotonic runs by randomizing the order of vertex insertion. This is actually implemented in the OverlayNG <span style="font-family: courier;">SnapRoundingNoder</span>, as discussed in the post <i><a href="http://lin-ear-th-inking.blogspot.com/2020/12/randomization-to-rescue.html">Randomization to the Rescue</a></i>. However, using this approach in the <span style="font-family: courier;">SnappingNoder</span> would effectively query the tree twice for each vertex. And in fact it's not necessary to randomize <i>all</i> the inserted points. A better-balanced tree can be produced by "seeding" it with a small set of well-chosen points. </p><p>But how can the points be chosen? They could be selected randomly, but true randomness can be quite "clumpy". Also, a non-deterministic algorithm is undesirable for repeatability. Both of these issues can be avoided by using a <b><a href="https://en.wikipedia.org/wiki/Low-discrepancy_sequence">low-discrepancy quasi-random sequence</a></b>. This concept has a fascinating theory, with many approaches available. The simplest to implement is an <b><a href="https://en.wikipedia.org/wiki/Low-discrepancy_sequence#Additive_recurrence">additive recurrence sequence</a></b> with a constant α (the notation <i>{...}</i> means "fractional value of"):</p><p style="text-align: center;"><span style="font-family: trebuchet; font-size: medium;">R(α) : t<sub>n</sub> = { t<sub>0</sub> + n * α }, n = 1,2,3,... <i> </i></span></p><p>If α is irrational the sequence never repeats (within the limits of finite-precision floating point). The excellent article <a href="http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/"><i>The Unreasonable Effectiveness of Quasirandom Sequences</i></a> suggests that the lowest possible discrepancy occurs when α is the reciprocal of the <a href="https://en.wikipedia.org/wiki/Golden_ratio">Golden Ratio</a> <i>φ. </i></p><p style="text-align: center;"><span style="font-family: trebuchet; font-size: medium;">1 / <i>φ</i> = 0.6180339887498949...</span></p><p>Implementing this is straightforward, and it is remarkably effective. For the example above, the resulting KD-tree graph is significantly better-balanced, with lower depth (85 versus 282):</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5YKxebnR9wL9D0MZ4CU6RXbYItD3VYPn8Kcx-vjuCTsEHtjBPycoeUX7ZgyoE8fXhE7rgSflc95OGEtuZt4OmPPOZFCsFdo4By6OzOVs9K3mLykqokOT2sutoj-7i8uW0LFyQmuTi-4Y/s499/kdtree-balanced.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="499" data-original-width="382" height="400" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5YKxebnR9wL9D0MZ4CU6RXbYItD3VYPn8Kcx-vjuCTsEHtjBPycoeUX7ZgyoE8fXhE7rgSflc95OGEtuZt4OmPPOZFCsFdo4By6OzOVs9K3mLykqokOT2sutoj-7i8uW0LFyQmuTi-4Y/w306-h400/kdtree-balanced.png" width="306" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Better-balanced KD-tree (</i><i>size = 10,552, </i><i>depth = 85)</i></div><p>Adding KD-tree seeding to the <span style="font-family: courier;">SnappingNoder</span> gives a <b>huge performance boost</b> to the QGIS test case. For the 526,466 input vertices the <b>tree depth is reduced from 17,776 to 183</b> and the <b>snapping time is improved by about 100 times, from ~80 s to ~800 ms</b>! (In this case the overall time for the union operation is reduced by only about 50%, since the remainder of the overlay process is time-consuming due to the large number of holes in the output.)</p><p>In GEOS the performance difference is smaller, but still quite significant. The <b>snapping time improves 14x, from 12.2 s to 0.82 s</b>, with the union operation time decreasing by 40%, from 31 to 18 secs.</p><p>This improvement will be merged into JTS and GEOS soon.</p><p><br /></p><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com1tag:blogger.com,1999:blog-2420860529344694449.post-25676688224224241432021-07-16T16:14:00.000-07:002021-07-16T16:14:01.338-07:00JTS IsValidOp Built Back Better<p>In a previous <a href="http://lin-ear-th-inking.blogspot.com/2021/05/jts-issimple-gets-simpler-and-faster.html">post</a> I described how the <a href="https://github.com/locationtech/jts">JTS Topology Suite</a> operation <span style="font-family: courier;">IsSimpleOp</span> has been completely rewritten to reduce code dependencies, improve performance, and provide a simpler, more understandable implementation. The post points out that the <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/operation/valid/IsValidOp.html">IsValidOp</a></span> implementation would benefit from the same treatment. This work has now been carried out, with similar benefits achieved. </p><p>The original <span style="font-family: courier;">IsValidOp</span> code used the <span style="font-family: courier;">GeometryGraph</span> framework to represent the topology of geometries. This code reuse reduced development effort, but it carried some serious drawbacks:</p><p></p><ul style="text-align: left;"><li>The <span style="font-family: courier;">GeometryGraph</span> structure was used for many JTS operations, including overlay, buffer, and spatial predicate evaluation. This made the code complex, hard to understand, and difficult to maintain and enhance</li><li>The <span style="font-family: courier;">GeometryGraph</span> structure computes the full topology graph of the input geometry, in order to support constructive operations such as overlay and buffer. This makes it slower and more subject to robustness problems. Non-constructive operations such as <span style="font-family: courier;">IsValidOp</span> and <span style="font-family: courier;">IsSimpleOp</span> can compute topology "on-the-fly", which allows short-circuiting processing as soon as an invalidity is found. </li></ul><div>The new <span style="font-family: courier;">IsValidOp</span> <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/valid/IsValidOp.java">implementation</a> is more self-contained, relying only on the <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/noding/package-summary.html">noding</a></span> framework and some basic spatial predicates. Using the <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/noding/MCIndexNoder.html">MCIndexNoder</a></span> allows short-circuited detection of some kinds of invalidity, which improves performance. And it will be easy to switch to a different noding strategy should a superior one arise.</div><div>However, dropping <span style="font-family: courier;">GeometryGraph</span> means that the topology information required to confirm validity needs to be computed some other way. The goal is to compute just enough topological structure to evaluate validity, and do that efficiently on an as-needed basis. This required some deep thinking about validity to pare the topology information required down to a minimum. This was assisted by the <a href="https://github.com/locationtech/jts/blob/master/modules/tests/src/test/resources/testxml/general/TestValid.xml">extensive set of unit tests for IsValidOp</a>, which ensured that all possible situations were handled by the new logic (although there was an untested situation which uncovered a <a href="https://github.com/locationtech/jts/pull/748">bug</a> late in development.) The resulting algorithm uses some elegant logic and data structures, which are explained in detail below. </div><h3 style="text-align: left;">OGC Validity</h3><div>JTS implements the <a href="https://www.ogc.org/standards/sfs">OGC Simple Features</a> geometry model. To understand the validity algorithm, it helps to review the rules of geometry validity in the OGC specification:</div><div>For non-polygonal geometry types, the validity rules are simple;</div><div><ol style="text-align: left;"><li>Coordinates must contain valid numbers</li><li>LineStrings must not be zero-length</li><li>LinearRings must be simple (have no self-intersections)</li></ol></div><div>For polygonal geometry, on the other hand, the validity rules are quite stringent (sometimes considered too much so!). The rules are:</div><div><ol style="text-align: left;"><li>Rings must be simple (have no self-intersections)</li><li>Rings must not cross, and can touch only at discrete points</li><li>Holes must lie inside their shell</li><li>Holes must not lie inside another hole of their polygon</li><li>The interiors of polygons must be connected</li><li>In a MultiPolygon, the element interiors must not intersect (so polygons cannot overlap)</li><li>In a MultiPolygon, the element boundaries may intersect only at a finite number of points</li></ol></div><h3 style="text-align: left;">Validity Algorithm for Polygonal Geometry</h3><div>The new <span style="font-family: courier;">IsValidOp</span> uses an entirely new algorithm to validate polygonal geometry. It has the following improvements:</div><div><ul style="text-align: left;"><li>It supports short-circuiting when an invalid situation is found, to improve performance</li><li>Spatial indexing is used in more places</li><li>The code is simpler and more modular. This makes it easier to understand, and easier to adapt to new requirements (e.g. such as the ability to validate self-touching rings described below)</li></ul></div><div>The algorithm consists of a sequence of checks for different kinds of invalid situations. The order of the checks is important, since the logic for some checks depends on previous checks passing. </div><p></p><h4 style="text-align: left;">1. Check Ring Intersections</h4><p>The first check determines if the rings in the geometry have any invalid intersections. This includes the cases of a ring crossing itself or another ring, a ring intersecting itself or another ring in a line segment (a collinear intersection), and a ring self-touching (which is invalid in the OGC polygon model). This check can use the robust and performant machinery in the JTS <span style="font-family: courier;">noding</span> package. If an invalid intersection is found, the algorithm can return that result immediately.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrK8g0zyct6pEXDGpZ1kC5Zn0QQ2DKRK92x3dhp3Y3JDk1tTozfJ9SySfq5KAMz166Wj-Y6mt7ZFN3QzFAl6bfbFbJf5XRLaFz_wiivuv_-D4SmFTzNC7NyVsCDsYPgrQtSCrgsb13ZDs/s480/invalid-intersections.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="364" data-original-width="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgrK8g0zyct6pEXDGpZ1kC5Zn0QQ2DKRK92x3dhp3Y3JDk1tTozfJ9SySfq5KAMz166Wj-Y6mt7ZFN3QzFAl6bfbFbJf5XRLaFz_wiivuv_-D4SmFTzNC7NyVsCDsYPgrQtSCrgsb13ZDs/s320/invalid-intersections.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Kinds of invalid intersections: </i></div><div class="separator" style="clear: both; text-align: center;"><i>(i) a self-touch; (ii) collinear; (iii) segment crossing; (iv) vertex crossing</i></div><p>This check is an essential precursor to all the remaining checks. If a geometry has no invalid intersections this confirms that its rings do not self-cross or partially overlap. This means that the results of orientation and point-in-polygon computations are reliable. It also indicates that the rings are properly nested (although it remains to be determined if the nesting of shells and holes is valid).</p><p>Intersection search is the most computationally expensive phase of the algorithm. Because of this, information about the locations where rings touch is saved for use in the final phase of checking connected interiors. </p><h4 style="text-align: left;">2. Check Shell and Hole nesting</h4><div>If no invalid intersections are found, then the rings are properly nested. Now it is necessary to verify that shells and holes are correctly positioned relative to each other:</div><div><ul style="text-align: left;"><li>A hole must lie inside its shell</li><li>A hole may not lie inside another hole. </li><li>In MultiPolygons, a shell may not lie inside another shell, unless the inner shell lies in a hole of the outer one. </li></ul>Ring position can often be tested using a simple Point-In-Polygon test. But it can happen that the ring point tested lies on the boundary of the other ring. In fact, it is possible that <i>all</i> points of a ring lie on another ring. In this case the Point-In-Polygon test is not sufficient to provide information about the relative position of the rings. Instead, the topology of the incident segments at the intersection is used to determine the relative position. Since the rings are known to not cross, it is sufficient to test whether a single segment of one ring lies in the interior or exterior of the other ring.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBQAvH0ZqSgrv1V9XI9-1EhYRQP5MSDJKnBvcGdlQGz1NgKrDF1ERJb9ODJzUtGtXymB54tfskaW5Q111HFSnUvANs0_W7Oakh1jgi53g7amFdM4mU0NkDeYFyLrpunmOJ9OcA4OCgpE8/s324/polygon-touch-all-vertices.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="265" data-original-width="324" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBQAvH0ZqSgrv1V9XI9-1EhYRQP5MSDJKnBvcGdlQGz1NgKrDF1ERJb9ODJzUtGtXymB54tfskaW5Q111HFSnUvANs0_W7Oakh1jgi53g7amFdM4mU0NkDeYFyLrpunmOJ9OcA4OCgpE8/s320/polygon-touch-all-vertices.png" width="320" /></a></div><br /><div style="text-align: center;"><i>MultiPolygon with an element where all vertices lie on the boundary</i></div><div>Checking nesting of holes and shells requires comparing all interacting pairs of rings. A simple looping algorithm has quadratic complexity, which is too slow for large geometries. Instead, spatial indexes (using the <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/index/strtree/STRtree.html">STRtree</a></span>) are used to make this process performant. </div><h4 style="text-align: left;">3. Check Connected Interior </h4><p>The final check is that the interior of each polygon is connected. In a polygon with no self-touching rings, there are two ways that interior disconnection can happen:</p><p></p><ul style="text-align: left;"><li>a chain of one or more touching holes touches the shell at both ends, thus splitting the polygon in two</li><li>a chain of of two or more touching holes forms a loop, thus enclosing a portion of the polygon interior </li></ul><div>To check that these conditions do not occur, the only information needed is the set of locations where two rings touch. These induce the structure of an <a href="https://en.wikipedia.org/wiki/Graph_%28discrete_mathematics%29">undirected graph</a>, with the rings being the graph vertices, and the touches forming the edges. The situations above correspond to the touch graph containing a <a href="https://en.wikipedia.org/wiki/Cycle_(graph_theory)">cycle</a> (the splitting situation is a cycle because the shell ring is also a graph vertex). (Equivalently, the touch graph of a valid polygon is a <b>tree, </b>or more accurately a <b>forest</b>, since there may be sets of holes forming disconnected subgraphs.) So a polygon can be verified to have a connected interior by checking that its touch graph has no cycles. This is done using a simple graph traversal, detecting vertices (rings) which are visited twice.</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2FXm2UNV6MpuslctajJVdSuNlxBR79FP100V0WlfW7nNqCLN4Y4Uuhi-0E0PVWge7hmrGuLM2ZL9YetL7iE6rCpM60WJK0rGfQNdQPj1L8zJC1z0PbOH3_c1RWJDLkm2HU-qYemH2vXA/s514/disconnected-polygon.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="379" data-original-width="514" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh2FXm2UNV6MpuslctajJVdSuNlxBR79FP100V0WlfW7nNqCLN4Y4Uuhi-0E0PVWge7hmrGuLM2ZL9YetL7iE6rCpM60WJK0rGfQNdQPj1L8zJC1z0PbOH3_c1RWJDLkm2HU-qYemH2vXA/s320/disconnected-polygon.png" width="320" /></a></div><i><div style="text-align: center;"><i>Situations causing a disconnected interior: </i></div><div style="text-align: center;"><i>(i) a chain of holes; (ii) a cycle of holes</i></div></i><div>If no invalid situations are discovered during the above checks, the polygonal geometry is valid according to the OGC rules.</div><p></p><h3 style="text-align: left;">Validity with Self-touching Rings</h3><div>Some spatial systems allow a polygon model in which "inverted shells" are valid. These are polygon shells which contain a self-touch in a way that encloses some exterior area. They also allow "exverted holes", which contain self-touches that disconnect the hole into two or more lobes. A key point is that they do <b>not</b> allow "exverted shells" or "inverted holes"; self-touches may disconnect the polygon exterior, but not the interior. Visually this looks like:</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiymLSbJL0jD8DH7WY_OM8yz_wJnLGdvWB3ko-si-19OWeYhOEu-U8FxeFS6AfBj8fd18ieYo3pwLMu0_OOWlZREXSXeq3TBdplFx9Yh-KiqWroeTp8Jq4cNYT-TPVr0KcTEN_nXkzuvOk/s559/inverted-exverted.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="252" data-original-width="559" height="180" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiymLSbJL0jD8DH7WY_OM8yz_wJnLGdvWB3ko-si-19OWeYhOEu-U8FxeFS6AfBj8fd18ieYo3pwLMu0_OOWlZREXSXeq3TBdplFx9Yh-KiqWroeTp8Jq4cNYT-TPVr0KcTEN_nXkzuvOk/w400-h180/inverted-exverted.png" width="400" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Left: Inverted shell and exverted hole (valid)</i></div><div class="separator" style="clear: both; text-align: center;"><i>Right: Exverted shell and inverted hole (invalid)</i></div><div><br /></div><div>This kind of topology is invalid under the OGC polygon model, which prohibits all ring self-intersections. However, the more lenient model still provides unambiguous topology, and most spatial algorithms can handle geometry of this form perfectly well. So there is a school of thought that considers the OGC model to be overly restrictive.</div><div>To support these systems JTS provides the <span style="font-family: courier;"><a href="https://locationtech.github.io/jts/javadoc/org/locationtech/jts/operation/valid/IsValidOp.html#setSelfTouchingRingFormingHoleValid-boolean-">setSelfTouchingRingFormingHoleValid</a></span> option for <span style="font-family: courier;">IsValidOp</span> to allow this topology model to be validated. Re-implementing this functionality in the new codebase initially presented a dilemma. It appeared that building the touch graph to check connectivity required computing the geometry of the additional rings formed by the inversions and exversions. This needed a significant amount of additional code and data structures, eliminating the simplicity of the graph-based approach. </div><div>However, it turns out that the solution is much simpler. It relies on the fact that valid self-touches can only disconnect the <b>exterior</b>, not the <b>interior</b>. This means that self-touches of inverted shells and exverted holes can be validated by the local topology at the self-intersection node. This condition can be tested by the same logic already used to test for nested touching shells. Even better, if the self-touches are valid, then the touch-graph algorithm to check connectivity still works. (This is the same phenomenon that allows most spatial algorithms to work correctly on the inverted/exverted ring model.) </div><h3 style="text-align: left;">Performance</h3><p>As expected, the new codebase provides better performance, due to simpler code and the ability to short-circuit when an invalidity is found. Here's some performance comparisons for various datasets:</p>
<p>
</p><table border="1" cellpadding="4">
<tbody><tr><td><b>Data</b></td><td><b>Size</b></td><td><b>New (ms)</b></td><td><b>Old (ms)</b></td><td><b>Improvement</b></td></tr>
<tr><td><span style="font-family: courier;">world</span></td> <td>244 Polygons, 366K vertices</td> <td>340</td><td>1250</td><td> 3.7 x</td></tr>
<tr><td><span style="font-family: courier;">invalid-polys</span></td> <td>640 Polygons, 455K vertices</td> <td>126</td><td>334</td><td> 2.6 x</td></tr>
<tr><td><span style="font-family: courier;">valid-polys</span></td> <td>640 Polygons, 455K vertices</td> <td>244</td><td>487</td><td> 2 x</td></tr>
<tr><td><span style="font-family: courier;">australia</span></td> <td>1 MultiPolygon, 1,222K vertices</td> <td>1321</td><td>69026</td><td> 52 x</td></tr>
</tbody></table>
<p></p>
<div style="text-align: left;"><ul style="text-align: left;"><li><span style="font-family: courier;">world</span><span style="font-family: inherit;"> is the standard dataset of world country outlines</span></li><li><span style="font-family: courier;">invalid-polys</span> is a dataset of all invalid polygons. <span style="font-family: courier;">valid-polys</span> is the same dataset processed by <span><span style="font-family: courier;">GeometryFixer</span><span style="font-family: inherit;">. The timings show the effect of short-circuiting invalidity to improve performance. </span></span></li><li><span><span style="font-family: courier;">australia</span><span style="font-family: inherit;"> is a geometry of the Australian coastline and near-shore islands, with 6,697 polygon elements. The dramatic difference in performance reflects the addition of a spatial index for checking nested shells.</span></span></li></ul><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc2VgW5lTkKpo3iPl3n9ba7pSJFrZ2OPXU0UcraQt-C5KxzHy7H5wpSmIub-ybs0L7hmEKKtcN2sWz4mwG3lCDlv5KhFpATB0khNJAjT-kXA7WzO-QZPLrfNt5AAZ870Z1q4StZek5ank/s364/australia-islands.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="313" data-original-width="364" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjc2VgW5lTkKpo3iPl3n9ba7pSJFrZ2OPXU0UcraQt-C5KxzHy7H5wpSmIub-ybs0L7hmEKKtcN2sWz4mwG3lCDlv5KhFpATB0khNJAjT-kXA7WzO-QZPLrfNt5AAZ870Z1q4StZek5ank/s320/australia-islands.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Australia and 6,696 islands</i></div><div><br /></div></div><h3 style="text-align: left;">Next Steps</h3><p>The improved IsValidOp will appear in JTS version 1.19. It has already been <a href="https://github.com/libgeos/geos/pull/464">ported</a> to <a href="https://trac.osgeo.org/geos">GEOS</a>, providing similar performance improvements. And since GEOS backs the <a href="https://postgis.net/">PostGIS</a> <span style="font-family: courier;"><a href="https://postgis.net/docs/ST_IsValid.html">ST_IsValid</a></span> function, that will become faster as well.</p><p>There is one remaining use of <span style="font-family: courier;">GeometryGraph</span> in JTS for a non-constructive operation: the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/relate/RelateOp.java">RelateOp</a></span><span style="font-family: inherit;"><span> class, </span>us</span>ed to compute all topological spatial predicates. Converting it should provide the same benefits of simpler code and improved performance. It should also improve robustness, and allow more lenient handling of invalid inputs. Watch this space!</p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-15992560169335120822021-05-26T17:48:00.000-07:002021-05-26T17:48:21.332-07:00JTS IsSimple gets simpler (and faster)<p>Hard to believe that the <a href="https://github.com/locationtech/jts">JTS Topology Suite</a> is <a href="https://github.com/locationtech/jts/blob/master/doc/JTS_Version_History.md#version-10">almost</a> 20 years old. That's 140 in dog years! Despite what they say about old dogs, one of the benefits of longevity is that you have the opportunity to learn a trick or two along the way. One of the key lessons learned after the initial release of JTS is that intersection (node) detection is a fundamental part of many spatial algorithms, and critical in terms of performance. This resulted in the development of the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/noding">noding</a></span> package to provide an API supporting many different kinds of intersection detection and insertion.</p><p>Prior to this, intersection detection was performed as part of the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/geomgraph">GeometryGraph</a></span> framework, which combined it with topology graph formation and analysis. At the time this seemed like an elegant way to maximize code reuse across many JTS operations, including overlay, buffering, spatial predicates and validation. But as often the case, there are significant costs to such general-purpose code:</p><p></p><ul style="text-align: left;"><li>The overall codebase is substantially more complex</li><li>A performance penalty is imposed on algorithms which don't require topology construction</li><li>Algorithms are harder to read and understand. </li><li>The code is brittle, and so hard to modify</li><li>Porting the code is more difficult </li></ul>Because of this, a focus of JTS development is to free operations from their dependency on <span style="font-family: courier;">GeometryGraph</span> - with the ultimate goal of expunging it from the JTS codebase. A major step along this road was the <a href="http://lin-ear-th-inking.blogspot.com/2020/10/overlayng-lands-in-jts-master.html">rewrite of the overlay operations</a>.<p></p><p>Another operation that relies on <span style="font-family: courier;">GeometryGraph</span> is the <span style="font-family: courier;">IsSimpleOp</span> class, which implements the <a href="https://www.ogc.org/standards/sfs">OGC Simple Features</a> <span style="font-family: courier;">isSimple</span> predicate. The algorithm for <span style="font-family: courier;">isSimple</span> essentially involves determining if the geometry linework contains a self-intersection. <span style="font-family: courier;">GeometryGraph</span> is unnecessarily complex for this particular task, since there is no need to compute the entire topology graph in order to find a single self-intersection. Reworking the code to use the the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/noding/MCIndexNoder.java">MCIndexNoder</a></span> class in the <span style="font-family: courier;">noding</span> API produces a much simpler and more performant <a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/valid/IsSimpleOp.java">implementation</a>. I also took the opportunity to move the code to the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/tree/master/modules/core/src/main/java/org/locationtech/jts/operation/valid">operation.valid</a></span> package, since the operations of <b>isSimple</b> and <b>isValid</b> are somewhat complementary.</p><p>Now, <span style="font-family: courier;">isSimple</span> is probably the least-used OGC operation. Its only real use is to test for self-intersections in lines or collections of lines, and that is not a critical issue for many workflows. However, there is one situation where it is quite useful: testing that linear network datasets are "vector-clean" - i.e. contain LineStrings which touch only at their endpoints.</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOl7PurP3HbJ48NJmn_vLZ6aWiT7wAlLs5X4mds6jIf1RSDyNVaU0mjUwtZS_J0ADPXl8veFmpC3dTioJR1RJ30HU1vsGu0iID2l-WDauKGDwoeMCNUDSFNSYtNw9oPcjLi2eerJ5hHVQ/s353/non-simple+network.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="254" data-original-width="353" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjOl7PurP3HbJ48NJmn_vLZ6aWiT7wAlLs5X4mds6jIf1RSDyNVaU0mjUwtZS_J0ADPXl8veFmpC3dTioJR1RJ30HU1vsGu0iID2l-WDauKGDwoeMCNUDSFNSYtNw9oPcjLi2eerJ5hHVQ/s320/non-simple+network.png" width="320" /></a></div><p style="text-align: center;"><i>A linear network containing non-simple intersections (<span style="font-family: courier;">isSimple == false</span>)</i></p><p>To demonstrate the performance improvement, I'll use a dataset for <a href="https://www.weather.gov/gis/Rivers">Rivers of the US</a> maintained by the US National Weather Service. It supplies two datasets: a full dataset of all rivers, and a subset of major rivers only. You might expect a hydrographic network to be "vector-clean", but in fact both of these datasets contain numerous instances of self-intersections and coincident linework.</p><p>Here's the results of running the <span style="font-family: courier;">isSimple</span> predicate on the datasets. On the larger dataset the new implementation provides a 20x performance boost!</p>
<table border="2">
<tbody><tr><td> <b>Dataset</b></td><td> <b>New time</b> </td><td> <b>Old time</b> </td></tr>
<tr><td> Subset (909,865 pts) </td><td> 0.25 s</td><td> 1 s</td></tr>
<tr><td> Full (5,212,102 pts)</td><td> 2 s</td><td> 30 s</td></tr>
</tbody></table>
<h4 style="text-align: left;">Finding Non-Simple Locations</h4><p>The new codebase made it easy to add a functionality enhancement that computes the locations of all places where lines self-intersect. This can be used to for visual confirmation that the operation is working as expected, and to indicate places where data quality needs to be improved. Here's the non-simple intersection points found in the river network subset:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH-9AdKshusxgIh0XLIZoSHtFPReu6S83noc5E1UgXXfQsiTekZw482cYIXTCkpMzdxAKk7ixEZ4aByX4YkM-O05OH-CdWaO8XzCnBu5_BEhJWifpdPR9gxx9d4n-fwZiRuQSFlYKIi3A/s621/us-rivers-non-simple-locs.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="307" data-original-width="621" height="198" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgH-9AdKshusxgIh0XLIZoSHtFPReu6S83noc5E1UgXXfQsiTekZw482cYIXTCkpMzdxAKk7ixEZ4aByX4YkM-O05OH-CdWaO8XzCnBu5_BEhJWifpdPR9gxx9d4n-fwZiRuQSFlYKIi3A/w400-h198/us-rivers-non-simple-locs.png" width="400" /></a></div><p>Closeups of some non-simple intersection locations:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXSkulrfLMA31CExLDdNg3l_b8B2R5lRfqraztA9SDhoFZcQ7gvDa6uz4-Qangn1qcHijEZA-BJugOrv0LiMlqs6ez6LjokA6xAs5dYVVJ0ZUbWRzxOE1WjbEQO4D87U4wH_1rmX6rzoE/s430/non-simple-rivers-1.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="217" data-original-width="430" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiXSkulrfLMA31CExLDdNg3l_b8B2R5lRfqraztA9SDhoFZcQ7gvDa6uz4-Qangn1qcHijEZA-BJugOrv0LiMlqs6ez6LjokA6xAs5dYVVJ0ZUbWRzxOE1WjbEQO4D87U4wH_1rmX6rzoE/s320/non-simple-rivers-1.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUEx0SiX4FZOVHWreEBa1dJB2DvQtiZq_JUz-2Sywin5OtlP5n4HvE5x3elVPGLxlm5ym4tCQ1g7w3oyk5EdqszAvdCAptoNzFFTh5HG96Ye5ENOqMdyYkmhgU4DkVqeIMI1CThGtUykk/s371/non-simple-rivers-2.png" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="245" data-original-width="371" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjUEx0SiX4FZOVHWreEBa1dJB2DvQtiZq_JUz-2Sywin5OtlP5n4HvE5x3elVPGLxlm5ym4tCQ1g7w3oyk5EdqszAvdCAptoNzFFTh5HG96Ye5ENOqMdyYkmhgU4DkVqeIMI1CThGtUykk/s320/non-simple-rivers-2.png" width="320" /></a></div><br /><p><span style="font-family: courier;">IsSimpleOp</span> is the easiest algorithm to convert over from using <span style="font-family: courier;">GeometryGraph</span>. As such it serves as a good proof-of-viability, and establishes useful code patterns for further conversions. </p><p>Next up is to give <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/operation/valid/IsValidOp.java">IsValidOp</a></span> the same treatment. This should provide similar benefits of simplicity and performance. And as always, porting the improved code to <a href="https://trac.osgeo.org/geos">GEOS</a>.</p><br /><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com1tag:blogger.com,1999:blog-2420860529344694449.post-54240717961281536132021-05-03T22:27:00.001-07:002021-05-03T22:27:22.074-07:00Fixing Invalid Geometry with JTS<p><i>TLDR: JTS can now fix invalid geometry!</i></p><p>The <a href="https://github.com/locationtech/jts"><b>JTS Topology Suite</b></a> implements the Geometry model defined in the OGC <a href="https://www.ogc.org/standards/sfa">Simple Features specification</a>. An important part of the specification is the definition of what constitutes valid geometry. These are defined by rules about the structural and geometric characteristics of geometry objects. Some validity rules apply to all geometry; e.g. vertices must be defined by coordinates with finite numeric values (so that <span style="font-family: courier;">NaN</span> and <span style="font-family: courier;">Inf</span> ordinates are not valid). In addition, each geometric subtype (Point, LineString, LinearRing, Polygon, and Multi-geometry collections) has its own specific rules for validity.</p><p>The rules for Polygons and MultiPolygons are by far the most restrictive. They include the following constraints:</p><p></p><ol style="text-align: left;"><li>Polygons rings must not self-intersect</li><li>Rings may touch at only a finite number of points, and must not cross</li><li>A Polygon interior must be connected (i.e. holes must not split a polygon into two parts)</li><li>MultiPolygon elements may touch at only a finite number of points, and must not overlap</li></ol><p></p><p>These rules guarantee that:</p><p></p><ul style="text-align: left;"><li>a given area is represented unambiguously by a polygonal geometry</li><li>algorithms operating on polygonal geometry can make assumptions which provide simpler implementation and more efficient processing</li></ul><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi01sDZyQLxaMR9t4DRCnitNta7_NP_XXIsId5XekJBsrmm0PpKvy1k32cK7shTKYwYmKspJ00c6AktWcCIIGBK-Q55OBqM17v8clKdNEB-XiPwnqtdYyzcGgdV83L027c1pid4SIIVUEM/s757/valid-polygons.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="250" data-original-width="757" height="132" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi01sDZyQLxaMR9t4DRCnitNta7_NP_XXIsId5XekJBsrmm0PpKvy1k32cK7shTKYwYmKspJ00c6AktWcCIIGBK-Q55OBqM17v8clKdNEB-XiPwnqtdYyzcGgdV83L027c1pid4SIIVUEM/w400-h132/valid-polygons.png" width="400" /></a></div><div style="text-align: center;"><i>Valid polygonal geometry is well-behaved</i></div><p>Given the highly-constrained definition of polygonal validity, it is not uncommon that real-world datasets contain polygons which do not satisfy all the rules, and hence are invalid. This occurs for various reasons:</p><p></p><ul style="text-align: left;"><li>Data is captured using tools which do not check validity, or which use a looser or different definition than the OGC standard</li><li>Data is imported from systems with different polygonal models</li><li>Data is erroneous or inaccurate </li></ul><p></p><p>Because of this, JTS does not enforce validity on geometry creation, apart from a few simple structural constraints (such as rings having identical first and last points). This allows invalid geometry to be represented as JTS geometry objects, and processed using JTS code. Some kinds of spatial algorithms can execute correctly on invalid geometry (e.g. determining the convex hull). But most algorithms require valid input in order to ensure correct results (e.g. the spatial predicates) or to avoid throwing exceptions (e.g. overlay operations). So the main reason for representing invalid geometry is to allow validity to be tested, to take appropriate action on failure.</p><div><p>Often users would like "appropriate action" to be Just Make It Work. This requires converting invalid geometry to be valid. Many spatial systems provide a way to do this: </p><p></p><ul><li>PostGIS has the <a href="https://postgis.net/docs/manual-3.1/ST_MakeValid.html"><span style="font-family: courier;">ST_MakeValid</span></a> function (which is backed by an <a href="https://git.osgeo.org/gitea/geos/geos/src/branch/main/src/operation/valid/MakeValid.cpp">implementation</a> in GEOS)</li><li>QGIS has a <a href="https://docs.qgis.org/3.16/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#fix-geometries">Fix Geometries</a> process. </li><li>OpenJUMP has <a href="https://sourceforge.net/p/jump-pilot/code/HEAD/tree/core/trunk/src/com/vividsolutions/jump/geom/MakeValidOp.java">MakeValidOp</a></li><li>The ESRI Java Geometry API has an operation called (confusingly) <a href="http://esri.github.io/geometry-api-java/javadoc/com/esri/core/geometry/ogc/OGCGeometry.html#makeSimple--"><span style="font-family: courier;">makeSimple</span></a>. </li></ul></div><p>But this has a been a conspicuous gap in the JTS API. While it is possible to test for validity, there has never been a way to fix an invalid geometry. To be fair, JTS has always had an <i>unofficial</i> way to make polygonal geometry valid. This is the well-known trick of computing <span style="font-family: courier;">geometry.buffer(0)</span>, which creates a valid output which often is a good match to the input. This has worked as a stop-gap for years (in spite of an issue which caused some problems, now fixed - see the post <a href="https://lin-ear-th-inking.blogspot.com/2020/12/fixing-buffer-for-fixing-polygons.html"><i>Fixing Buffer for fixing Polygons</i></a>). However, using <span style="font-family: courier;">buffer(0)</span> on self-intersecting "figure-8" polygons produces a "lossy" result. Specifically, it retains only the largest lobes of the input linework. This is undesirable for some uses (although it is advantageous in other situations, such as trimming off small self-intersections after polygon simplification).</p><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBn2OWlQbUnfdqcH8pvdGFLXU8r_jiG0vzzqUJhcMIFrr4ZAoyzMjUDDvG4rAilJr_iSwLqoaf1BdUimCm6wgawRVO2RkLaVtMERA4YtC8v-K_KVLm5f_9IIuux8041k9Ek67B32FAJZo/s578/figure-8-buffer-0.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="244" data-original-width="578" height="169" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgBn2OWlQbUnfdqcH8pvdGFLXU8r_jiG0vzzqUJhcMIFrr4ZAoyzMjUDDvG4rAilJr_iSwLqoaf1BdUimCm6wgawRVO2RkLaVtMERA4YtC8v-K_KVLm5f_9IIuux8041k9Ek67B32FAJZo/w400-h169/figure-8-buffer-0.png" width="400" /></a></div><div style="text-align: center;"><i>Buffer(0) of Figure-8 is lossy </i></div><p></p><div><p>So, it's about time that JTS stepped up to provide a supported, guaranteed way of fixing invalid geometry. This should handle all geometry, although polygonal geometry repair is the most critical requirement.</p></div><div>This raises the question of what exactly the semantics of repairing polygons should be. While validity is well-specified, there are no limits to the complexity of <i>invalid</i> polygons, and a variety of possible approaches to fixing them. The most significant decision is how to determine the interior and exterior of a polygonal geometry with self-intersections or overlaps. (This is the classic "bow-tie" or "figure-8" - although self-intersecting polygons can be far more complex.) The question comes down to whether the geometry <b>linework</b> or <b>structure</b> is used to determine interior areas. </div><div><br /></div><div>If <b>linework</b> is used to create validity, to node the constituent linework to form a topologically-valid coverage. This coverage is then scanned with an alternating even-odd strategy to assign areas as interior or exterior. This may result in adjacent interior or exterior areas, in which case these are merged.</div><div><br /></div><div>Alternatively, the <b>structure</b> of the polygonal geometry can be taken as determinative. The shell and hole rings are assumed to accurately specify the nature of the area they enclose (interior or exterior). Likewise, the (potentially overlapping or adjacent) elements of a MultiPolygon are assumed to enclose interior area. The repair operation processes each ring and polygon separately. Holes are subtracted from shells. Finally, if required the repaired polygons are unioned to form the valid result. </div><div><br /></div><div>PostGIS <span style="font-family: courier;">MakeValid</span> and the ESRI Java Geometry API <span style="font-family: courier;">makeSimple</span> both use the linework approach. However, for some relatively simple invalid geometries this produces results which seem overly complex. </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhQMD4XO_xZ_E6tZjLOa7VxJ7zTZTE1_LALi2q1g2WH2THSo0yxCTvsdBCq2dfwr65G7z5SJhDq8S-AzhaMJfZQdOPjrRKvkNG6d-jogvn5F516xmQWNSCVW_rOFpvJeDIGdX3NuDNU7c/s651/makeValid-complex-2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="297" data-original-width="651" height="183" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhQMD4XO_xZ_E6tZjLOa7VxJ7zTZTE1_LALi2q1g2WH2THSo0yxCTvsdBCq2dfwr65G7z5SJhDq8S-AzhaMJfZQdOPjrRKvkNG6d-jogvn5F516xmQWNSCVW_rOFpvJeDIGdX3NuDNU7c/w400-h183/makeValid-complex-2.png" width="400" /></a></div><div style="text-align: center;"><i>Complex output from ST_MakeValid</i></div><div><br /></div><div>For this reason I have implemented the structure-based approach in JTS. It provides results that are closer to the existing <span style="font-family: courier;">buffer(0)</span> technique (and conveniently allows using the existing buffer code). This made it a relatively simple matter to implement the repair algorithm as the <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/core/src/main/java/org/locationtech/jts/geom/util/GeometryFixer.java">GeometryFixer</a></span> class.</div><div><br /></div><div>Here's some examples of how GeometryFixer works. First, the example above, showing the (arguably) simpler result that arises from using the structure information:</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLZOvxfQfcWGs6yhi2RQfvGHrbIUk3_cTdAHTiR5dDOWERt1b5lCo0Wa1WzUBekoU2abN1MnOCkiaYjKrauJWJrt7omHSgTr9YirUKyFbTJvVtJOPSU-Tz025l00AOEQSoUoxPk90NdQA/s666/geomfixer-complex-2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="308" data-original-width="666" height="185" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhLZOvxfQfcWGs6yhi2RQfvGHrbIUk3_cTdAHTiR5dDOWERt1b5lCo0Wa1WzUBekoU2abN1MnOCkiaYjKrauJWJrt7omHSgTr9YirUKyFbTJvVtJOPSU-Tz025l00AOEQSoUoxPk90NdQA/w400-h185/geomfixer-complex-2.png" width="400" /></a></div><div><div>Figure-8s are handled as desired (keeping all area):</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwp7xisN2MWXYBuDrTbQg0b-R69G66NbqooeG5SzTKMV13u7ahG3dtfVVQWIlrdc91Uj8mOvTRpU9G5Q6p9-kw49-h_AKkF4xtstkWN6MchlKvtNAKZISBlRFyjFGnL_DmEU8AlTPdMuw/s727/geomfixer-figure-8.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="336" data-original-width="727" height="185" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjwp7xisN2MWXYBuDrTbQg0b-R69G66NbqooeG5SzTKMV13u7ahG3dtfVVQWIlrdc91Uj8mOvTRpU9G5Q6p9-kw49-h_AKkF4xtstkWN6MchlKvtNAKZISBlRFyjFGnL_DmEU8AlTPdMuw/w400-h185/geomfixer-figure-8.png" width="400" /></a></div><br /><div>Self-overlapping shells have all interior area preserved:</div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik6uZcxLWfCsUB3GlU7-bSbO7d7PBqYjvbt-ZSX81wV2XUiN8SL_c_cJ5Z0BFkCHGFc2UX1K0GgCKuH-HAP9wVZX1tlv8W40wWuIUipifISxn6dPkanYSzR43mJXRccHROU97T0sGvUY4/s507/geomfixer-self-overlap.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="238" data-original-width="507" height="188" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik6uZcxLWfCsUB3GlU7-bSbO7d7PBqYjvbt-ZSX81wV2XUiN8SL_c_cJ5Z0BFkCHGFc2UX1K0GgCKuH-HAP9wVZX1tlv8W40wWuIUipifISxn6dPkanYSzR43mJXRccHROU97T0sGvUY4/w400-h188/geomfixer-self-overlap.png" width="400" /></a></div>Of course, the GeometryFixer also handles simple fixes for all geometry types, such as removing invalid coordinates.</div><div><br /></div><div>One further design decision is how to handle geometries which are invalid due to collapse (e.g. a line with a single point, or a ring which has only two unique vertices). GeometryFixer provides an option to either remove collapses, or to return them as equivalent lower dimensional geometry. </div><div><br /></div><div>To see the full range of effects of GeometryFixer, the JTS TestBuilder can be used to view and run the GeometryFixer on the set of 75 test cases for invalid polygonal geometry in the file <span style="font-family: courier;"><a href="https://github.com/locationtech/jts/blob/master/modules/tests/src/test/resources/testxml/misc/TestInvalidA.xml">TestInvalidA.xml</a></span>. </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3_miH5TVP1NMZM8hIeLDcVrgRB6qN4PG3lgzSvdSYy-7r4XlsUy0b7zstwameU7ToLEscwyTSx1EaacwvG0cqB4TOpr_pzKHZbbHYVzk0U-fOsijiRh74EdHGD3XNmMaHFBZbACTmLJM/s726/geomfixer-TestBuilder.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="701" data-original-width="726" height="386" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3_miH5TVP1NMZM8hIeLDcVrgRB6qN4PG3lgzSvdSYy-7r4XlsUy0b7zstwameU7ToLEscwyTSx1EaacwvG0cqB4TOpr_pzKHZbbHYVzk0U-fOsijiRh74EdHGD3XNmMaHFBZbACTmLJM/w400-h386/geomfixer-TestBuilder.png" width="400" /></a></div><br /><div>It's been a long time coming, but finally JTS can function as a full-service repair shop for geometry, no matter how mangled it might be.</div><div><br /><div><br /></div><div><br /></div><div><br /></div><div> </div><div><br /></div><div><br /></div></div>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com2tag:blogger.com,1999:blog-2420860529344694449.post-72542931336757246912021-04-28T19:08:00.001-07:002021-04-28T19:08:42.503-07:00JTS goes to Mars!<p>The other day this badge popped up on my GitHub profile:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSReC3bYpyC2Yqx0V8ZX1ej0RB-6y1rSPLrpxj_u7ItUu1Nypj9lva88KWW0u3NPhUrjtdX9W_uC_Q8cHT7ORvUmnrCYCDWZNrWOKoe2Tuz85zKy-skRg3I6d1hTQXUF7MJ_Hr7kllT4Y/s353/2021-04-25.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="149" data-original-width="353" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjSReC3bYpyC2Yqx0V8ZX1ej0RB-6y1rSPLrpxj_u7ItUu1Nypj9lva88KWW0u3NPhUrjtdX9W_uC_Q8cHT7ORvUmnrCYCDWZNrWOKoe2Tuz85zKy-skRg3I6d1hTQXUF7MJ_Hr7kllT4Y/s320/2021-04-25.png" width="320" /></a></div><p>Sure enough, there is <a href="https://github.com/locationtech/jts">JTS</a> in the <a href="https://docs.github.com/en/github/setting-up-and-managing-your-github-profile/personalizing-your-profile#list-of-qualifying-repositories-for-mars-2020-helicopter-contributor-badge">list of dependencies</a> provided by the Jet Propulsion Laboratory (JPL). </p><p>So take that, Elon - JTS got there first! With a lot of help from NASA, of course. </p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDfGEJ2pwfmq45ztaLJZm6ggrX1Cs1KdaBaB1ipAhrJV_D13DbCqCCKAZfYcP5169WkH5g8edE54_JgrS4BI0X3NCufiQgA5U0PKWYyvoQc_2VjhgSuLSRYu5hDEVhfQ_Gaez4yzpZp_E/s262/marvin-gun.jpeg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="262" data-original-width="192" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDfGEJ2pwfmq45ztaLJZm6ggrX1Cs1KdaBaB1ipAhrJV_D13DbCqCCKAZfYcP5169WkH5g8edE54_JgrS4BI0X3NCufiQgA5U0PKWYyvoQc_2VjhgSuLSRYu5hDEVhfQ_Gaez4yzpZp_E/s0/marvin-gun.jpeg" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Oh how nice, the Earthlings are giving me some target practice!</i></div><br /><p><br /></p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com1tag:blogger.com,1999:blog-2420860529344694449.post-58683159666439850252021-02-02T10:42:00.000-08:002021-02-02T10:42:14.106-08:00OverlayNG and Invalid Geometry<p>A <a href="https://elephanttamer.net/?p=55">recent blog post</a> by Elephant Tamer gives a critical appraisal of the improvements to overlay processing shipped in <a href="https://postgis.net/2020/12/18/postgis-3.1.0">PostGIS 3.1</a> with <a href="https://trac.osgeo.org/geos">GEOS 3.9</a>. The author is disappointed that PostGIS still reports errors when overlay is used on invalid geometry. However, this is based on a misunderstanding of the technology. </p><p>GEOS 3.9 includes OverlayNG, <a href="http://lin-ear-th-inking.blogspot.com/2020/10/overlayng-lands-in-jts-master.html">ported from</a> the JTS Topology Suite). It brings a major advance in overlay robustness, along with other improvements (described <a href="http://lin-ear-th-inking.blogspot.com/2020/05/jts-overlay-next-generation.html">here</a> and <a href="http://lin-ear-th-inking.blogspot.com/2020/06/jts-overlayng-noding-strategies.html">here</a>). Previously, robustness limitations in the overlay algorithm could sometimes cause errors even for inputs which were topologically valid. This was doubly problematic because there was no fully effective way to process the input geometries to avoid the errors. Now, OverlayNG solves this problem completely. <b>Valid inputs</b> will <b>always</b> produce a valid and essentially correct(*) output.</p><p><i>(*) "Essentially" correct, because in order to achieve full robustness a snapping heuristic may be applied to the input geometry. However, this is done with a very fine tolerance, so should not appreciably alter the output from the theoretically correct value.</i></p><p>But for <b>invalid</b> inputs, OverlayNG will still report errors. The reason is that there is a wide variety of gruesome ways in which geometry can be invalid. Automated handling of invalidity would involve expensive extra processing, and also require making assumptions about what area a corrupt geometry is intended to represent. Rather than silently repairing invalid geometry and returning potentially incorrect results, the design decision is to report this situation as an error.</p><p>In fact, OverlayNG <i>is</i> able to handle "mildly" invalid polygons, as described in <a href="http://lin-ear-th-inking.blogspot.com/2020/06/jts-overlayng-tolerant-topology.html">this post</a>. This covers situations which are <i>technically</i> invalid according to the OGC SFS specification, but which still have well-defined topology. This includes <b>self-touching rings</b> (sometimes called "inverted polygons" or "exverted holes"), and <b>zero-width gores and spikes</b>.</p><p>Taking a detailed look at the data used in the blog post, we can see these improvements at work. The dataset is the ecology polygons obtained from the <a href="http://sdi.gdos.gov.pl/wfs?SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&TYPENAME=GDOS:UzytkiEkologiczne&SRSNAME=EPSG:2180&outputFormat=shape-zip&format_options=charset:utf-8">GDOS WFS server</a>. This contains 7662 geometries, of which 10 are invalid. Using the old overlay algorithm, 9 of these invalid polygons cause <span style="font-family: courier;">TopologyException</span> errors. Using OverlayNG, only 4 of them cause errors. </p><p>The polygons that can now be processed successfully are typical "OGC-invalid" situations, which do not materially affect the polygonal topology. These include <b>self-touching rings with pinch points</b>:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhu5W1Qcc-r3e4KCczmkHv6hIDp3ppYKx50dgFKmjq7pP_DYpePFtUB7IVGhJZS_XsPSrAwNF2m8JjpvpvDJmnk3x-vvFYGwosExDD7xCv7WIVvxBDsisziKJePCkBVcFoHiFKSoQNKUNY/s433/gdos-pinch.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="389" data-original-width="433" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhu5W1Qcc-r3e4KCczmkHv6hIDp3ppYKx50dgFKmjq7pP_DYpePFtUB7IVGhJZS_XsPSrAwNF2m8JjpvpvDJmnk3x-vvFYGwosExDD7xCv7WIVvxBDsisziKJePCkBVcFoHiFKSoQNKUNY/s320/gdos-pinch.png" width="320" /></a></div><p>and <b>zero-width gores</b>:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1feaQkDoJ0VLmYJtZJpNY3hvTgkNe09meQux25uyyB4B-q8JOyKwmyTWJe0IW2lYTKl9E7X2x-dUOui9qZ_6GNi6WLmmmc_QlH2v95RNemf_NWN7a4_p-F7ICMJItCQ6X4I-P_RCG6SQ/s448/gdos-zero-width-gore.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="448" data-original-width="362" height="320" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh1feaQkDoJ0VLmYJtZJpNY3hvTgkNe09meQux25uyyB4B-q8JOyKwmyTWJe0IW2lYTKl9E7X2x-dUOui9qZ_6GNi6WLmmmc_QlH2v95RNemf_NWN7a4_p-F7ICMJItCQ6X4I-P_RCG6SQ/s320/gdos-zero-width-gore.png" /></a></div><br /><p>Of the cases that still cause errors, two are classic small <b>bow-tie errors</b>:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrxO-s4Vr9KC1CnuqxZszwUGk5HOYea-J1_zm_jT7h51HIA5nHI_ocdCENdyqXVg-f7m8-XcnHmLo1azM4KxPogVlaugXU8yzqXVvKXXnuDdAY2yEfDbLnlbD5140zyX16ACpVrzO1xjo/s363/gdos-bowtie.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="237" data-original-width="363" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrxO-s4Vr9KC1CnuqxZszwUGk5HOYea-J1_zm_jT7h51HIA5nHI_ocdCENdyqXVg-f7m8-XcnHmLo1azM4KxPogVlaugXU8yzqXVvKXXnuDdAY2yEfDbLnlbD5140zyX16ACpVrzO1xjo/s320/gdos-bowtie.png" width="320" /></a></div><br /><p>And two are <b>wildly invalid self-crossing rings</b>:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYyBOxV_h8VSwP_krNWuvHJl6UFeCf-y1FpLWtUPda2bDWosdY0qNnkgkRveMisuIF_6lOCmIc97_i8d1pjM8jyatlv2YwbynoId5O_RAqA5TWBL6hO8Op1YhEpjhBiyCahTLko0gifus/s389/gdos-wild-2.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="309" data-original-width="389" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYyBOxV_h8VSwP_krNWuvHJl6UFeCf-y1FpLWtUPda2bDWosdY0qNnkgkRveMisuIF_6lOCmIc97_i8d1pjM8jyatlv2YwbynoId5O_RAqA5TWBL6hO8Op1YhEpjhBiyCahTLko0gifus/s320/gdos-wild-2.png" width="320" /></a></div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLABWmqAbObU8S9tl3KpztzZwsEyrSEOFSZCrI_LRLGeIiEZ79RoXl0PiRqoJrrEsEgX4SdBpjMO3fi_MNiep_u6SrLL-fgPX3AeaCCe6LsLL0SFWdoSaktMrbp266IXRF1CTMoT44SPQ/s427/gdos-wild-invalid-1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="287" data-original-width="427" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLABWmqAbObU8S9tl3KpztzZwsEyrSEOFSZCrI_LRLGeIiEZ79RoXl0PiRqoJrrEsEgX4SdBpjMO3fi_MNiep_u6SrLL-fgPX3AeaCCe6LsLL0SFWdoSaktMrbp266IXRF1CTMoT44SPQ/s320/gdos-wild-invalid-1.png" width="320" /></a></div><br /><p>The last two are good examples of geometry which is so invalid that it is impossible to unambiguously decide what area is represented (although <span style="font-family: courier;">ST_MakeValid</span> will happily grind them into something that is technically valid).</p><p>Ultimately it is the user's responsibility to ensure that geometries to be processed by overlay (and many other PostGIS functions) have valid topology (as reported by <a href="https://postgis.net/docs/manual-3.1/ST_IsValid.html"><span style="font-family: courier;">ST_IsValid</span></a>). Ideally this is done by correcting the data at source. But it can also be done <i>a posteriori</i> in the database itself, by either the <a href="https://postgis.net/docs/manual-3.1/ST_MakeValid.html"><span style="font-family: courier;">ST_MakeValid</span></a> function, or the well-known <a href="https://postgis.net/workshops/postgis-intro/validity.html#st-buffer"><span style="font-family: courier;">buffer(0)</span></a> trick. (Which to use is a topic for another blog post...)</p><p>One improvement that <i>could</i> be made is to check for input validity when OverlayNG throws an error. Then PostGIS can report definitively that an overlay error is caused by invalid input. If there is an overlay error that is <i>not</i> caused by invalidity, the PostGIS team wants to hear about it! </p><p>And perhaps there is a case to be made for repairing invalid geometry automatically, even if the repair is suspect. Possibly this could be invoked via a flag parameter on the overlay functions. More research is required - feedback is welcome!</p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com1tag:blogger.com,1999:blog-2420860529344694449.post-47987231492729778592021-01-13T17:07:00.001-08:002021-01-13T17:07:09.561-08:00Using the GEOS geosop CLI<p>In a <a href="http://lin-ear-th-inking.blogspot.com/2021/01/introducing-geosop-cli-for-geos.html">previous post</a> I announced the new <span style="font-family: courier;"><a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/util/geosop/README.md">geosop</a></span> command-line interface (CLI) for the <a href="https://trac.osgeo.org/geos">GEOS</a> geometry API. This post provides examples of using <span style="font-family: courier;">geosop</span> for various tasks. (Refer to the <a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/util/geosop/README.md">README</a> for more information about the various options used.)</p><h3>API Testing</h3><div><span style="font-family: courier;">geosop</span> makes testing the GEOS library <i>much</i> easier. Previously, testing the behaviour of the API usually required breaking out the C compiler (and updating autotools and cmake build files, and deciding whether to commit the new code for later use or throw it away, etc, etc). Now testing is often just a matter of invoking a <span style="font-family: courier;">geosop</span> operation on appropriate data, or at worst adding a few lines of code to the exiting framework.</div><div><br /></div><div>For example, there is a long-standing issue with how GEOS handles number formatting in WKT output. There are recent bug reports about this in <a href="https://github.com/GEOSwift/GEOSwift/issues/196">GeoSwift</a> and the Julia <a href="https://github.com/JuliaGeo/LibGEOS.jl/issues/86">LibGEOS</a>. <span style="font-family: courier;">geosop</span> makes it easy to run the test cases and see the less-than-desirable output:</div><div><br /></div><div><span style="font-family: courier;">geosop -a "POINT (654321.12 0.12)" -f wkt</span></div><div><span style="font-family: courier;">POINT (654321.1199999999953434 0.1200000000000000)</span></div><div><br /></div><div><span style="font-family: courier;">geosop -a "POINT (-0.4225977234 46.3406448)" -f wkt</span></div><div><span style="font-family: courier;">POINT (-0.4225977234000000 46.3406447999999997)</span></div><div><br /></div><div>There's also an issue with precision handling. To test this we added a <span style="font-family: courier;">--precision</span> parameter to <span style="font-family: courier;">geosop</span>. (This is the kind of rapid development enabled by having the CLI codebase co-resident with the API.)</div><div><br /></div><div><span style="font-family: courier;">geosop -a "POINT (654321.126 0.126)" --precision 2 -f wkt</span></div><div><span style="font-family: courier;">POINT (6.5e+05 0.13)</span></div><div><br /></div><div>Again we see undesirable behaviour. Using scientific notation for small numbers is unnecessary and difficult to read. And the precision value is determining the number of significant digits, not the number of decimal places as intended by the GEOS <span style="font-family: courier;">WKTWriter.setRoundingPrecision</span> API.</div><div><br /></div><div>These were all caused by using standard C/C++ numeric formatting, which is surprisingly limited and non-useful. After some <a href="https://git.osgeo.org/gitea/geos/geos/commit/2376cd6bf5d0743b02b588af17d2e9067c1874de">fine work</a> by Paul Ramsey to integrate the much better <a href="https://github.com/ulfjack/ryu">Ryu</a> library, GEOS now has WKT output that is sensible and handles precision in a useful way.</div><div><br /></div><div>By default, <span style="font-family: courier;">WKTWriter</span> now nicely round-trips WKT text:</div><div><br /></div><div><span style="font-family: courier;">geosop -a "POINT (654321.12 0.12)" -f wkt</span></div><div><span style="font-family: courier;">POINT (654321.12 0.12)</span></div><div><span style="font-family: courier;"><br /></span></div><div><span style="font-family: courier;">geosop -a "POINT (-0.4225977234 46.3406448)" -f wkt</span></div><div><span style="font-family: courier;">POINT (-0.4225977234 46.3406448)</span></div><div><br /></div><div>If <span style="font-family: courier;"><a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/src/io/WKTWriter.cpp#L134">WKTWriter.setRoundingPrecision</a> </span>or<span style="font-family: courier;"> </span><span style="font-family: courier;"><a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/capi/geos_c.cpp#L1111">GEOSWKTWriter_setRoundingPrecision</a></span> is called, the precision value applies to the decimal part of the number: </div><div><br /></div><div><div><div><span style="font-family: courier;">geosop -a "POINT (654321.1234567 0.126)" --precision 0 -f wkt</span></div><div><span style="font-family: courier;">POINT (654321 0)</span></div><div><span style="font-family: courier;"><br /></span></div><div><span style="font-family: courier;">geosop -a "POINT (654321.1234567 0.126)" --precision 2 -f wkt</span></div><div><span style="font-family: courier;">POINT (654321.12 0.13)</span></div></div><div><span style="font-family: courier;"><br /></span></div><div><div><span style="font-family: courier;">geosop -a "POINT (654321.1234567 0.126)" --precision 4 -f wkt</span></div><div><span style="font-family: courier;">POINT (654321.1235 0.126)</span></div></div><div><span style="font-family: courier;"><br /></span></div><div><div><span style="font-family: courier;">geosop -a "POINT (654321.1234567 0.126)" --precision 6 -f wkt</span></div><div><span style="font-family: courier;">POINT (654321.123457 0.126)</span></div></div></div><div><br /></div><h3>Performance Testing</h3><div>A key use case for <span style="font-family: courier;">geosop</span> is to provide easy performance testing. Performance of geometric operations is highly data-dependent. It's useful to be able to run operations over different datasets and measure performance. This allows detecting performance hotspots, and confirming the efficiency of algorithms.</div><div><br /></div><div>As a simple example of performance testing, for many years GEOS has provided optimized spatial predicates using the concept of a <a href="https://lin-ear-th-inking.blogspot.com/2007/08/preparedgeometry-efficient-batch.html"><b>prepared geometry</b></a>. Prepared geometry uses cached spatial indexes to dramatically improve performance for repeated spatial operations against a geometry. Here is a performance comparison of the <span style="font-family: courier;">intersects</span> spatial predicate in its basic and prepared form. </div><div><br /></div><div><div><span style="font-family: courier;">geosop -a world.wkt -b world.wkt -t intersects</span></div><div><span style="font-family: courier;">Ran 59,536 intersects ops ( 179,072,088 vertices)</span></div><div><span style="font-family: courier;"> -- 16,726,188 usec (GEOS 3.10.0dev)</span></div></div><div><span style="font-family: courier;"><br /></span></div><div><div><span style="font-family: courier;">geosop -a world.wkt -b world.wkt -t intersectsPrep</span></div><div><span style="font-family: courier;">Ran 59,536 intersectsPrep ops ( 179,072,088 vertices)</span></div><div><span style="font-family: courier;"> -- 1,278,348 usec (GEOS 3.10.0dev)</span></div></div><div><br /></div><div>The example of testing the world countries dataset against itself is artificial, but it shows off the dramatic <b>16x</b> performance boost provided by using a prepared operation. </div><div><br /></div><div>Another interesting use is longitudinal testing of GEOS performance across different versions of the library. For instance, here's a comparison of the performance of <span style="font-family: courier;">interiorPoint </span>between GEOS 3.7 and 3.8. The interiorPoint algorithm in GEOS 3.7 relied on overlay operations, which made it slow and sensitive to geometry invalidity. GEOS 3.8 included a major <a href="http://lin-ear-th-inking.blogspot.com/2019/02/betterfaster-interior-point-for.html">improvement</a> which greatly improved the performance, and made the algorithm more robust. Note that the dataset being used contains some (mildly) invalid geometries towards its end, which produces an error in GEOS 3.7 if the entire dataset is run. The <span style="font-family: courier;">--alimit 3800</span> option limits the number of geometries processed to avoid this issue.</div><div><br /></div><h4 style="text-align: left;">GEOS 3.7</h4><div><span style="font-family: courier; font-size: small;">geosop -a ne_10m_admin_1_states_provinces.wkb --alimit 3800 -t interiorPoint</span></div><div><span style="font-family: courier; font-size: x-small;">Ran 3,800 operations ( 1,154,703 vertices) -- 2,452,540 usec (GEOS 3.7)</span></div><div><br /></div><h4 style="text-align: left;">GEOS 3.8</h4><div><span style="font-family: courier; font-size: small;">geosop -a ne_10m_admin_1_states_provinces.wkb --alimit 3800 -t interiorPoint</span></div><div><div><span style="font-family: courier; font-size: x-small;">Ran 3,800 operations ( 1,154,703 vertices) -- 35,665 usec (GEOS 3.8)</span></div></div><div><br /></div><div>The dramatic improvement in <span style="font-family: courier;">interiorPoint</span> performance is clearly visible.</div><div><br /></div><h3 style="text-align: left;">Geoprocessing</h3><div>The <i>raison d'etre</i> of GEOS is to carry out geoprocessing. Most users will likely do this using one of the numerous <a href="https://trac.osgeo.org/geos/wiki/Applications">languages, applications and databases</a> that include GEOS. But it is still useful, convenient, and perhaps more performant to be able to process geometric data natively in GEOS. </div><div><br /></div><div>The design of <span style="font-family: courier;">geosop</span> enables more complex geoprocessing via chaining operations together using shell pipes. For example, here is a process which creates a <b>Voronoi diagram</b> of some points located in the British Isles, and then <b>clips</b> the Voronoi polygons to the outline of the islands. This also shows a few more capabilities of <span style="font-family: courier;">geosop</span>:</div><div><ul style="text-align: left;"><li>input can be supplied as WKT (or WKB) geometry literals on the command-line</li><li>input can be read from the standard input (here as WKB)</li><li>the output data is sent to the standard output, so can be directed into a file</li></ul></div><div><br /></div><div><span style="font-family: courier;">geosop </span></div><div><span style="font-family: courier;"> -a "MULTIPOINT ((1342 1227.5), (1312 1246.5), (1330 1270), (1316.5 1306.5), (1301 1323), (1298.5 1356), (1247.5 1288.5), (1237 1260))" </span></div><div><span style="font-family: courier;"> -f wkb voronoi </span></div><div><span style="font-family: courier;">| geosop -a stdin.wkb -b uk.wkt -f wkt intersection </span></div><div><span style="font-family: courier;">> uk-vor.wkt</span></div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgM-48B18euFMEFCafR0Xhah-qMv1-e76X3PmDPNF0y5jSRnDEtBoXBHAP53NYadqOFJqcVOhKxEPdNqr-C6PvoVbtYx1k2B2rR1-1a6tS5ejuRpTVH_eO6km_r5ux7wUwXNmSYH4-SFfM/s437/uk-voronoi.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="437" data-original-width="376" height="398" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgM-48B18euFMEFCafR0Xhah-qMv1-e76X3PmDPNF0y5jSRnDEtBoXBHAP53NYadqOFJqcVOhKxEPdNqr-C6PvoVbtYx1k2B2rR1-1a6tS5ejuRpTVH_eO6km_r5ux7wUwXNmSYH4-SFfM/w342-h398/uk-voronoi.png" width="342" /></a></div><br /><div><br /></div><br /><div><br /></div>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-9035086150625054012021-01-12T17:31:00.002-08:002021-01-13T10:30:00.426-08:00Introducing geosop - a CLI for GEOS<p>The <a href="https://trac.osgeo.org/geos">GEOS</a> geometry API is used by <a href="https://trac.osgeo.org/geos/wiki/Applications">many, many projects</a> to do their heavy geometric lifting. But GEOS has always had a bit of a PR problem. Most of those projects provide a more accessible interface to perform GEOS operations. Some offer a high-level language like <a href="http://trac.gispython.org/lab/wiki/Shapely">Python</a>, <a href="https://cran.r-project.org/web/packages/sf/">R</a>, or <a href="http://www.postgis.org/">SQL</a> (and these typically come with a REPL to make things even easier). Or there are GUIs like <a href="https://qgis.org/">QGIS</a>, or a command-line interface (CLI) like <a href="https://gdal.org/">GDAL/OGR</a>. </p><p>But you can't do much with GEOS on its own. It is a C/C++ library, and to use it you need to break out the compiler and start cutting code. It's essentially "headless". Even for GEOS developers, writing an entire C program just to try out a geometry operation on a dataset is painful, to say the least. </p><p>There is the GEOS <span style="font-family: courier;">XMLTester</span> utility, of course. It processes carefully structured XML files, but that is hardly convenient. (And in case this brings to mind a snide comment like "2001 called and wants its file format back", XML actually works very well in JTS and GEOS as a portable and readable format for geometry tests. But I digress.)</p><p><a href="https://github.com/locationtech/jts">JTS</a> (on which GEOS is based) has the <a href="https://github.com/locationtech/jts/blob/master/doc/JTSTestBuilder.md">TestBuilder</a> GUI, which works well for testing out and visualizing the results of JTS operations. JTS also has a CLI called <a href="https://github.com/locationtech/jts/blob/master/doc/JTSOp.md">JtsOp</a>. Writing a GUI for GEOS would be a tall order. But a command-line interface (CLI) is much simpler to code, and has significant utility. In fact there is an interesting project called <a href="https://github.com/jericks/geos-cli">geos-cli</a> that provides a simple CLI for GEOS. But it's ideal to have the CLI code as part of the GEOS project, since it ensures being up-to-date with the library code, and makes it easy to add operations to test new functionality.</p><p>This need has led to the development of <a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/util/geosop/README.md"><span style="font-family: courier;">geosop</span></a>. It is a CLI for GEOS which performs a range of useful tasks:</p><p></p><ul style="text-align: left;"><li>Run GEOS operations to confirm their semantics</li><li>Test the behaviour of GEOS on specific geometric data</li><li>Time the performance of operation execution</li><li>Profile GEOS code to find hotspots</li><li>Check memory usage characteristics of GEOS code</li><li>Generate spatial data for use in visualization or testing</li><li>Convert datasets between WKT and WKB</li></ul><ul style="text-align: left;"></ul><p></p><div><span style="font-family: courier;">geosop</span> has the following capabilities:</div><div></div><p></p><ul><li>Read WKT and WKB from files, standard input, or command-line literals</li><li>Execute GEOS operations on the list(s) of input geometries. Binary operations are executed on every pair of input geometries (i.e. the <b><a href="https://en.wikipedia.org/wiki/Join_(SQL)#Cross_join">cross join</a> </b>aka<b> <a href="https://en.wikipedia.org/wiki/Cartesian_product">Cartesian product</a>)</b></li><li>Output geometry results in WKT or WKB (or text, for non-geometric results)</li><li>Display the execution time of data input and operations</li><li>Display a full log of the command processing</li></ul><div>Here's a look at how it works. </div><div><span style="font-family: courier;"><br /></span></div><div><span style="font-family: courier;">geosop -h</span> gives a list of the options and operations available:</div><div><br /></div><div><div><span style="font-family: courier; font-size: x-small;">geosop - GEOS v. 3.10.0dev</span></div><div><span style="font-family: courier; font-size: x-small;">Executes GEOS geometry operations</span></div><div><span style="font-family: courier; font-size: x-small;">Usage:</span></div><div><span style="font-family: courier; font-size: x-small;"> geosop [OPTION...] opName opArg</span></div><div><span style="font-family: courier; font-size: x-small;"><br /></span></div><div><span style="font-family: courier; font-size: x-small;"> -a arg source for A geometries (WKT, WKB, file, stdin,</span></div><div><span style="font-family: courier; font-size: x-small;"> stdin.wkb)</span></div><div><span style="font-family: courier; font-size: x-small;"> -b arg source for B geometries (WKT, WKB, file, stdin,</span></div><div><span style="font-family: courier; font-size: x-small;"> stdin.wkb)</span></div><div><span style="font-family: courier; font-size: x-small;"> --alimit arg Limit number of A geometries read</span></div><div><span style="font-family: courier; font-size: x-small;"> -c, --collect Collect input into single geometry</span></div><div><span style="font-family: courier; font-size: x-small;"> -e, --explode Explode result</span></div><div><span style="font-family: courier; font-size: x-small;"> -f, --format arg Output format</span></div><div><span style="font-family: courier; font-size: x-small;"> -h, --help Print help</span></div><div><span style="font-family: courier; font-size: x-small;"> -p, --precision arg Sets number of decimal places in WKT output</span></div><div><span style="font-family: courier; font-size: x-small;"> -r, --repeat arg Repeat operation N times</span></div><div><span style="font-family: courier; font-size: small;"> -t, --time Print execution time</span></div><div><span style="font-family: courier; font-size: x-small;"> -v, --verbose Verbose output</span></div><div><span style="font-family: courier; font-size: x-small;"><br /></span></div><div><span style="font-family: courier; font-size: x-small;">Operations:</span></div><div><span style="font-family: courier; font-size: x-small;"> area A - computes area for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> boundary A - computes boundary for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> buffer A N - cmputes the buffer of geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> centroid A - computes centroid for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> contains A B - tests if geometry A contains geometry B</span></div><div><span style="font-family: courier; font-size: x-small;"> containsPrep A B - tests if geometry A contains geometry B, using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> containsProperlyPrep A B - tests if geometry A properly contains geometry B using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> convexHull A - computes convexHull for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> copy A - computes copy for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> covers A B - tests if geometry A covers geometry B</span></div><div><span style="font-family: courier; font-size: x-small;"> coversPrep A B - tests if geometry A covers geometry B using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> difference A B - computes difference of geometry A from B</span></div><div><span style="font-family: courier; font-size: x-small;"> differenceSR A B - computes difference of geometry A from B rounding to a precision scale factor</span></div><div><span style="font-family: courier; font-size: x-small;"> distance A B - computes distance between geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> distancePrep A B - computes distance between geometry A and B using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> envelope A - computes envelope for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> interiorPoint A - computes interiorPoint for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> intersection A B - computes intersection of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> intersectionSR A B - computes intersection of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> intersects A B - tests if geometry A and B intersect</span></div><div><span style="font-family: courier; font-size: x-small;"> intersectsPrep A B - tests if geometry A intersects B using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> isValid A - tests if geometry A is valid</span></div><div><span style="font-family: courier; font-size: x-small;"> length A - computes length for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> makeValid A - computes makeValid for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> nearestPoints A B - computes nearest points of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> nearestPointsPrep A B - computes nearest points of geometry A and B using PreparedGeometry</span></div><div><span style="font-family: courier; font-size: x-small;"> polygonize A - computes polygonize for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> reducePrecision A N - reduces precision of geometry to a precision scale factor</span></div><div><span style="font-family: courier; font-size: x-small;"> relate A B - computes DE-9IM matrix for geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> symDifference A B - computes symmetric difference of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> symDifferenceSR A B - computes symmetric difference of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> unaryUnion A - computes unaryUnion for geometry A</span></div><div><span style="font-family: courier; font-size: x-small;"> union A B - computes union of geometry A and B</span></div><div><span style="font-family: courier; font-size: x-small;"> unionSR A B - computes union of geometry A and B</span></div></div><div><br /></div><div>Most GEOS operations are provided, and the list will be completed soon.</div><div><br /></div><div>Some examples of using <span style="font-family: courier;">geosop</span> are below.</div><div><ul style="text-align: left;"><li>Compute the interior point for each country in a world polygons dataset, and output them as WKT:</li></ul></div><blockquote style="border: none; margin: 0px 0px 0px 40px; padding: 0px;"><div style="text-align: left;"><span style="font-family: courier;">geosop -a world.wkt -f wkt interiorPoint</span></div></blockquote><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJ1gEFkiOHhWqTB8LxIGWs26UpyVqp4xk4OTCwmA5Lmq1Lh_DNCs3FiPr3NLOAFlCAYqdu-CDG5YShRH68LPNLlqB6gXTAvtpxdPJTvk0CkjrmOmwCIlCrkU0LvOosw-85MQuIZTX31zk/s737/world-interiorPoint.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="366" data-original-width="737" height="208" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjJ1gEFkiOHhWqTB8LxIGWs26UpyVqp4xk4OTCwmA5Lmq1Lh_DNCs3FiPr3NLOAFlCAYqdu-CDG5YShRH68LPNLlqB6gXTAvtpxdPJTvk0CkjrmOmwCIlCrkU0LvOosw-85MQuIZTX31zk/w419-h208/world-interiorPoint.png" width="419" /></a></div><br /><div><br /></div><div><ul style="text-align: left;"><li>Determine the time required to compute buffers of distance 1 for each country in the world:</li></ul></div><blockquote style="border: none; margin: 0px 0px 0px 40px; padding: 0px;"><div style="text-align: left;"><span style="font-family: courier;">geosop -a world.wkt --time buffer 1</span></div></blockquote><div><br /></div><div><ul style="text-align: left;"><li>Compute the union of all countries in Europe:</li></ul></div><blockquote style="border: none; margin: 0px 0px 0px 40px; padding: 0px;"><div style="text-align: left;"><span style="font-family: courier;">geosop -a europe.wkb --collect -f wkb unaryUnion </span> </div></blockquote><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGgXVoLU3GNIZ0HhdhYzzP9M563UiZXSJVWS0YPGGFqL8pPHMv06-QafDdSu68tA0DxUItBc2R0e9LQKAhKHZUsQaqUPxV6SXm1SDplwuYKW17xG38NXH94ywrWnLPr2Xb5U5LvbsYyd4/s533/europe-union.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="380" data-original-width="533" height="297" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGgXVoLU3GNIZ0HhdhYzzP9M563UiZXSJVWS0YPGGFqL8pPHMv06-QafDdSu68tA0DxUItBc2R0e9LQKAhKHZUsQaqUPxV6SXm1SDplwuYKW17xG38NXH94ywrWnLPr2Xb5U5LvbsYyd4/w417-h297/europe-union.png" width="417" /></a></div><br /><p> The <a href="https://git.osgeo.org/gitea/geos/geos/src/branch/master/util/geosop/README.md">README</a> gives many more examples of how to use the various command-line options. In a subsequent post I'll give some demonstrations of using <span style="font-family: courier;">geosop</span> for various tasks including GEOS testing, performance tuning, and geoprocessing.</p><h3 style="text-align: left;">Future Work</h3><div>There's potential to make geosop even more useful: </div><div><ul style="text-align: left;"><li>GeoJSON is a popular format for use in spatial toolchains. Adding GeoJSON reading and writing would allow geosop to be more widely used for geo-processing. </li><li>Adding SVG output would provide a way to visualize the results of GEOS operations. </li><li>Improve support for performance testing by adding operations to generate various kinds of standard test datasets (such as point grids, polygon grids, and random point fields). </li></ul>And of course, work will be ongoing to keep <span style="font-family: courier;">geosop</span> up-to-date as new operations and functionality are added to GEOS.</div><div><br /></div><div><br /></div>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0tag:blogger.com,1999:blog-2420860529344694449.post-86396281493884482262020-12-23T11:56:00.003-08:002020-12-23T11:56:53.415-08:00Fixing Buffer for fixing Polygons<p>The OGC <a href="https://www.ogc.org/standards/sfa">Simple Features specification</a> implemented by <a href="https://github.com/locationtech/jts">JTS</a> has strict rules about what constitutes a valid polygonal geometry. These include:</p><p></p><ul style="text-align: left;"><li>Polygon rings must be <i>simple</i>; i.e. they may not touch or cross themselves</li><li>MultiPolygon elements may not overlap or touch at more than a finite number of points (i.e they may not intersect along an edge)</li></ul><div>These rules were chosen for good reason. They ensure that OGC-valid polygonal geometry is the simplest possible representation of an enclosed area. This greatly simplifies the evaluation of most operations on polygonal geometry, which leads to improved performance. JTS operations generally require input which is valid according to OGC rules. And they always (with some rare exceptions) emit result geometry which is OGC-valid.</div><div><br /></div><div>But data in the wild is often not this well-behaved. This creates the need to "clean" or "make valid" polygonal geometry in order to carry out operations on it. Shortly after JTS was first released we discovered a useful trick: constructing a <b>zero-width buffer</b> via <span style="font-family: courier;">geom.buffer(0)</span> converts an invalid polygonal geometry into a valid one. It can also be used as a simple way of converting "inverted" polygon topology (ESRI-style) into valid OGC topology. The reason this works is that the buffer algorithm inherently has to handle overlaps and self-intersections since they often occur during the generation of raw buffer offset curves. The algorithm nodes self-intersections, merges overlaps, and creates new polygons or holes if necessary.</div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzp5MZBqhZuEQU9JZdeHiaFAtO10hf4EsyzwGSrFhWztibWOdiCvNxcgbbeo9Ndbn3SNFl3OHUIxMl_GggEbOBV5QBvpHa9FhuBUeAndyLxl_ldy1wBofHjDp0hyphenhyphenGuzSPmfkpgSVEzPvs/s296/polygon-invalid.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="195" data-original-width="296" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzp5MZBqhZuEQU9JZdeHiaFAtO10hf4EsyzwGSrFhWztibWOdiCvNxcgbbeo9Ndbn3SNFl3OHUIxMl_GggEbOBV5QBvpHa9FhuBUeAndyLxl_ldy1wBofHjDp0hyphenhyphenGuzSPmfkpgSVEzPvs/s0/polygon-invalid.png" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>A polygon with many invalidities: overlap, self-touch in point and line, and a "bow-tie".</i></div><br /><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDf2nWan8cnW1oOIaxYohlQMslKDFkhe5z6LuxNP2GCx1DSmW8-p-od1IXC1l8N8VRhd2dcm8pPeVwRPDY5N5p15isTZG8QbbJATMRqsMn5lN8yFyoCzyH-J3uKQAwUfvsfkcGPStfmwM/s284/polygon-valid-buffer0.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="181" data-original-width="284" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiDf2nWan8cnW1oOIaxYohlQMslKDFkhe5z6LuxNP2GCx1DSmW8-p-od1IXC1l8N8VRhd2dcm8pPeVwRPDY5N5p15isTZG8QbbJATMRqsMn5lN8yFyoCzyH-J3uKQAwUfvsfkcGPStfmwM/s0/polygon-valid-buffer0.png" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: center;"><i>The polygon fixed by using <span style="font-family: courier;">buffer(0)</span><span style="font-family: inherit;">. </span></i></div><div class="separator" style="clear: both; text-align: center;"><i><span style="font-family: inherit;">Note that the bow-tie portion on the right is considered to lie in the exterior of the polygon due to ring orientation, and thus is removed.</span></i></div><div><br /></div><div>In the 20 years since the release of JTS (and its derivative <a href="https://trac.osgeo.org/geos">GEOS</a>) this trick has passed into the lore of open-source spatial data processing. It has become a recommended technique for fixing invalid polygonal geometry in numerous projects, such as <a href="https://postgis.net/workshops/postgis-intro/validity.html#st-buffer">PostGIS</a>, <a href="https://shapely.readthedocs.io/en/stable/manual.html#constructive-methods">Shapely</a>, <a href="https://github.com/rgeo/rgeo/issues/188">RGeo</a>, <a href="https://docs.geotools.org/latest/userguide/tutorial/geometry/geometrycrs.html#things-to-try">GeoTools</a>, <a href="https://www.r-spatial.org/r/2017/03/19/invalid.html#making-invalid-polygons-valid">R-sf</a> and <a href="https://issues.qgis.org/issues/9777">QGIS</a>. It's also used internally in JTS, in algorithms such as DouglasPeuckerSimplifier, VWSimplifier, and Densifier which might otherwise produce invalid polygonal results.</div><div><br /></div><div>BUT - there's a nasty little surprise lying in wait for users of <span style="font-family: courier;">buffer(0)</span>!<span style="font-family: courier;"> </span><i>It doesn't always work.</i> It turns out that the buffer algorithm has a serious flaw: in some situations involving invalid "bow-tie" topology it will discard a large part of the input geometry. This has been reported in quite a few issues (<a href="https://github.com/locationtech/jts/issues/629">here</a> and <a href="https://github.com/locationtech/jts/issues/498">here</a> in JTS, and also in GEOS and Shapely). </div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyLXWQERQoE_pkmADdyE1f77BY6OP4WEbQqD8a3j-1KT_rqvKPakMxyKvv-OouDIxGW1_LmN-RVk8uUi8zuyXhnX_5r2rfOGlNn5UJqW7kGjDD79c2WvPRBPZhwrAp3heWipAnLiOzB7M/s644/simplifyDP-buffer0-fail.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="441" data-original-width="644" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhyLXWQERQoE_pkmADdyE1f77BY6OP4WEbQqD8a3j-1KT_rqvKPakMxyKvv-OouDIxGW1_LmN-RVk8uUi8zuyXhnX_5r2rfOGlNn5UJqW7kGjDD79c2WvPRBPZhwrAp3heWipAnLiOzB7M/s320/simplifyDP-buffer0-fail.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Result of running <span style="font-family: courier;">DouglasPeuckerSimplifier</span> on a polygon with a bow-tie. (See <a href="https://github.com/locationtech/jts/issues/498">issue</a>)</i></div><div><br /></div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3N2PDm8XWYmmVdtuut4Cm8-xa7g5OSLeHnAvEZoWwlaImhDu6E6efr5afFv1BCKFE8koGX7_Gs0o2L2jx3ZLQTypmP1WauutWbI3ZRHoMJlBaWBfqRReoj4OVCGyDRtM0IPg9QTkVuuE/s480/simplifyDB-buffer0-fail-zoom.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="397" data-original-width="480" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3N2PDm8XWYmmVdtuut4Cm8-xa7g5OSLeHnAvEZoWwlaImhDu6E6efr5afFv1BCKFE8koGX7_Gs0o2L2jx3ZLQTypmP1WauutWbI3ZRHoMJlBaWBfqRReoj4OVCGyDRtM0IPg9QTkVuuE/s320/simplifyDB-buffer0-fail-zoom.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>Close-up of the result - </i><i> clearly undesirable</i><i>.</i></div><br /><div>The problem occurs because the buffer algorithm computes the orientation of rings in order to build the buffer offset curve (in the case of a zero-width buffer this is just the original ring linework). Currently the <span style="font-family: courier;">Orientation.isCCW</span> test is used to do this. This uses an efficient algorithm that determines ring orientation by checking the line segments incident on the uppermost vertex of the ring (see <a href="https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon">Wikipedia</a> for an explanation of why this works.) For a <b>valid</b> ring (where the linework does not cross itself) this works perfectly. However, in a <b>invalid</b> self-crossing ring (sometimes called a "bow-tie" or "figure-8") a choice must be made about which lobe is assigned to be the "interior". The upper-vertex approach always picks the top lobe. If that happens to be very small, the larger part of the ring is considered "exterior" and hence is removed by buffering. </div><p></p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjil2HmbltkaSinpaxhS8ri1zqh5Z0-CjwySDq-gYOahPP4GQxuvh88qrFGmN26tK83nAf7zHlPuyc5hEzfZqW7wxsbVLDF5N8dYR6MvaxbtLHxfjjafi175OeRnAUrTkYWrip78HtBCWk/s436/buffer-0-fail.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="281" data-original-width="436" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjil2HmbltkaSinpaxhS8ri1zqh5Z0-CjwySDq-gYOahPP4GQxuvh88qrFGmN26tK83nAf7zHlPuyc5hEzfZqW7wxsbVLDF5N8dYR6MvaxbtLHxfjjafi175OeRnAUrTkYWrip78HtBCWk/s320/buffer-0-fail.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>A bow-tie polygon where buffer makes the evidently wrong choice for interior.</i></div><div class="separator" style="clear: both; text-align: center;"><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgyiVXFtfOtMX3MlQGI0zK9-gxbN6G0bNT_DjTvjqeSDD5SG4jlw7jag-Vc3pyTrnGZKCOA-WEcvsAvV30vh2lQX0AjwkhDjLM7FVAAcANaIoQ1qJjONkk-sRNwz2u8Kx47zQij7ahuxk/s465/buffer-fail.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="358" data-original-width="465" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgyiVXFtfOtMX3MlQGI0zK9-gxbN6G0bNT_DjTvjqeSDD5SG4jlw7jag-Vc3pyTrnGZKCOA-WEcvsAvV30vh2lQX0AjwkhDjLM7FVAAcANaIoQ1qJjONkk-sRNwz2u8Kx47zQij7ahuxk/s320/buffer-fail.png" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><i>The problem occurs for non-zero buffer distances as well.</i></div></div><p></p><p>This problem has limped along for many years now, never being quite enough of a pain point to motivate the effort needed to find a fix (or to fund one!). And to be honest, the buffer code is some of the most complicated and delicate in JTS, and I was concerned about wading into it to add what seemed poised to be a fiddly correction.</p><p>But recently there has been renewed interest in providing a Make-Valid capability for JTS. This inspired me to revisit the usage of <span style="font-family: courier;">buffer(0)</span>, and think more deeply about ring orientation and its role in determining valid polygonal topology. And this led to discovering a surprisingly simple solution for the buffer issue.</p><p>The fix is to use an orientation test which takes into account the <i>entire</i> ring. This is provided by the <b>Signed-Area Orientation</b> test, implemented in <span style="font-family: courier;">Orientation.isCCWArea</span> using the <a href="https://en.wikipedia.org/wiki/Shoelace_formula">Shoelace Formula</a>. This effectively determines orientation based on the <b>largest area</b> enclosed by the ring. This corresponds more closely to user expectation based on visual assessment. It also minimizes the change in area and (usually) extent.</p><p>And indeed it works:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIPTB91KA_cX1LmwaOIQ0KFjnq3TN4FfUMqyeOqCxt622r_ZMoO-rw0pRNv9V0NvujSOC9RhjOwHemOB2TiaNRQeJyjhDb7nnz5pGpG2fsCEkbNnNmsJF9_yo_XoewRlAYUzcwwVcpRik/s449/buffer-0-fixed.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="300" data-original-width="449" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgIPTB91KA_cX1LmwaOIQ0KFjnq3TN4FfUMqyeOqCxt622r_ZMoO-rw0pRNv9V0NvujSOC9RhjOwHemOB2TiaNRQeJyjhDb7nnz5pGpG2fsCEkbNnNmsJF9_yo_XoewRlAYUzcwwVcpRik/s320/buffer-0-fixed.png" width="320" /></a></div><br /><p>It fixes the simplification issue nicely:</p><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIBK0xL5y10F1HanAh8bf-zrqWmmbsxks0D-i1Xiw8_gvuKVvq3uYFKMZxdKvv-eYhzlW_mu70gl4YdFrQRMEsAicH4cqZzzm6MSvdRLnjZCH0pRAxUYidGS6HlvyPzUFMd54x3Up8gCU/s665/simplifyDP-buffer0-fix.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="440" data-original-width="665" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiIBK0xL5y10F1HanAh8bf-zrqWmmbsxks0D-i1Xiw8_gvuKVvq3uYFKMZxdKvv-eYhzlW_mu70gl4YdFrQRMEsAicH4cqZzzm6MSvdRLnjZCH0pRAxUYidGS6HlvyPzUFMd54x3Up8gCU/s320/simplifyDP-buffer0-fix.png" width="320" /></a></div><br /><p>The <a href="https://github.com/locationtech/jts/pull/655">fix</a> consists of about 4 lines of actual code. To paraphrase a great orator, never in the history of JTS has so much benefit been given to so many by so few lines of code. Now <span style="font-family: courier;">buffer(0)</span> can be recommended unreservedly as an effective, performant way to fix polygonal geometry. And all those helpful documentation pages can drop any qualifications they might have.</p><p>As usual, this fix will soon show up in GEOS, and from there in PostGIS and other downstream projects. </p><p>This isn't the end of the story. There are times when the effect of <span style="font-family: courier;">buffer(0)</span> is not what is desired for fixing polygon topology. This is discussed nicely in <a href="https://obrl-soil.github.io/cleaning-polygons-internal/">this blog post</a>. The ongoing research into Make Valid will explore alternatives and how to provide an API for them.</p>Dr JTShttp://www.blogger.com/profile/02383381220154739793noreply@blogger.com0