From 771538fa8ce13be2ebf6cddbe105f8b06f95263e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 2 Mar 2023 11:35:45 -0800 Subject: [PATCH 01/79] Fix GeometryTestCase error message format Signed-off-by: Martin Davis --- modules/core/src/test/java/test/jts/GeometryTestCase.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/core/src/test/java/test/jts/GeometryTestCase.java b/modules/core/src/test/java/test/jts/GeometryTestCase.java index c2197686cf..b6faa30fe0 100644 --- a/modules/core/src/test/java/test/jts/GeometryTestCase.java +++ b/modules/core/src/test/java/test/jts/GeometryTestCase.java @@ -37,6 +37,7 @@ public abstract class GeometryTestCase extends TestCase{ private static final String CHECK_EQUAL_FAIL = "FAIL - Expected = %s -- Actual = %s\n"; + private static final String CHECK_EQUAL_FAIL_MSG = "FAIL - %s: Expected = %s -- Actual = %s\n"; final GeometryFactory geomFactory; @@ -88,7 +89,7 @@ protected void checkEqual(String msg, Geometry expected, Geometry actual) { equal = actualNorm.equalsExact(expectedNorm); } if (! equal) { - System.out.format(CHECK_EQUAL_FAIL, msg + ": ", expectedNorm, actualNorm ); + System.out.format(CHECK_EQUAL_FAIL_MSG, msg, expectedNorm, actualNorm ); } assertTrue(equal); } From c6b5d99f90c88f5a007387a592f60e42941287c5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 6 Mar 2023 17:05:36 -0800 Subject: [PATCH 02/79] Add TestBuilder Edit Grid display toggle Signed-off-by: Martin Davis --- .../jtstest/testbuilder/AppIcons.java | 1 + .../testbuilder/GeometryEditPanel.java | 6 ++++++ .../jtstest/testbuilder/TestCasePanel.java | 18 +++++++++++++++--- .../jtstest/testbuilder/DrawingGrid.png | Bin 0 -> 4646 bytes 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 modules/app/src/main/resources/org/locationtech/jtstest/testbuilder/DrawingGrid.png diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppIcons.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppIcons.java index 314de8500d..0dee505502 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppIcons.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppIcons.java @@ -29,6 +29,7 @@ public class AppIcons { public final static ImageIcon GEOM_EXCHANGE = load("ExchangeGeoms.png"); public final static ImageIcon GEOFUNC_BINARY = load("BinaryGeomFunction.png"); + public final static ImageIcon EDIT_GRID = load("DrawingGrid.png"); public final static ImageIcon DOWN = load("Down.png"); public final static ImageIcon UP = load("Up.png"); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java index 9522e268f8..61e30d54d2 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java @@ -195,6 +195,12 @@ private LayerList getLayerList() return tbModel.getLayers(); } + public void setShowingGrid(boolean isEnabled) + { + viewStyle.setGridEnabled(isEnabled); + forceRepaint(); + } + public void setShowingInput(boolean isEnabled) { if (tbModel == null) return; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java index cdbea7e396..dca7dc145f 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/TestCasePanel.java @@ -285,9 +285,8 @@ public void stateChanged(ChangeEvent e) { JCheckBox cbDisplayAB = new JCheckBox(); cbDisplayAB.setSelected(true); - cbDisplayAB.setToolTipText("Dislplay A and B"); - //cbDisplayAB.setText("Display Input"); - cbDisplayAB.addActionListener(new java.awt.event.ActionListener() { + cbDisplayAB.setToolTipText("Display A and B"); + cbDisplayAB.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { JTSTestBuilderController.editPanel().setShowingInput(cbDisplayAB.isSelected()); } @@ -295,6 +294,17 @@ public void actionPerformed(ActionEvent e) { JLabel lblDisplayAB = new JLabel(); lblDisplayAB.setIcon(AppIcons.GEOFUNC_BINARY); + JCheckBox cbDisplayGrid = new JCheckBox(); + cbDisplayGrid.setSelected(true); + cbDisplayGrid.setToolTipText("Display Grid"); + cbDisplayGrid.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + JTSTestBuilderController.editPanel().setShowingGrid(cbDisplayGrid.isSelected()); + } + }); + JLabel lblDisplayGrid = new JLabel(); + lblDisplayGrid.setIcon(AppIcons.EDIT_GRID); + cbRevealTopo.setToolTipText("Reveal Topology - visualize topological detail by stretching geometries"); spStretchDist.setToolTipText("Stretch Distance (pixels)"); spStretchDist.setMaximumSize(new Dimension(20,20)); @@ -306,6 +316,8 @@ public void actionPerformed(ActionEvent e) { jPanelReveal.add(Box.createHorizontalStrut(8)); jPanelReveal.add(cbDisplayAB); jPanelReveal.add(lblDisplayAB); + jPanelReveal.add(cbDisplayGrid); + jPanelReveal.add(lblDisplayGrid); jPanelReveal.add(Box.createHorizontalGlue()); jPanelReveal.setBorder(BorderFactory.createLoweredBevelBorder()); diff --git a/modules/app/src/main/resources/org/locationtech/jtstest/testbuilder/DrawingGrid.png b/modules/app/src/main/resources/org/locationtech/jtstest/testbuilder/DrawingGrid.png new file mode 100644 index 0000000000000000000000000000000000000000..87aec76e0dba66cb924e16c64b73b6f659155b2e GIT binary patch literal 4646 zcmai&WmFUnu=bZ+SZM*5W>LC(0f8l!lrC35x+El)?hpy-6p-!}q@}w{x;s|^2|-ps zxc<+*@8|c-nP<-V&79}V*O_Q-O=V&bEeHSr5UZ*v>i(-F|0NmzKh;_~PX+);Yy9+I zB6TginO)uA+B!JdFe80kZJ2F*9BcsqpOq?Og!_>I$=wj=0l`}LKGXztfbVhVkeJLf zDgqq=j*nrf(FE% z9+911EMlnD6soZtM9kD@tW-mEg6zffsSuQ`8$_b2vQAwP=KG5)A9nq&n>CFSzAgCm z_AIYcK=)jHZgnc^!+(D23M|aqkd^uC+Bvf7d)t>0l#tbRFaN9YS6QqU!)hOk$HTh? zg5z)7PF+?FP$jdXvB|!oAcZ+((?)jBJNy1+-|Qjot&DStaid=D%FkT#8~O%#Un*0? zqDGldkh2KV^~2g9pvRxSvu)YG5cSH970a})OoK}JezyqIXj+bXX*f$*;ITm8gkFA| z_1A!5+GDnP&R(R{ZEW_*-{1RE{Io@yPf$J>kQYsl=y>Arh*Y&a>~d*maJJ{x@lluL zHc%Um>)R?}8G=ZgSF{N1OaJ87)Ew_-qNk!~Z;azgeq#$R;$_KiZ~EL}*przrdIu)Y zCmM_MEM>;dS{4C2(A!28m=`8>x@k@Qdet{bCqe8Y&kU~TWELB-#T}Et4+@Lz*0vps z|0oTN?~ktky5akK1IYg@Bb=De^$ZdL)*YT!dLnTHw*0_(V4aKybid#%&eR=U)>cxc zakeqZb1TiXjO8{7AY@I~cgG8;M41EPb+flTWx&;1@U!fydZKE>XXvs+_umCG6 zwu*bzb-27&^B*y|r=Fa8kriGsSqa)}dxo z#VXZYl(3~Eb&OWpz;U1GvSRNSU7y?E7z2eSps1x%lu5ylmLo|^`WByPb$&05Hq1_x zSYV$ce?&*#m2DxhUDySj(dvkG%+jx`hK@V~Vix0*h;IMDucYIbVgkhILE?WR(p^gk zYcnJzKRdW;FZSTnG)tw#H+MI$**>ie1;@YUllpkl{j0CS7g(fDd@+RJ$IW~}cmaQc zOI+jI=XyvZAmL8NEbL6*YTb`)MLSWBWFWp6r&~1p2s??Z7e|5dd4)9ksAM&8n0Uwf z(;aBX%?AI9IMYU=(j62iXO=}IrH<)>rfAKFXqtnvU=~+`Dee0?(jrz zY#TcEco1j~XTE2T1kUC@l@y`Wm~kD_U0N+f*HB}ACd~8&mn(RrgFrb2gtq_ z6#NjTjbcP{7j#dVATs4hsc{x166mRkHr{|zes7AvEf>T!sHC}`YEe9;Dyzn-A=yl) zRN^=bxENj>i2UI1xVPeO4qQJefP2U(Ja2ADEUuLZ=26rT zFL>`z{;tRPLZB3Xn`qS_ITi-4!M-*o(kPzXiw#Fn>wRLuWR(Mk?LhXwx%EAj^?T9c zg|ue|;Z{lN`3W~W;4fQ*<7{RhA&OruR+?45Nn2$*WYIf3)G$5TAsY+X(~^K78V&+a zpjp~#a&}Z3@-3*xA!ZF#jB1;5e~QAUsF&GsUvQSlds>?J6R>j%74INhcf_7L+VSav z^OEndzncjLFbwmiZuvn^VoO!t50%b25xiQEM4OH~j8(|hwT1d&nX425I@Vtm_x@DS zt+kauljeF^+`M+n?JMm&ImCAXiR96V=YZK_vib0vUTTLLB(tol`9=MDp$->}6so)8 z!CL%wrbM?zf_@J3+J~}NS(5|37>I~ebP+fEWDD8YaMR~+j#_thrW^8KKM^ItnT7fWZHtK}{hlWsMcct#ZWZ0ekskqY2 zO8uZK7PE;`#Y8AfM--9*IhT9C1#}|}EGB|2EL99VyBUFfd5KW#t14O3*nRK7`(ib5 zFhV-<#I`{?harRUqA>@v`yz!-H6|-SGX)83l`33=uWyPlbWnQ+Pa*TwB%9rJ_LWjS z%4I7qj0%hG7^@1)#nnG_)<2`;CLwc-Us=>4@}6U|GF#s6dGsstP!cP~`Kps8r1M+7 zj6#X}W-tI}y=dD*V2YpUk{XOQefK~_botCp2B;9W7I9{s?HomN@y_Pn=Es_XdTFSc z(~)^2Rvh|`>d4EYcBKVJTD|lZt)%)&(e*y)e3Q9qvYIosVahkZM)L;pMjP{Q;)qaM z@UuPjpJZ+3{Ngcg94k(8?8C5S?$hMbgSROR-~9^kUK`a+)$tMbNBObp?;Z8J61g{? zh$0Yy<1_BMD!NHT1ZG^!BMyDlVS1IyqQ}t1`14LL%6Ea4jgI+MUO7}~X*9opY_7L_ z?*^)yXX?yUy(A`_E3lkiTY)7IU^&4rP1xG-;e%tG0^X4+&tRk^5w3syJQ&p$D~-k5 zTTlHa&)+01$jb(G89V3D=hw23@KP;x?)Qz1Y6}@NyL|3l-nh>9YzXs_ zvoTGD-lH~3mm_8FS8SN7&Qlh((NP*DAx;oAF`6=zdo=ACzHsSdwxy{|q0nBFN)P4- zu6Z@`1FqqQlJ1fw@l>{MJ0S+n;g=X|Sn+yllR-&M=_T)@w-C5MUu)Y~ic<$3( z67b{CZ=AF}akQ^ODyhm1VN~+5aY~7Js9HHsNkyKIl;;Q7TIY>Np@bna(YPMN5XYOJ z@I|b*k$%c>X1c5(jlwO6>2#C#Aga31SzaI$-c>G2?l>p^x*0m?{c0-dd?YzKnMxA@ z>fxoLLZ$87G&hh&;FyM6RIq%IzhP`xp@WgTzoi4-@T!ZQxA-%i+o(_GZ#2j#!yV2qe%xY_UL-(QYK2VX%t+;_|ZRsI)$ zg{=juY+}1zLjoW5ufMyKjY=E~Mdt%ViMXUiZqOad<|)$?iNO zok@)-!H;l;9D`-;IB7VLS#|b~l!d`eiI;JI!?63GI2YWK&|gYPz7DZU(|cUq&|p4B z;d1GePKi{t_S|kQW@gg@iYbDA7-%w}^6IRfdPIpuXB}h_u}!Czn-d{IlT{li-&~xH zWrC;%=5iEdyVVx$)BX~FvUzvK0S!EpJW3zbc}q)PlWx?ZI>@<5vjc__azGEghJZ&a z$h|E7#m4XRllNx-N~g?2fNAy)|Cs0w#$n8%MafBme{p=}u}gjxQab8kaX-s!O0$Ha zLzZu;z1BpbY@ltwpm{vF)5L=quQio{2^L!Y8!8iuEp1lmOA10Z4xNus15Hod0_q0j zjQr{IoZP0ayhd(~>Hn`Kjmp|w-^ss*Z!``T{jZ{=6Y!c6%!zVG~S8jnF1c6nL&9H_juuH{bF%xNTeTznkiC=~m#rcOdJN-mwILk8O zZLECzY7zV#rX|pDw*;OT!nv2TYRLZWY=vYzw6e1M=vqLb7U!>rJgw8yTdn6~0g$uI zE*9}6qH)>@A{F()C8}~1?m9&U)KIj2d${39CZjF~215F!=B}(yG7{0&RtHLh7DTU5 zBFD<7y?h6HAda%iwRmye#Iav=HqY3(b8{wMMD~S zM#jV!w;qF z-nPTN1@C%|kuiHl8-?7ifjhjd*o*8Nh3rwmtHwyYYOQv-Z1qzVi^qI z4Qg+@ZuSQfTnWv7j(3C2It!;o#eO`l1pC3sAb`K%w2wC9s=5@NyGM*zof`u_=?^Wk z$_ZKZU^233yN?zt{dE1a*-bsZdC=aNI<)}J=e9F=(M&DXK6isHTXo&SPi-Ydwdf(# zkDNa@2y$o9o>RK%&m%{l2r;5^zCs=Mxg0lAx&(4&jnZ2*JqQSSED0$TIm!zogq-^A z=T&PH3;#AJ1W#S5@97R3U_Twg=9*$|3p$gEFFiB5-CsTzae~mnAPl+MYKpZWG#R zobNwzRMnC&3uM+XWqlhGu3%maTRxIG{rTqfWK($JJ_x|m=N@azm;dSCO9kIjU0D(E z@L%M2l%@P5gsv(GkAEK*NQ93bEsZ^e8N^FL&#swk%q9a~RT z2LYG~6tmCnjdH2Cm6BiFz^br^a_m@Dl6e->(fEK6j}j>`^KMAqQr!MkvPTws6^ii3 zlBBGm>uOOg?n>+OCttTOIH!cXa;d{oLq9ej2VL`8i_sL~_uj1bwPC$iG32KT`}yCp O09ClAVzs<^*#7{rea1cj literal 0 HcmV?d00001 From 64f20134035af0770b7ab1c5a4caabce74e73db8 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 7 Mar 2023 10:09:47 -0800 Subject: [PATCH 03/79] Javadoc Signed-off-by: Martin Davis --- .../org/locationtech/jts/coverage/CoveragePolygonValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index 3d9ad07253..4642fa824d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -34,7 +34,7 @@ * with the set of polygons surrounding it. * If the polygon is coverage-valid an empty {@link LineString} is returned. * Otherwise, the result is a linear geometry containing - * the polygon boundary linework causing the invalidity. + * the target polygon boundary linework causing the invalidity. *

* A polygon is coverage-valid if: *

    From ef8e62cb5aaa4d9d548963b7d8a240f8ad94b8b9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 9 Mar 2023 08:35:20 -0800 Subject: [PATCH 04/79] Fix CoveragePolygonValidator to detect segment-equal covering polygons (#965) * Handle covering polygons * Improve detection of oriented segments Signed-off-by: Martin Davis --- .../coverage/CoveragePolygonValidator.java | 203 ++++++++++++------ .../jts/coverage/CoverageRing.java | 99 +++++---- .../jts/coverage/InvalidSegmentDetector.java | 21 ++ .../CoveragePolygonValidatorTest.java | 33 ++- .../jts/coverage/CoverageValidatorTest.java | 13 ++ 5 files changed, 254 insertions(+), 115 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java index 4642fa824d..a84c69e33a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoveragePolygonValidator.java @@ -31,7 +31,7 @@ /** * Validates that a polygon forms a valid polygonal coverage - * with the set of polygons surrounding it. + * with the set of polygons adjacent to it. * If the polygon is coverage-valid an empty {@link LineString} is returned. * Otherwise, the result is a linear geometry containing * the target polygon boundary linework causing the invalidity. @@ -46,7 +46,10 @@ * The algorithm detects the following coverage errors: *
      *
    1. the polygon is a duplicate of another one - *
    2. a polygon boundary segment is collinear with an adjacent segment but not equal to it + *
    3. a polygon boundary segment equals an adjacent segment (with same orientation). + * This determines that the polygons overlap + *
    4. a polygon boundary segment is collinear and overlaps an adjacent segment + * but is not equal to it *
    5. a polygon boundary segment touches an adjacent segment at a non-vertex point *
    6. a polygon boundary segment crosses into an adjacent polygon *
    7. a polygon boundary segment is in the interior of an adjacent polygon @@ -55,13 +58,15 @@ * If any of these errors is present, the target polygon * does not form a valid coverage with the adjacent polygons. *

      - * The validity rules does not preclude gaps between coverage polygons. + * The validity rules do not preclude properly-noded gaps between coverage polygons. * However, this class can detect narrow gaps, * by specifying a maximum gap width using {@link #setGapWidth(double)}. * Note that this will also identify narrow gaps separating disjoint coverage regions, * and narrow gores. * In some situations it may also produce false positives * (i.e. linework identified as part of a gap which is wider than the given width). + * To fully identify gaps it maybe necessary to use {@link CoverageUnion} and analyze + * the holes in the result to see if they are acceptable. *

      * A polygon may be coverage-valid with respect to * a set of surrounding polygons, but the collection as a whole may not @@ -70,6 +75,9 @@ * which are not coverage-valid relative to other ones in the set. * A coverage is valid only if every polygon in the coverage is coverage-valid. * Use {@link CoverageValidator} to validate an entire set of polygons. + *

      + * The adjacent set may contain polygons which do not intersect the target polygon. + * These are effectively ignored during validation (but may decrease performance). * * @see CoverageValidator * @@ -113,8 +121,9 @@ public static Geometry validate(Geometry targetPolygon, Geometry[] adjPolygons, private Geometry targetGeom; private double gapWidth = 0.0; private GeometryFactory geomFactory; - private IndexedPointInAreaLocator[] adjPolygonLocators; private Geometry[] adjGeoms; + private List adjPolygons; + private IndexedPointInAreaLocator[] adjPolygonLocators; /** * Create a new validator. @@ -149,37 +158,48 @@ public void setGapWidth(double gapWidth) { * @return a linear geometry containing the segments causing invalidity (if any) */ public Geometry validate() { - List adjPolygons = extractPolygons(adjGeoms); + adjPolygons = extractPolygons(adjGeoms); adjPolygonLocators = new IndexedPointInAreaLocator[adjPolygons.size()]; - if (hasDuplicateGeom(targetGeom, adjPolygons)) { - //TODO: convert to LineString copies - return targetGeom.getBoundary(); - } - List targetRings = CoverageRing.createRings(targetGeom); List adjRings = CoverageRing.createRings(adjPolygons); /** - * Mark matching segments as valid first. - * Valid segments are not considered for further checks. + * Mark matching segments first. + * Matched segments are not considered for further checks. * This improves performance substantially for mostly-valid coverages. */ Envelope targetEnv = targetGeom.getEnvelopeInternal().copy(); targetEnv.expandBy(gapWidth); - markMatchedSegments(targetRings, adjRings, targetEnv); - - //-- check if target is fully matched and thus forms a clean coverage - if (CoverageRing.isValid(targetRings)) - return createEmptyResult(); - - findInvalidInteractingSegments(targetRings, adjRings, gapWidth); - findInteriorSegments(targetRings, adjPolygons); + checkTargetRings(targetRings, adjRings, targetEnv); return createInvalidLines(targetRings); } + private void checkTargetRings(List targetRings, List adjRings, Envelope targetEnv) { + markMatchedSegments(targetRings, adjRings, targetEnv); + + /** + * Short-circuit if target is fully known (matched or invalid). + * This often happens in clean coverages, + * when the target is surrounded by matching polygons. + * It can also happen in invalid coverages + * which have polygons which are duplicates, + * or perfectly overlap other polygons. + * + */ + if (CoverageRing.isKnown(targetRings)) + return; + + /** + * Here target has at least one unmatched segment. + * Do further checks to see if any of them are are invalid. + */ + markInvalidInteractingSegments(targetRings, adjRings, gapWidth); + markInvalidInteriorSegments(targetRings, adjPolygons); + } + private static List extractPolygons(Geometry[] geoms) { List polygons = new ArrayList(); for (Geometry geom : geoms) { @@ -193,31 +213,14 @@ private Geometry createEmptyResult() { } /** - * Check if adjacent geoms contains a duplicate of the target. - * This situation is not detected by segment alignment checking, - * since all segments are matches. - - * @param geom - * @param adjPolygons - * @return - */ - private boolean hasDuplicateGeom(Geometry geom, List adjPolygons) { - for (Polygon adjPoly : adjPolygons) { - if (adjPoly.getEnvelopeInternal().equals(geom.getEnvelopeInternal())) { - if (adjPoly.equalsTopo(geom)) - return true; - } - } - return false; - } - - /** - * Marks matched segments as valid. + * Marks matched segments. * This improves the efficiency of validity testing, since in valid coverages - * all segments (except exterior ones) will be matched, + * all segments (except exterior ones) are matched, * and hence do not need to be tested further. - * In fact, the entire target polygon may be marked valid, - * which allows avoiding all further tests. + * Segments which are equal and have same orientation + * are detected and marked invalid. + * In fact, the entire target polygon may be matched and valid, + * which allows avoiding further tests. * Segments matched between adjacent polygons are also marked valid, * since this prevents them from being detected as misaligned, * if this is being done. @@ -236,7 +239,7 @@ private void markMatchedSegments(List targetRings, /** * Adds ring segments to the segment map, * and detects if they match an existing segment. - * Matched segments are marked as coverage-valid. + * Matched segments are marked. * * @param rings * @param envLimit @@ -246,57 +249,133 @@ private void markMatchedSegments(List rings, Envelope envLimit, Map segmentMap) { for (CoverageRing ring : rings) { for (int i = 0; i < ring.size() - 1; i++) { - CoverageRingSegment seg = CoverageRingSegment.create(ring, i); + Coordinate p0 = ring.getCoordinate(i); + Coordinate p1 = ring.getCoordinate(i + 1); //-- skip segments which lie outside the limit envelope - if (! envLimit.intersects(seg.p0, seg.p1)) { + if (! envLimit.intersects(p0, p1)) { continue; } - //-- if segments match, mark them valid + //-- if segment keys match, mark them as matched (or invalid) + CoverageRingSegment seg = CoverageRingSegment.create(ring, i); if (segmentMap.containsKey(seg)) { CoverageRingSegment segMatch = segmentMap.get(seg); - segMatch.markValid(); - seg.markValid(); + /** + * Since inputs should be valid, + * the segments are assumed to be in different rings. + */ + seg.match(segMatch); } else { + //-- store the segment as key and value, to allow retrieving when matched segmentMap.put(seg, seg); } } } } + /** + * Models a segment in a CoverageRing. + * The segment is normalized so it can be compared with segments + * in any orientation. + * Records valid matching segments in a coverage, + * which must have opposite orientations. + * Also detects equal segments with identical + * orientation, and marks them as coverage-invalid. + */ private static class CoverageRingSegment extends LineSegment { public static CoverageRingSegment create(CoverageRing ring, int index) { Coordinate p0 = ring.getCoordinate(index); Coordinate p1 = ring.getCoordinate(index + 1); - return new CoverageRingSegment(p0, p1, ring, index); + //-- orient segment as if ring is in canonical orientation + if (ring.isInteriorOnRight()) { + return new CoverageRingSegment(p0, p1, ring, index); + } + else { + return new CoverageRingSegment(p1, p0, ring, index); + } } - private CoverageRing ring; - private int index; + private CoverageRing ringForward = null; + private int indexForward = -1; + private CoverageRing ringOpp = null; + private int indexOpp = -1; + - public CoverageRingSegment(Coordinate p0, Coordinate p1, CoverageRing ring, int index) { + private CoverageRingSegment(Coordinate p0, Coordinate p1, CoverageRing ring, int index) { super(p0, p1); - normalize(); - this.ring = ring; - this.index = index; + + if (p1.compareTo(p0) < 0) { + reverse(); + ringOpp = ring; + indexOpp = index; + } + else { + ringForward = ring; + indexForward = index; + } } - public void markValid() { - ring.markValid(index); + public void match(CoverageRingSegment seg) { + boolean isInvalid = checkInvalid(seg); + if (isInvalid) { + return; + } + //-- record the match + if (ringForward == null) { + ringForward = seg.ringForward; + indexForward = seg.indexForward; + } + else { + ringOpp = seg.ringOpp; + indexOpp = seg.indexOpp; + } + //-- mark ring segments as matched + ringForward.markMatched(indexForward); + ringOpp.markMatched(indexOpp); + } + + private boolean checkInvalid(CoverageRingSegment seg) { + if (ringForward != null && seg.ringForward != null) { + ringForward.markInvalid(indexForward); + seg.ringForward.markInvalid(seg.indexForward); + return true; + } + if (ringOpp != null && seg.ringOpp != null) { + ringOpp.markInvalid(indexOpp); + seg.ringOpp.markInvalid(seg.indexOpp); + return true; + } + return false; } } //-------------------------------------------------- - private void findInvalidInteractingSegments(List targetRings, List adjRings, + /** + * Marks invalid target segments which cross an adjacent ring segment, + * lie partially in the interior of an adjacent ring, + * or are nearly collinear with an adjacent ring segment up to the distance tolerance + * + * @param targetRings the rings with segments to test + * @param adjRings the adjacent rings + * @param distanceTolerance the gap distance tolerance, if any + */ + private void markInvalidInteractingSegments(List targetRings, List adjRings, double distanceTolerance) { InvalidSegmentDetector detector = new InvalidSegmentDetector(distanceTolerance); MCIndexSegmentSetMutualIntersector segSetMutInt = new MCIndexSegmentSetMutualIntersector(targetRings, distanceTolerance); segSetMutInt.process(adjRings, detector); } - private void findInteriorSegments(List targetRings, List adjPolygons) { + /** + * Marks invalid target segments which are fully interior + * to an adjacent polygon. + * + * @param targetRings the rings with segments to test + * @param adjPolygons the adjacent polygons + */ + private void markInvalidInteriorSegments(List targetRings, List adjPolygons) { for (CoverageRing ring : targetRings) { for (int i = 0; i < ring.size() - 1; i++) { //-- skip check for segments with known state. @@ -337,8 +416,6 @@ private boolean isInteriorVertex(Coordinate p, List adjPolygons) { //TODO: try a spatial index? for (int i = 0; i < adjPolygons.size(); i++) { Polygon adjPoly = adjPolygons.get(i); - if (! adjPoly.getEnvelopeInternal().intersects(p)) - continue; if (polygonContainsPoint(i, adjPoly, p)) return true; @@ -347,6 +424,8 @@ private boolean isInteriorVertex(Coordinate p, List adjPolygons) { } private boolean polygonContainsPoint(int index, Polygon poly, Coordinate pt) { + if (! poly.getEnvelopeInternal().intersects(pt)) + return false; PointOnGeometryLocator pia = getLocator(index, poly); return Location.INTERIOR == pia.locate(pt); } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java index aaaa904b53..a3cbc3dfd3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java @@ -16,6 +16,7 @@ import org.locationtech.jts.algorithm.Orientation; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; @@ -49,14 +50,24 @@ private static void createRings(Polygon poly, List rings) { private static CoverageRing createRing(LinearRing ring, boolean isShell) { Coordinate[] pts = ring.getCoordinates(); + if (CoordinateArrays.hasRepeatedOrInvalidPoints(pts)) { + pts = CoordinateArrays.removeRepeatedOrInvalidPoints(pts); + } boolean isCCW = Orientation.isCCW(pts); boolean isInteriorOnRight = isShell ? ! isCCW : isCCW; return new CoverageRing(pts, isInteriorOnRight); } - public static boolean isValid(List rings) { + /** + * Tests if all rings have known status (matched or invalid) + * for all segments. + * + * @param rings a list of rings + * @return true if all ring segments have known status + */ + public static boolean isKnown(List rings) { for (CoverageRing ring : rings) { - if (! ring.isValid()) + if (! ring.isKnown()) return false; } return true; @@ -64,52 +75,70 @@ public static boolean isValid(List rings) { private boolean isInteriorOnRight; private boolean[] isInvalid; - private boolean[] isValid; + private boolean[] isMatched; - public CoverageRing(Coordinate[] pts, boolean isInteriorOnRight) { + private CoverageRing(Coordinate[] pts, boolean isInteriorOnRight) { super(pts, null); this.isInteriorOnRight = isInteriorOnRight; isInvalid = new boolean[size() - 1]; - isValid = new boolean[size() - 1]; + isMatched = new boolean[size() - 1]; } + /** + * Reports if the ring has canonical orientation, + * with the polygon interior on the right (shell is CW). + * + * @return true if the polygon interior is on the right + */ public boolean isInteriorOnRight() { return isInteriorOnRight; } + /** - * Tests if a segment is marked valid. + * Marks a segment as invalid. * - * @param index the segment index - * @return true if the segment is valid + * @param i the segment index */ - public boolean isValid(int index) { - return isValid[index]; + public void markInvalid(int i) { + isInvalid[i] = true; } /** - * Tests if a segment is marked invalid. + * Marks a segment as valid. * - * @param index the segment index - * @return true if the segment is invalid + * @param i the segment index */ - public boolean isInvalid(int index) { - return isInvalid[index]; + public void markMatched(int i) { + //if (isInvalid[i]) + // throw new IllegalStateException("Setting invalid edge to matched"); + isMatched[i] = true; } /** - * Tests whether all segments are valid. + * Tests if all segments in the ring have known status + * (matched or invalid). * - * @return true if all segments are valid + * @return true if all segments have known status */ - public boolean isValid() { - for (int i = 0; i < isValid.length; i++) { - if (! isValid[i]) + public boolean isKnown() { + for (int i = 0; i < isMatched.length; i++) { + if (! (isMatched[i] && isInvalid[i])) return false; } return true; } - + + /** + * Tests if a segment is marked invalid. + * + * @param index the segment index + * @return true if the segment is invalid + */ + public boolean isInvalid(int index) { + return isInvalid[index]; + } + /** * Tests whether all segments are invalid. * @@ -137,13 +166,13 @@ public boolean hasInvalid() { } /** - * Tests whether the validity state of a ring segment is known. + * Tests whether the matched/invalid state of a ring segment is known. * * @param i the index of the ring segment - * @return true if the segment validity state is known + * @return true if the segment state is known */ public boolean isKnown(int i) { - return isValid[i] || isInvalid[i]; + return isMatched[i] || isInvalid[i]; } /** @@ -204,28 +233,6 @@ public int next(int index) { return index + 1; return 0; } - - /** - * Marks a segment as invalid. - * - * @param i the segment index - */ - public void markInvalid(int i) { - if (isValid[i]) - throw new IllegalStateException("Setting valid edge to invalid"); - isInvalid[i] = true; - } - - /** - * Marks a segment as valid. - * - * @param i the segment index - */ - public void markValid(int i) { - if (isInvalid[i]) - throw new IllegalStateException("Setting invalid edge to valid"); - isValid[i] = true; - } public void createInvalidLines(GeometryFactory geomFactory, List lines) { //-- empty case diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/InvalidSegmentDetector.java b/modules/core/src/main/java/org/locationtech/jts/coverage/InvalidSegmentDetector.java index 0e80442fc8..1a64e4ec4d 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/InvalidSegmentDetector.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/InvalidSegmentDetector.java @@ -24,6 +24,9 @@ * must be {@link CoverageRing}s. * If an invalid situation is detected the input target segment is * marked invalid using {@link CoverageRing#markInvalid(int)}. + *

      + * This class assumes it is used with {@link SegmentSetMutualIntersector}, + * so that segments in the same ring are not evaluated. * * @author Martin Davis * @@ -53,6 +56,8 @@ public void processIntersections(SegmentString ssAdj, int iAdj, SegmentString ss // note the source of the edges is important CoverageRing target = (CoverageRing) ssTarget; CoverageRing adj = (CoverageRing) ssAdj; + + //-- Assert: rings are not equal (because this is used with SegmentSetMutualIntersector) //-- skip target segments with known status if (target.isKnown(iTarget)) return; @@ -69,6 +74,8 @@ public void processIntersections(SegmentString ssAdj, int iAdj, SegmentString ss //-- skip zero-length segments if (t0.equals2D(t1) || adj0.equals2D(adj1)) return; + if (isEqual(t0, t1, adj0, adj1)) + return; /* //-- skip segments beyond distance tolerance @@ -83,6 +90,14 @@ public void processIntersections(SegmentString ssAdj, int iAdj, SegmentString ss } } + private boolean isEqual(Coordinate t0, Coordinate t1, Coordinate adj0, Coordinate adj1) { + if (t0.equals2D(adj0) && t1.equals2D(adj1)) + return true; + if (t0.equals2D(adj1) && t1.equals2D(adj0)) + return true; + return false; + } + private boolean isInvalid(Coordinate tgt0, Coordinate tgt1, Coordinate adj0, Coordinate adj1, CoverageRing adj, int indexAdj) { @@ -149,6 +164,12 @@ private boolean isInteriorSegment(Coordinate intVertex, Coordinate tgt0, Coordin //-- find adjacent-ring vertices on either side of intersection vertex Coordinate adjPrev = adj.findVertexPrev(indexAdj, intVertex); Coordinate adjNext = adj.findVertexNext(indexAdj, intVertex); + + //-- don't check if test segment is equal to either corner segment + if (tgtEnd.equals2D(adjPrev) || tgtEnd.equals2D(adjNext)) { + return false; + } + //-- if needed, re-orient corner to have interior on right if (! adj.isInteriorOnRight()) { Coordinate temp = adjPrev; diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoveragePolygonValidatorTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoveragePolygonValidatorTest.java index a7a7e3a9de..e61dd5b29c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoveragePolygonValidatorTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoveragePolygonValidatorTest.java @@ -39,12 +39,18 @@ public void testCollinearUnmatchedEdge() { "LINESTRING (100 200, 200 200)"); } - public void testDuplicateGeometry() { + public void testDuplicate() { checkInvalid("POLYGON ((1 3, 3 3, 3 1, 1 1, 1 3))", "MULTIPOLYGON (((1 3, 3 3, 3 1, 1 1, 1 3)), ((5 3, 5 1, 3 1, 3 3, 5 3)))", "LINEARRING (1 3, 1 1, 3 1, 3 3, 1 3)"); } + public void testDuplicateReversed() { + checkInvalid("POLYGON ((1 3, 3 3, 3 1, 1 1, 1 3))", + "MULTIPOLYGON (((1 3, 1 1, 3 1, 3 3, 1 3))), ((5 3, 5 1, 3 1, 3 3, 5 3)))", + "LINEARRING (1 3, 1 1, 3 1, 3 3, 1 3)"); + } + public void testCrossingSegment() { checkInvalid("POLYGON ((1 9, 9 9, 9 3, 1 3, 1 9))", "POLYGON ((1 1, 5 6, 9 1, 1 1))", @@ -107,7 +113,7 @@ public void testBothMultiPolygon() { public void testInteriorSegmentsWithMatch() { checkInvalid("POLYGON ((7 6, 1 1, 3 6, 7 6))", "MULTIPOLYGON (((1 9, 9 9, 9 1, 1 1, 3 6, 1 9)), ((0 1, 0 9, 1 9, 3 6, 1 1, 0 1)))", - "LINESTRING (3 6, 7 6, 1 1)"); + "LINESTRING (7 6, 1 1, 3 6, 7 6)"); } public void testAdjacentHoleOverlap() { @@ -127,7 +133,25 @@ public void testFullyContained() { "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", "LINESTRING (3 7, 7 7, 7 3, 3 3, 3 7)"); } + + public void testFullyCoveredAndMatched() { + checkInvalid("POLYGON ((1 3, 2 3, 2 2, 1 2, 1 3))", + "MULTIPOLYGON (((1 1, 1 2, 2 2, 2 1, 1 1)), ((3 1, 2 1, 2 2, 3 2, 3 1)), ((3 3, 3 2, 2 2, 2 3, 3 3)), ((2 3, 3 3, 3 2, 3 1, 2 1, 1 1, 1 2, 1 3, 2 3)))", + "LINESTRING (1 2, 1 3, 2 3)"); + } + public void testTargetCoveredAndMatching() { + checkInvalid("POLYGON ((1 7, 5 7, 9 7, 9 3, 5 3, 1 3, 1 7))", + "MULTIPOLYGON (((5 9, 9 7, 5 7, 1 7, 5 9)), ((1 7, 5 7, 5 3, 1 3, 1 7)), ((9 3, 5 3, 5 7, 9 7, 9 3)), ((1 3, 5 3, 9 3, 5 1, 1 3)))", + "LINESTRING (1 7, 5 7, 9 7, 9 3, 5 3, 1 3, 1 7))"); + } + + public void testCoveredBy2AndMatching() { + checkInvalid("POLYGON ((1 9, 9 9, 9 5, 1 5, 1 9))", + "MULTIPOLYGON (((1 5, 9 5, 9 1, 1 1, 1 5)), ((1 9, 5 9, 5 1, 1 1, 1 9)), ((9 9, 9 1, 5 1, 5 9, 9 9)))", + "LINESTRING (1 5, 1 9, 9 9, 9 5)"); + } + //======== Gap cases ============================= public void testGap() { @@ -149,11 +173,6 @@ public void testRingsCCW() { "POLYGON ((1 1, 9 1, 9 4, 6 5, 1 1))"); } - public void testTargetCoveredAndMatching() { - checkValid("POLYGON ((1 7, 5 7, 9 7, 9 3, 5 3, 1 3, 1 7))", - "MULTIPOLYGON (((5 9, 9 7, 5 7, 1 7, 5 9)), ((1 7, 5 7, 5 3, 1 3, 1 7)), ((9 3, 5 3, 5 7, 9 7, 9 3)), ((1 3, 5 3, 9 3, 5 1, 1 3)))"); - } - //-- confirms zero-length segments are skipped in processing public void testRepeatedCommonVertexInTarget() { checkValid("POLYGON ((1 1, 1 3, 5 3, 5 3, 9 1, 1 1))", diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java index b7e472d7df..e52b0de9cf 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java @@ -49,6 +49,19 @@ public void testOverlappingSquares() { ); } + public void testFullyCoveredTriangles() { + checkInvalid(readArray( + "POLYGON ((1 9, 9 1, 1 1, 1 9))", + "POLYGON ((9 9, 1 9, 9 1, 9 9))", + "POLYGON ((9 9, 9 1, 1 1, 1 9, 9 9))" + ), + readArray( + "LINESTRING (9 1, 1 1, 1 9)", + "LINESTRING (9 1, 9 9, 1 9)", + "LINESTRING (9 9, 9 1, 1 1, 1 9, 9 9)") + ); + } + //======== Gap cases ============================= public void testGap() { From e730a447314570f0b15f05c8f95620e559f9578f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Michaud?= Date: Thu, 9 Mar 2023 18:48:45 +0100 Subject: [PATCH 05/79] Fix coverage simplifier issues 953 962 (#963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix #953 and #962 to preserve topology on CoverageSimplifier Signed-off-by: Michaƫl Michaud --- .../jts/coverage/CoverageEdge.java | 17 ++- .../jts/coverage/CoverageRingEdges.java | 16 ++- .../jts/coverage/CoverageSimplifier.java | 17 ++- .../jts/coverage/TPVWSimplifier.java | 57 +++++---- .../locationtech/jts/simplify/LinkedLine.java | 2 +- .../jts/coverage/CoverageSimplifierTest.java | 112 +++++++++++++++++- 6 files changed, 188 insertions(+), 33 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index acaf291f02..edb0d790b7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -26,12 +26,16 @@ class CoverageEdge { public static CoverageEdge createEdge(LinearRing ring) { Coordinate[] pts = extractEdgePoints(ring, 0, ring.getNumPoints() - 1); - return new CoverageEdge(pts); + CoverageEdge edge = new CoverageEdge(pts); + edge.constraintFree = true; + return edge; } public static CoverageEdge createEdge(LinearRing ring, int start, int end) { Coordinate[] pts = extractEdgePoints(ring, start, end); - return new CoverageEdge(pts); + CoverageEdge edge = new CoverageEdge(pts); + edge.constraintFree = false; + return edge; } private static Coordinate[] extractEdgePoints(LinearRing ring, int start, int end) { @@ -119,7 +123,8 @@ else if (i > pts.length - 1) { private Coordinate[] pts; private int ringCount = 0; - + private boolean constraintFree = true; + public CoverageEdge(Coordinate[] pts) { this.pts = pts; } @@ -131,7 +136,11 @@ public void incRingCount() { public int getRingCount() { return ringCount; } - + + public boolean isConstraintFree() { + return constraintFree; + } + public void setCoordinates(Coordinate[] pts) { this.pts = pts; } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index f24b456b5d..a82cec2a31 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateList; @@ -85,6 +86,7 @@ public List selectEdges(int ringCount) { private void build() { Set nodes = findNodes(coverage); Set boundarySegs = CoverageBoundarySegmentFinder.findBoundarySegments(coverage); + nodes.addAll(findBoundaryNodes(boundarySegs)); HashMap uniqueEdgeMap = new HashMap(); for (Geometry geom : coverage) { for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { @@ -167,7 +169,7 @@ private CoverageEdge createEdge(LinearRing ring, HashMap uniqueEdgeMap) { CoverageEdge edge; - LineSegment edgeKey = CoverageEdge.key(ring, start, end); + LineSegment edgeKey = (end == start) ? CoverageEdge.key(ring) : CoverageEdge.key(ring, start, end); if (uniqueEdgeMap.containsKey(edgeKey)) { edge = uniqueEdgeMap.get(edgeKey); } @@ -216,6 +218,18 @@ private Set findNodes(Geometry[] coverage) { return nodes; } + + private Set findBoundaryNodes(Set lineSegments) { + Map counter = new HashMap<>(); + for (LineSegment line : lineSegments) { + counter.put(line.p0, counter.getOrDefault(line.p0, 0) + 1); + counter.put(line.p1, counter.getOrDefault(line.p1, 0) + 1); + } + return counter.entrySet().stream() + .filter(e->e.getValue()>2) + .map(Map.Entry::getKey).collect(Collectors.toSet()); + } + /** * Recreates the polygon coverage from the current edge values. * diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index 1d68ddb2e1..c90000f1a7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -11,6 +11,7 @@ */ package org.locationtech.jts.coverage; +import java.util.BitSet; import java.util.List; import org.locationtech.jts.geom.Geometry; @@ -108,7 +109,7 @@ public Geometry[] simplifyInner(double tolerance) { List innerEdges = cov.selectEdges(2); List outerEdges = cov.selectEdges(1); MultiLineString constraint = createLines(outerEdges); - + simplifyEdges(innerEdges, constraint, tolerance); Geometry[] result = cov.buildCoverage(); return result; @@ -116,7 +117,8 @@ public Geometry[] simplifyInner(double tolerance) { private void simplifyEdges(List edges, MultiLineString constraints, double tolerance) { MultiLineString lines = createLines(edges); - MultiLineString linesSimp = TPVWSimplifier.simplify(lines, constraints, tolerance); + BitSet freeRingsIndices = getFreeRingIndices(edges); + MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRingsIndices, constraints, tolerance); //Assert: mlsSimp.getNumGeometries = edges.length setCoordinates(edges, linesSimp); @@ -131,10 +133,19 @@ private void setCoordinates(List edges, MultiLineString lines) { private MultiLineString createLines(List edges) { LineString lines[] = new LineString[edges.size()]; for (int i = 0; i < edges.size(); i++) { - lines[i] = geomFactory.createLineString(edges.get(i).getCoordinates()); + CoverageEdge edge = edges.get(i); + lines[i] = geomFactory.createLineString(edge.getCoordinates()); } MultiLineString mls = geomFactory.createMultiLineString(lines); return mls; } + + private BitSet getFreeRingIndices(List edges) { + BitSet freeRings = new BitSet(edges.size()); + for (int i = 0 ; i < edges.size() ; i++) { + freeRings.set(i, edges.get(i).isConstraintFree()); + } + return freeRings; + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 7eac78812c..e166fcfbbd 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -12,6 +12,7 @@ package org.locationtech.jts.coverage; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; import java.util.PriorityQueue; @@ -48,7 +49,6 @@ class TPVWSimplifier { * Simplifies a set of lines, preserving the topology of the lines. * * @param lines the lines to simplify - * @param constraints the linear constraints * @param distanceTolerance the simplification tolerance * @return the simplified lines */ @@ -67,15 +67,17 @@ public static MultiLineString simplify(MultiLineString lines, double distanceTol * @param distanceTolerance the simplification tolerance * @return the simplified lines */ - public static MultiLineString simplify(MultiLineString lines, + public static MultiLineString simplify(MultiLineString lines, BitSet freeRingsIndices, MultiLineString constraints, double distanceTolerance) { TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); + simp.setFreeRingIndices(freeRingsIndices); simp.setConstraints(constraints); MultiLineString result = (MultiLineString) simp.simplify(); return result; } private MultiLineString input; + private BitSet freeRingIndices; private double areaTolerance; private GeometryFactory geomFactory; private MultiLineString constraints = null; @@ -90,14 +92,18 @@ private void setConstraints(MultiLineString constraints) { this.constraints = constraints; } + public void setFreeRingIndices(BitSet freeRingIndices) { + this.freeRingIndices = freeRingIndices; + } + private Geometry simplify() { List edges = createEdges(input); List constraintEdges = createEdges(constraints); - + EdgeIndex edgeIndex = new EdgeIndex(); edgeIndex.add(edges); edgeIndex.add(constraintEdges); - + LineString[] result = new LineString[edges.size()]; for (int i = 0 ; i < edges.size(); i++) { Edge edge = edges.get(i); @@ -111,9 +117,11 @@ private List createEdges(MultiLineString lines) { List edges = new ArrayList(); if (lines == null) return edges; + if (freeRingIndices == null) + freeRingIndices = new BitSet(lines.getNumGeometries()); for (int i = 0 ; i < lines.getNumGeometries(); i++) { LineString line = (LineString) lines.getGeometryN(i); - edges.add(new Edge(line, areaTolerance)); + edges.add(new Edge(line, freeRingIndices.get(i), areaTolerance)); } return edges; } @@ -122,14 +130,18 @@ private static class Edge { private double areaTolerance; private LinkedLine linkedLine; private int minEdgeSize; + private boolean constraintFree; + private int nbPts; private VertexSequencePackedRtree vertexIndex; private Envelope envelope; - - Edge(LineString inputLine, double areaTolerance) { + + Edge(LineString inputLine, boolean constraintFree, double areaTolerance) { this.areaTolerance = areaTolerance; + this.constraintFree = constraintFree; this.envelope = inputLine.getEnvelopeInternal(); Coordinate[] pts = inputLine.getCoordinates(); + this.nbPts = pts.length; linkedLine = new LinkedLine(pts); minEdgeSize = linkedLine.isRing() ? 3 : 2; @@ -154,8 +166,7 @@ public int size() { private Coordinate[] simplify(EdgeIndex edgeIndex) { PriorityQueue cornerQueue = createQueue(); - - while (! cornerQueue.isEmpty() + while (! cornerQueue.isEmpty() && size() > minEdgeSize) { Corner corner = cornerQueue.poll(); //-- a corner may no longer be valid due to removal of adjacent corners @@ -174,27 +185,29 @@ && size() > minEdgeSize) { private PriorityQueue createQueue() { PriorityQueue cornerQueue = new PriorityQueue(); - for (int i = 1; i < linkedLine.size() - 1; i++) { + int minIndex = (linkedLine.isRing() && constraintFree) ? 0 : 1; + int maxIndex = nbPts - 1; + for (int i = minIndex; i < maxIndex; i++) { addCorner(i, cornerQueue); } return cornerQueue; } private void addCorner(int i, PriorityQueue cornerQueue) { - if (! linkedLine.isCorner(i)) - return; - Corner corner = new Corner(linkedLine, i); - if (corner.getArea() <= areaTolerance) { - cornerQueue.add(corner); + if (constraintFree || (i != 0 && i != nbPts-1)) { + Corner corner = new Corner(linkedLine, i); + if (corner.getArea() <= areaTolerance) { + cornerQueue.add(corner); + } } - } + } private boolean isRemovable(Corner corner, EdgeIndex edgeIndex) { Envelope cornerEnv = corner.envelope(); //-- check nearby lines for violating intersections //-- the query also returns this line for checking for (Edge edge : edgeIndex.query(cornerEnv)) { - if (hasIntersectingVertex(corner, cornerEnv, edge)) + if (hasIntersectingVertex(corner, cornerEnv, edge)) return false; //-- check if corner base equals line (2-pts) //-- if so, don't remove corner, since that would collapse to the line @@ -213,15 +226,14 @@ private boolean isRemovable(Corner corner, EdgeIndex edgeIndex) { * * @param corner the corner vertices * @param cornerEnv the envelope of the corner - * @param hull the hull to test + * @param edge the hull to test * @return true if there is an intersecting vertex */ private boolean hasIntersectingVertex(Corner corner, Envelope cornerEnv, Edge edge) { int[] result = edge.query(cornerEnv); - for (int i = 0; i < result.length; i++) { - int index = result[i]; - + for (int index : result) { + Coordinate v = edge.getCoordinate(index); // ok if corner touches another line - should only happen at endpoints if (corner.isVertex(v)) @@ -251,9 +263,10 @@ private void removeCorner(Corner corner, PriorityQueue cornerQueue) { int index = corner.getIndex(); int prev = linkedLine.prev(index); int next = linkedLine.next(index); + cornerQueue.removeIf(c -> c.getIndex() == prev || c.getIndex() == next); linkedLine.remove(index); vertexIndex.remove(index); - + //-- potentially add the new corners created addCorner(prev, cornerQueue); addCorner(next, cornerQueue); diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/LinkedLine.java b/modules/core/src/main/java/org/locationtech/jts/simplify/LinkedLine.java index 3f8077706e..a7bcd2a848 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/LinkedLine.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/LinkedLine.java @@ -50,7 +50,7 @@ private int[] createNextLinks(int size) { for (int i = 0; i < size; i++) { next[i] = i + 1; } - next[size - 1] = isRing ? 0 : size; + next[size - 1] = isRing ? 0 : NO_COORD_INDEX; return next; } diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index b6181a0f06..80aa70439b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -16,6 +16,8 @@ import junit.textui.TestRunner; import test.jts.GeometryTestCase; +import java.util.Arrays; + public class CoverageSimplifierTest extends GeometryTestCase { public static void main(String args[]) { TestRunner.run(CoverageSimplifierTest.class); @@ -97,8 +99,114 @@ public void testFilledHole() { "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (50 20, 20 30, 20 80, 60 50, 80 20, 50 20))" ), 28, readArray( - "POLYGON ((20 30, 20 80, 80 20, 50 20, 20 30))", - "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (20 30, 50 20, 80 20, 20 80, 20 30))" ) + "POLYGON ((20 30, 20 80, 80 20, 20 30))", + "POLYGON ((10 10, 10 90, 90 90, 90 10, 10 10), (20 30, 80 20, 20 80, 20 30))" ) + ); + } + + public void testTouchingHoles() { + checkResult(readArray( + "POLYGON (( 0 0, 0 11, 19 11, 19 0, 0 0 ), ( 4 5, 12 5, 12 6, 10 6, 10 8, 9 8, 9 9, 7 9, 7 8, 6 8, 6 6, 4 6, 4 5 ), ( 12 6, 14 6, 14 9, 13 9, 13 7, 12 7, 12 6 ))", + "POLYGON (( 12 6, 12 5, 4 5, 4 6, 6 6, 6 8, 7 8, 7 9, 9 9, 9 8, 10 8, 10 6, 12 6 ))", + "POLYGON (( 12 6, 12 7, 13 7, 13 9, 14 9, 14 6, 12 6 ))"), + 1.0, + readArray( + "POLYGON (( 0 0, 0 11, 19 11, 19 0, 0 0 ), ( 12 6, 10 6, 10 8, 7 9, 6 6, 4 5, 12 5, 12 6 ), ( 12 6, 14 6, 14 9, 12 6 ))", + "POLYGON (( 12 6, 10 6, 10 8, 7 9, 6 6, 4 5, 12 5, 12 6 ))", + "POLYGON (( 12 6, 14 6, 14 9, 12 6 ))" ) + ); + } + + public void testHoleTouchingShell() { + checkResultInner(readArray( + "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (170 220, 170 160, 200 140, 200 250, 170 220), (170 250, 200 250, 200 300, 170 250))", + "POLYGON ((170 220, 200 250, 200 140, 170 160, 170 220))", + "POLYGON ((170 250, 200 300, 200 250, 170 250))"), + 100.0, + readArray( + "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (200 250, 170 160, 200 140, 200 250), (200 250, 200 300, 170 250, 200 250))", + "POLYGON ((200 250, 170 160, 200 140, 200 250))", + "POLYGON ((200 250, 200 300, 170 250, 200 250))" ) + ); + } + + public void testHolesTouchingHolesAndShellInner() { + checkResultInner(readArray( + "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), + 4.0, + readArray( + "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))") + ); + } + + public void testHolesTouchingHolesAndShell() { + checkResult(readArray( + "POLYGON (( 8 5, 9 4, 9 2, 1 2, 1 4, 2 4, 2 5, 1 5, 1 8, 9 8, 9 6, 8 5 ), ( 8 5, 7 6, 6 6, 6 4, 7 4, 8 5 ), ( 7 6, 8 6, 7 7, 7 6 ), ( 6 6, 6 7, 5 6, 6 6 ), ( 6 4, 5 4, 6 3, 6 4 ), ( 7 4, 7 3, 8 4, 7 4 ))"), + 4.0, + readArray( + "POLYGON (( 1 2, 1 8, 9 8, 8 5, 9 2, 1 2 ), ( 5 4, 6 3, 6 4, 5 4 ), ( 5 6, 6 6, 6 7, 5 6 ), ( 6 4, 7 4, 8 5, 7 6, 6 6, 6 4 ), ( 7 3, 8 4, 7 4, 7 3 ), ( 7 6, 8 6, 7 7, 7 6 ))") + ); + } + + public void testMultiPolygonWithTouchingShellsInner() { + checkResultInner( + readArray( + "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))"), + 1.0, + readArray( + "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))") + ); + } + + public void testMultiPolygonWithTouchingShells() { + checkResult( + readArray( + "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))"), + 1.0, + readArray( + "MULTIPOLYGON ((( 2 7, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 1 7, 0 8, 1 8, 1 7 )), (( 1 6, 0 5, 0 6, 1 6 )), (( 2 6, 3 5, 2 5, 2 6 )))") + ); + } + + public void testTouchingShellsInner() { + checkResultInner(readArray( + "POLYGON ((0 0, 0 5, 5 6, 10 5, 10 0, 0 0))", + "POLYGON ((0 10, 5 6, 10 10, 0 10))"), + 4.0, + readArray( + "POLYGON ((0 0, 0 5, 5 6, 10 5, 10 0, 0 0))", + "POLYGON ((0 10, 5 6, 10 10, 0 10))") + ); + } + + public void testShellSimplificationAtStartingNode() { + checkResult(readArray( + "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), + 1.5, + readArray( + "POLYGON ((1 7, 5 7, 5 3, 2 3, 1 7))") + ); + } + + public void testSimplifyInnerAtStartingNode() { + checkResultInner(readArray( + "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", + "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), + 1.5, + readArray( + "POLYGON ((0 5, 0 9, 6 9, 6 2, 1 2, 0 5), (1 7, 2 3, 5 3, 5 7, 1 7))", + "POLYGON ((1 7, 5 7, 5 3, 2 3, 1 7))") + ); + } + + public void testSimplifyAllAtStartingNode() { + checkResult(readArray( + "POLYGON (( 0 5, 0 9, 6 9, 6 2, 1 2, 0 5 ), ( 1 5, 2 3, 5 3, 5 7, 1 7, 1 5 ))", + "POLYGON (( 1 5, 1 7, 5 7, 5 3, 2 3, 1 5 ))"), + 1.5, + readArray( + "POLYGON ((0 9, 6 9, 6 2, 1 2, 0 9), (1 7, 2 3, 5 3, 5 7, 1 7))", + "POLYGON ((1 7, 5 7, 5 3, 2 3, 1 7))") ); } From 3203a931a72a4116097277e6b004c3aa8791da1e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 9 Mar 2023 14:01:49 -0800 Subject: [PATCH 06/79] Javadoc, variable renaming Signed-off-by: Martin Davis --- .../jts/coverage/CoverageEdge.java | 23 ++++--- .../jts/coverage/CoverageSimplifier.java | 33 ++++++---- .../jts/coverage/TPVWSimplifier.java | 64 ++++++++++++------- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index edb0d790b7..3a186cb89f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -18,6 +18,8 @@ /** * An edge of a polygonal coverage formed from all or a section of a polygon ring. + * An edge may be a free ring, which is a ring which has not node points + * (i.e. does not touch any other rings in the parent coverage). * * @author mdavis * @@ -26,15 +28,13 @@ class CoverageEdge { public static CoverageEdge createEdge(LinearRing ring) { Coordinate[] pts = extractEdgePoints(ring, 0, ring.getNumPoints() - 1); - CoverageEdge edge = new CoverageEdge(pts); - edge.constraintFree = true; + CoverageEdge edge = new CoverageEdge(pts, true); return edge; } public static CoverageEdge createEdge(LinearRing ring, int start, int end) { Coordinate[] pts = extractEdgePoints(ring, start, end); - CoverageEdge edge = new CoverageEdge(pts); - edge.constraintFree = false; + CoverageEdge edge = new CoverageEdge(pts, false); return edge; } @@ -123,10 +123,11 @@ else if (i > pts.length - 1) { private Coordinate[] pts; private int ringCount = 0; - private boolean constraintFree = true; + private boolean isFreeRing = true; - public CoverageEdge(Coordinate[] pts) { + public CoverageEdge(Coordinate[] pts, boolean isFreeRing) { this.pts = pts; + this.isFreeRing = isFreeRing; } public void incRingCount() { @@ -137,8 +138,14 @@ public int getRingCount() { return ringCount; } - public boolean isConstraintFree() { - return constraintFree; + /** + * Returns whether this edge is a free ring; + * i.e. one with no constrained nodes. + * + * @return true if this is a free ring + */ + public boolean isFreeRing() { + return isFreeRing; } public void setCoordinates(Coordinate[] pts) { diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index c90000f1a7..03ba3f6555 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -37,16 +37,23 @@ * The simplified result coverage has the following characteristics: *

        *
      • It has the same number and types of polygonal geometries as the input - *
      • Coverage node points (inner vertices shared by three or more polygons, or boundary vertices shared by two or more) are not changed - *
      • if the input is a valid coverage, then so is the result + *
      • Node points (inner vertices shared by three or more polygons, + * or boundary vertices shared by two or more) are not changed + *
      • If the input is a valid coverage, then so is the result *
      + * This class also supports inner simplification, which simplifies + * only edges of the coverage which are adjacent to two polygons. + * This allows partial simplification of a coverage, since a simplified + * subset of a coverage still matches the remainder of the coverage. + *

      + * The input coverage should be valid according to {@link CoverageValidator}. * * @author Martin Davis */ public class CoverageSimplifier { /** - * Simplify the boundaries of a set of polygonal geometries forming a coverage, + * Simplifies the boundaries of a set of polygonal geometries forming a coverage, * preserving the coverage topology. * * @param coverage a set of polygonal geometries forming a coverage @@ -59,8 +66,9 @@ public static Geometry[] simplify(Geometry[] coverage, double tolerance) { } /** - * Simplify the inner boundaries of a set of polygonal geometries forming a coverage, + * Simplifies the inner boundaries of a set of polygonal geometries forming a coverage, * preserving the coverage topology. + * Edges which form the exterior boundary of the coverage are left unchanged. * * @param coverage a set of polygonal geometries forming a coverage * @param tolerance the simplification tolerance @@ -75,7 +83,7 @@ public static Geometry[] simplifyInner(Geometry[] coverage, double tolerance) { private GeometryFactory geomFactory; /** - * Create a new simplifier instance. + * Create a new coverage simplifier instance. * * @param coverage a set of polygonal geometries forming a coverage */ @@ -99,7 +107,8 @@ public Geometry[] simplify(double tolerance) { /** * Computes the inner-boundary simplified coverage, - * preserving the coverage topology. + * preserving the coverage topology, + * and leaving outer boundary edges unchanged. * * @param tolerance the simplification tolerance * @return the simplified polygons @@ -108,17 +117,17 @@ public Geometry[] simplifyInner(double tolerance) { CoverageRingEdges cov = CoverageRingEdges.create(input); List innerEdges = cov.selectEdges(2); List outerEdges = cov.selectEdges(1); - MultiLineString constraint = createLines(outerEdges); + MultiLineString constraintEdges = createLines(outerEdges); - simplifyEdges(innerEdges, constraint, tolerance); + simplifyEdges(innerEdges, constraintEdges, tolerance); Geometry[] result = cov.buildCoverage(); return result; } private void simplifyEdges(List edges, MultiLineString constraints, double tolerance) { MultiLineString lines = createLines(edges); - BitSet freeRingsIndices = getFreeRingIndices(edges); - MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRingsIndices, constraints, tolerance); + BitSet freeRings = getFreeRings(edges); + MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); //Assert: mlsSimp.getNumGeometries = edges.length setCoordinates(edges, linesSimp); @@ -140,10 +149,10 @@ private MultiLineString createLines(List edges) { return mls; } - private BitSet getFreeRingIndices(List edges) { + private BitSet getFreeRings(List edges) { BitSet freeRings = new BitSet(edges.size()); for (int i = 0 ; i < edges.size() ; i++) { - freeRings.set(i, edges.get(i).isConstraintFree()); + freeRings.set(i, edges.get(i).isFreeRing()); } return freeRings; } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index e166fcfbbd..0813c480e6 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -31,7 +31,9 @@ * Computes a Topology-Preserving Visvalingnam-Whyatt simplification * of a set of input lines. * The simplified lines will contain no more intersections than are present - * in the original input, and line endpoints are preserved. + * in the original input. + * Line and ring endpoints are preserved, except for rings + * which are flagged as "free". *

      * The amount of simplification is determined by a tolerance value, * which is a non-zero quantity. @@ -61,44 +63,49 @@ public static MultiLineString simplify(MultiLineString lines, double distanceTol /** * Simplifies a set of lines, preserving the topology of the lines between * themselves and a set of linear constraints. + * The endpoints of lines are preserved. + * The endpoint of rings are preserved as well, unless + * the ring is indicated as "free" via a bit flag with the same index. * * @param lines the lines to simplify - * @param constraints the linear constraints + * @param freeRings flags indicating which ring edges do not have node endpoints + * @param constraintLines the linear constraints * @param distanceTolerance the simplification tolerance * @return the simplified lines */ - public static MultiLineString simplify(MultiLineString lines, BitSet freeRingsIndices, - MultiLineString constraints, double distanceTolerance) { + public static MultiLineString simplify(MultiLineString lines, BitSet freeRings, + MultiLineString constraintLines, double distanceTolerance) { TPVWSimplifier simp = new TPVWSimplifier(lines, distanceTolerance); - simp.setFreeRingIndices(freeRingsIndices); - simp.setConstraints(constraints); + simp.setFreeRingIndices(freeRings); + simp.setConstraints(constraintLines); MultiLineString result = (MultiLineString) simp.simplify(); return result; } - private MultiLineString input; - private BitSet freeRingIndices; + private MultiLineString inputLines; + private BitSet isFreeRing; private double areaTolerance; private GeometryFactory geomFactory; - private MultiLineString constraints = null; + private MultiLineString constraintLines = null; private TPVWSimplifier(MultiLineString lines, double distanceTolerance) { - this.input = lines; + this.inputLines = lines; this.areaTolerance = distanceTolerance * distanceTolerance; - geomFactory = input.getFactory(); + geomFactory = inputLines.getFactory(); } private void setConstraints(MultiLineString constraints) { - this.constraints = constraints; + this.constraintLines = constraints; } - public void setFreeRingIndices(BitSet freeRingIndices) { - this.freeRingIndices = freeRingIndices; + public void setFreeRingIndices(BitSet isFreeRing) { + //Assert: bit set has same size as number of lines. + this.isFreeRing = isFreeRing; } private Geometry simplify() { - List edges = createEdges(input); - List constraintEdges = createEdges(constraints); + List edges = createEdges(inputLines); + List constraintEdges = createEdges(constraintLines); EdgeIndex edgeIndex = new EdgeIndex(); edgeIndex.add(edges); @@ -117,11 +124,11 @@ private List createEdges(MultiLineString lines) { List edges = new ArrayList(); if (lines == null) return edges; - if (freeRingIndices == null) - freeRingIndices = new BitSet(lines.getNumGeometries()); + if (isFreeRing == null) + isFreeRing = new BitSet(lines.getNumGeometries()); for (int i = 0 ; i < lines.getNumGeometries(); i++) { LineString line = (LineString) lines.getGeometryN(i); - edges.add(new Edge(line, freeRingIndices.get(i), areaTolerance)); + edges.add(new Edge(line, isFreeRing.get(i), areaTolerance)); } return edges; } @@ -130,15 +137,24 @@ private static class Edge { private double areaTolerance; private LinkedLine linkedLine; private int minEdgeSize; - private boolean constraintFree; + private boolean isFreeRing; private int nbPts; private VertexSequencePackedRtree vertexIndex; private Envelope envelope; - Edge(LineString inputLine, boolean constraintFree, double areaTolerance) { + /** + * Creates a new edge. + * The endpoints of the edge are preserved during simplification, + * unless it is a ring and the {@Link #isFreeRing} flag is set. + * + * @param inputLine the line or ring + * @param isFreeRing whether a ring endpoint can be removed + * @param areaTolerance the simplification tolerance + */ + Edge(LineString inputLine, boolean isFreeRing, double areaTolerance) { this.areaTolerance = areaTolerance; - this.constraintFree = constraintFree; + this.isFreeRing = isFreeRing; this.envelope = inputLine.getEnvelopeInternal(); Coordinate[] pts = inputLine.getCoordinates(); this.nbPts = pts.length; @@ -185,7 +201,7 @@ && size() > minEdgeSize) { private PriorityQueue createQueue() { PriorityQueue cornerQueue = new PriorityQueue(); - int minIndex = (linkedLine.isRing() && constraintFree) ? 0 : 1; + int minIndex = (linkedLine.isRing() && isFreeRing) ? 0 : 1; int maxIndex = nbPts - 1; for (int i = minIndex; i < maxIndex; i++) { addCorner(i, cornerQueue); @@ -194,7 +210,7 @@ private PriorityQueue createQueue() { } private void addCorner(int i, PriorityQueue cornerQueue) { - if (constraintFree || (i != 0 && i != nbPts-1)) { + if (isFreeRing || (i != 0 && i != nbPts-1)) { Corner corner = new Corner(linkedLine, i); if (corner.getArea() <= areaTolerance) { cornerQueue.add(corner); From 88d596582889d708cc66caf60731b6786fa3e178 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 12 Mar 2023 18:29:43 -0700 Subject: [PATCH 07/79] Delete TPVWSimplifier change causing infinite loop Signed-off-by: Martin Davis --- .../main/java/org/locationtech/jts/coverage/TPVWSimplifier.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 0813c480e6..28a01ba9ba 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -279,7 +279,6 @@ private void removeCorner(Corner corner, PriorityQueue cornerQueue) { int index = corner.getIndex(); int prev = linkedLine.prev(index); int next = linkedLine.next(index); - cornerQueue.removeIf(c -> c.getIndex() == prev || c.getIndex() == next); linkedLine.remove(index); vertexIndex.remove(index); From 748d29bf2cfee6bd50ee7e1d1b400e891204a6d9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 14 Mar 2023 12:55:16 -0700 Subject: [PATCH 08/79] Javadoc Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/coverage/TPVWSimplifier.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 28a01ba9ba..80f551ef95 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -28,7 +28,7 @@ import org.locationtech.jts.simplify.LinkedLine; /** - * Computes a Topology-Preserving Visvalingnam-Whyatt simplification + * Computes a Topology-Preserving Visvalingam-Whyatt simplification * of a set of input lines. * The simplified lines will contain no more intersections than are present * in the original input. @@ -40,7 +40,7 @@ * It is the square root of the area tolerance used * in the Visvalingam-Whyatt algorithm. * This equates roughly to the maximum - * distance by which a simplfied line can change from the original. + * distance by which a simplified line can change from the original. * * @author mdavis * From 4012623200913f05ebad1ede7a63771c14f4448a Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 14 Mar 2023 12:55:33 -0700 Subject: [PATCH 09/79] Fix TestBuilder layer style control updating Signed-off-by: Martin Davis --- .../jtstest/testbuilder/LayerStylePanel.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerStylePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerStylePanel.java index a1ebb1c6b5..a59194f0e5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerStylePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerStylePanel.java @@ -102,7 +102,16 @@ public void setLayer(Layer layer, boolean isModifiable) { txtName.setText(layer.getName()); txtName.setEditable(isModifiable); txtName.setFocusable(isModifiable); - + updateStyleControls(); + } + + void updateStyleControls() { + ColorControl.update(btnVertexColor, layer.getLayerStyle().getVertexColor() ); + ColorControl.update(btnLabelColor, layer.getLayerStyle().getLabelColor() ); + ColorControl.update(btnLineColor, geomStyle().getLineColor() ); + ColorControl.update(btnFillColor, geomStyle().getFillColor() ); + sliderLineAlpha.setValue(geomStyle().getLineAlpha()); + sliderFillAlpha.setValue(geomStyle().getFillAlpha()); cbShift.setSelected(layer.getLayerStyle().isShifted()); cbVertex.setSelected(layer.getLayerStyle().isVertices()); cbVertexLabel.setSelected(layer.getLayerStyle().isVertexLabels()); @@ -121,16 +130,7 @@ public void setLayer(Layer layer, boolean isModifiable) { cbSegIndex.setSelected(layer.getLayerStyle().isSegIndex()); lineWidthModel.setValue((double) geomStyle().getStrokeWidth()); setPaletteType(comboPalette, layer.getLayerStyle().getFillType()); - updateStyleControls(); - } - - void updateStyleControls() { - ColorControl.update(btnVertexColor, layer.getLayerStyle().getVertexColor() ); - ColorControl.update(btnLabelColor, layer.getLayerStyle().getLabelColor() ); - ColorControl.update(btnLineColor, geomStyle().getLineColor() ); - ColorControl.update(btnFillColor, geomStyle().getFillColor() ); - sliderLineAlpha.setValue(geomStyle().getLineAlpha()); - sliderFillAlpha.setValue(geomStyle().getFillAlpha()); + JTSTestBuilder.controller().updateLayerList(); } From 2fd0aa6c90c734cd3df05942ce443900b13ce5ea Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 15 Mar 2023 11:47:12 -0700 Subject: [PATCH 10/79] Revert name change of OffsetCurve.rawOffset Signed-off-by: Martin Davis --- .../org/locationtech/jts/operation/buffer/OffsetCurve.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index 653d9c0f7d..e22d407a7b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -226,7 +226,7 @@ private Geometry toLineString(Geometry geom) { * @param bufParams the buffer parameters to use * @return the raw offset curve points */ - public static Coordinate[] rawOffsetCurve(LineString line, double distance, BufferParameters bufParams) + public static Coordinate[] rawOffset(LineString line, double distance, BufferParameters bufParams) { Coordinate[] pts = line.getCoordinates(); Coordinate[] cleanPts = CoordinateArrays.removeRepeatedOrInvalidPoints(pts); @@ -247,7 +247,7 @@ public static Coordinate[] rawOffsetCurve(LineString line, double distance, Buff */ public static Coordinate[] rawOffset(LineString line, double distance) { - return rawOffsetCurve(line, distance, new BufferParameters()); + return rawOffset(line, distance, new BufferParameters()); } private Geometry computeCurve(LineString lineGeom, double distance) { @@ -274,7 +274,7 @@ private Geometry computeCurve(LineString lineGeom, double distance) { } private List computeSections(LineString lineGeom, double distance) { - Coordinate[] rawCurve = rawOffsetCurve(lineGeom, distance, bufferParams); + Coordinate[] rawCurve = rawOffset(lineGeom, distance, bufferParams); List sections = new ArrayList(); if (rawCurve.length == 0) { return sections; From 3bfeac1cea6209974795123dfb7c2ff3f18081c9 Mon Sep 17 00:00:00 2001 From: Felix Obermaier Date: Wed, 15 Mar 2023 20:55:07 +0100 Subject: [PATCH 11/79] Improve use of isFreeRing in createEdges (#970) --- .../locationtech/jts/coverage/TPVWSimplifier.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index 80f551ef95..c2d5725f09 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -104,8 +104,8 @@ public void setFreeRingIndices(BitSet isFreeRing) { } private Geometry simplify() { - List edges = createEdges(inputLines); - List constraintEdges = createEdges(constraintLines); + List edges = createEdges(inputLines, this.isFreeRing); + List constraintEdges = createEdges(constraintLines, null); EdgeIndex edgeIndex = new EdgeIndex(); edgeIndex.add(edges); @@ -120,15 +120,14 @@ private Geometry simplify() { return geomFactory.createMultiLineString(result); } - private List createEdges(MultiLineString lines) { + private List createEdges(MultiLineString lines, BitSet isFreeRing) { List edges = new ArrayList(); if (lines == null) return edges; - if (isFreeRing == null) - isFreeRing = new BitSet(lines.getNumGeometries()); for (int i = 0 ; i < lines.getNumGeometries(); i++) { LineString line = (LineString) lines.getGeometryN(i); - edges.add(new Edge(line, isFreeRing.get(i), areaTolerance)); + boolean isFree = isFreeRing == null ? false : isFreeRing.get(i); + edges.add(new Edge(line, isFree, areaTolerance)); } return edges; } @@ -311,4 +310,4 @@ public List query(Envelope queryEnv) { } } -} \ No newline at end of file +} From 572872a55751a7618fc25e6a156eb3cb74d70e3f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 16 Mar 2023 10:11:01 -0700 Subject: [PATCH 12/79] Simplify implementation of Envelope.disjoint Signed-off-by: Martin Davis --- .../src/main/java/org/locationtech/jts/geom/Envelope.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java b/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java index 0bcce201c1..527b057e99 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java @@ -591,11 +591,7 @@ public boolean intersects(Coordinate a, Coordinate b) { *@see #intersects(Envelope) */ public boolean disjoint(Envelope other) { - if (isNull() || other.isNull()) { return true; } - return other.minx > maxx || - other.maxx < minx || - other.miny > maxy || - other.maxy < miny; + return ! intersects(other); } /** From bd32d40e9f487b33d3d3ce8e60f943d994bc9eb6 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 19 Mar 2023 21:04:37 -0700 Subject: [PATCH 13/79] Javadoc Signed-off-by: Martin Davis --- .../src/main/java/org/locationtech/jts/geom/Envelope.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java b/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java index 527b057e99..a784bcfe14 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Envelope.java @@ -541,6 +541,8 @@ public Envelope intersection(Envelope env) /** * Tests if the region defined by other * intersects the region of this Envelope. + *

      + * A null envelope never intersects. * *@param other the Envelope which this Envelope is * being checked for intersecting @@ -584,6 +586,8 @@ public boolean intersects(Coordinate a, Coordinate b) { /** * Tests if the region defined by other * is disjoint from the region of this Envelope. + *

      + * A null envelope is always disjoint. * *@param other the Envelope being checked for disjointness *@return true if the Envelopes are disjoint From acaa06a1eb7adc05b0aa4b1e1581b829bb236f49 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Mar 2023 13:23:10 -0700 Subject: [PATCH 14/79] Fix OffsetCurve to handle zero offset distance (#971) Signed-off-by: Martin Davis --- doc/JTS_Version_History.md | 5 +++-- .../jts/operation/buffer/OffsetCurve.java | 5 +++++ .../jts/operation/buffer/OffsetCurveTest.java | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 77de9ddc4e..fc494fade0 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -39,16 +39,17 @@ Distributions for older JTS versions can be obtained at the ### Bug Fixes * Fix `PreparedGeometry` handling of EMPTY elements (#904) * Fix `WKBReader` parsing of WKB containing multiple empty elements (#905) -* Fix `LineSegment.orientationIndex(LineSegment)` to correct orientation for non-collinear segments on right (#914) +* Fix `LineSegment.orientationIndex(LineSegment)` to correct orientation for non-collinear segments on right (#914) * Fix `DepthSegment` compareTo method (#920) * Ensure `GeometryFixer` does not change coordinate dimension (#922) * Improve `ConvexHull` radial sort robustness (#927) * Improve robustness of Delaunay Triangulation frame size heuristic (#931) * Fix `PreparedLineString.intersects` to handle mixed GCs correctly (#944) * Fix `QuadEdgeSubdivision.TriangleEdgesListVisitor` (#945) -* Fix `PolygonHoleJoiner` to handle all valid inputs +* Fix `PolygonHoleJoiner` to handle all valid inputs (allows `PolygonTriangulator`, `ConstrainedDelaunayTriangulator`, and `ConcaveHullOfPolygons` to work correctly) (#946) * Fix `OffsetCurve` handling of input with repeated points (#956) +* Fix `OffsetCurve` handling zero offset distance (#971) ### Performance Improvements diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index e22d407a7b..4564fdc5c9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -38,6 +38,7 @@ * If the offset distance is positive the curve lies on the left side of the input; * if it is negative the curve is on the right side. * The curve(s) have the same direction as the input line(s). + * The result for a zero offset distance is a copy of the input linework. *

      * The offset curve is based on the boundary of the buffer for the geometry * at the offset distance (see {@link BufferOp}. @@ -256,6 +257,10 @@ private Geometry computeCurve(LineString lineGeom, double distance) { if (lineGeom.getNumPoints() < 2 || lineGeom.getLength() == 0.0) { return geomFactory.createLineString(); } + //-- zero offset distance + if (distance == 0) { + return lineGeom.copy(); + } //-- two-point line if (lineGeom.getNumPoints() == 2) { return offsetSegment(lineGeom.getCoordinates(), distance); diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java index 1e309bf580..f3ad6ae94f 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java @@ -54,6 +54,20 @@ public void testZeroLenLine() { "LINESTRING EMPTY" ); } + + public void testZeroOffsetLine() { + checkOffsetCurve( + "LINESTRING (0 0, 1 0, 1 1)", 0, + "LINESTRING (0 0, 1 0, 1 1)" + ); + } + + public void testZeroOffsetPolygon() { + checkOffsetCurve( + "POLYGON ((1 9, 9 1, 1 1, 1 9))", 0, + "LINESTRING (1 9, 1 1, 9 1, 1 9)" + ); + } /** * Test bug fix for removing repeated points in input for raw curve. From 36d44bfec4c26ffff8721d5f08306d44229174ad Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 20 Mar 2023 16:37:16 -0700 Subject: [PATCH 15/79] Improve exception message for Tri.isInteriorVertex Signed-off-by: Martin Davis --- .../main/java/org/locationtech/jts/triangulate/tri/Tri.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java index cf92d8398b..37c112fa15 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java @@ -558,6 +558,9 @@ public boolean isInteriorVertex(int index) { Tri adj = curr.getAdjacent(currIndex); if (adj == null) return false; int adjIndex = adj.getIndex(curr); + if (adjIndex < 0) { + throw new IllegalStateException("Inconsistent adjacency - invalid triangulation"); + } curr = adj; currIndex = Tri.next(adjIndex); } From 835436ec47bf1218fc1afb7e701a139d9d6f5f0d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Mar 2023 13:04:12 -0700 Subject: [PATCH 16/79] Improve Tri illegal index argument reporting Signed-off-by: Martin Davis --- .../locationtech/jts/triangulate/tri/Tri.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java index 37c112fa15..1ad3ad22ca 100755 --- a/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java +++ b/modules/core/src/main/java/org/locationtech/jts/triangulate/tri/Tri.java @@ -39,6 +39,8 @@ */ public class Tri { + private static final String INVALID_TRI_INDEX = "Invalid Tri index"; + /** * Creates a {@link GeometryCollection} of {@link Polygon}s * representing the triangles in a list. @@ -176,7 +178,7 @@ public void setTri(int edgeIndex, Tri tri) { case 1: tri1 = tri; return; case 2: tri2 = tri; return; } - Assert.shouldNeverReachHere(); + throw new IllegalArgumentException(INVALID_TRI_INDEX); } private void setCoordinates(Coordinate p0, Coordinate p1, Coordinate p2) { @@ -436,13 +438,12 @@ public boolean hasCoordinate(Coordinate v) { * @return the vertex coordinate */ public Coordinate getCoordinate(int index) { - if ( index == 0 ) { - return p0; - } - if ( index == 1 ) { - return p1; + switch(index) { + case 0: return p0; + case 1: return p1; + case 2: return p2; } - return p2; + throw new IllegalArgumentException(INVALID_TRI_INDEX); } /** @@ -491,8 +492,7 @@ public Tri getAdjacent(int index) { case 1: return tri1; case 2: return tri2; } - Assert.shouldNeverReachHere(); - return null; + throw new IllegalArgumentException(INVALID_TRI_INDEX); } /** From b40003890f4152d9aebec987133a06af0dc79c01 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 21 Mar 2023 13:18:02 -0700 Subject: [PATCH 17/79] Javadoc Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/coverage/package-info.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java b/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java index b3f081b2c2..bcbe3682ec 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java @@ -16,11 +16,14 @@ * A polygonal coverage is a non-overlapping, fully-noded set of polygons. * Specifically, a set of polygons is a valid coverage if: *

        + *
      1. The polygons are valid *
      2. The interiors of all polygons are disjoint. * This is the case if no polygon has a boundary which intersects the interior of another polygon. - *
      3. Where polygons are adjacent (their boundaries intersect), the vertices - * (and thus line segments) of the common boundary match exactly. + *
      4. Where polygons are adjacent (i.e. their boundaries intersect), + * they are edge-matched: the vertices + * (and thus line segments) of the common boundary section match exactly. *
      + * A coverage may contain holes and disjoint regions. *

      * Coverage algorithms (such as {@link CoverageUnion}) * generally require the input coverage to be valid to produce correct results. From dc11e0976718d076b8c66251ad8c5c660123318e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 30 Mar 2023 13:08:17 -0700 Subject: [PATCH 18/79] Add LargestEmptyCircle boundary parameter (#973) Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 20 +-- .../construct/LargestEmptyCircle.java | 134 ++++++++++++------ .../construct/LargestEmptyCircleTest.java | 49 ++++++- 3 files changed, 148 insertions(+), 55 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index a8cc5f2b78..52fa580b2a 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -86,27 +86,27 @@ public static double maximumInscribedCircleRadiusLen(Geometry g, //-------------------------------------------- @Metadata(description="Constructs the Largest Empty Circle in a set of obstacles") - public static Geometry largestEmptyCircle(Geometry g, - @Metadata(title="Distance tolerance") + public static Geometry largestEmptyCircle(Geometry obstacles, Geometry boundary, + @Metadata(title="Accuracy distance tolerance") double tolerance) { - LineString radiusLine = LargestEmptyCircle.getRadiusLine(g, tolerance); + LineString radiusLine = LargestEmptyCircle.getRadiusLine(obstacles, boundary, tolerance); return circleByRadiusLine(radiusLine, 60); } @Metadata(description="Computes a radius line of the Largest Empty Circle in a set of obstacles") - public static Geometry largestEmptyCircleCenter(Geometry g, - @Metadata(title="Distance tolerance") + public static Geometry largestEmptyCircleCenter(Geometry obstacles, Geometry boundary, + @Metadata(title="Accuracy distance tolerance") double tolerance) { - return LargestEmptyCircle.getCenter(g, tolerance); + return LargestEmptyCircle.getCenter(obstacles, boundary, tolerance); } @Metadata(description="Computes a radius line of the Largest Empty Circle in a set of obstacles") - public static Geometry largestEmptyCircleRadius(Geometry g, - @Metadata(title="Distance tolerance") + public static Geometry largestEmptyCircleRadius(Geometry obstacles, Geometry boundary, + @Metadata(title="Accuracy distance tolerance") double tolerance) { - return LargestEmptyCircle.getRadiusLine(g, tolerance); + return LargestEmptyCircle.getRadiusLine(obstacles, boundary, tolerance); } - + //-------------------------------------------- @Metadata(description="Constructs an n-point circle from a 2-point line giving the radius") diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java index 4f35f975b5..0296a1b17e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java @@ -23,20 +23,29 @@ import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Location; import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygonal; import org.locationtech.jts.operation.distance.IndexedFacetDistance; /** * Constructs the Largest Empty Circle for a set - * of obstacle geometries, up to a specified tolerance. + * of obstacle geometries, up to a given accuracy distance tolerance. * The obstacles are point and line geometries. + * (Polygonal obstacles may be supplied, but only their boundaries are used.) *

      - * The Largest Empty Circle is the largest circle which - * has its center in the convex hull of the obstacles (the boundary), - * and whose interior does not intersect with any obstacle. + * The Largest Empty Circle (LEC) is the largest circle + * whose interior does not intersect with any obstacle + * and whose center lies within a polygonal boundary. * The circle center is the point in the interior of the boundary - * which has the farthest distance from the obstacles (up to tolerance). - * The circle is determined by the center point - * and a point lying on an obstacle indicating the circle radius. + * which has the farthest distance from the obstacles + * (up to the accuracy of the distance tolerance). + * The circle itself is determined by the center point + * and a point lying on an obstacle determining the circle radius. + *

      + * The polygonal boundary may be supplied explicitly. + * If it is not specified the convex hull of the obstacles is used as the boundary. + *

      + * To compute an LEC which lies wholly within + * a polygonal boundary, include the boundary polygon as an obstacle as well. *

      * The implementation uses a successive-approximation technique * over a grid of square cells covering the obstacles and boundary. @@ -47,10 +56,10 @@ *

      Future Enhancements

      *
        *
      • Support polygons as obstacles - *
      • Support a client-defined boundary polygon *
      * * @author Martin Davis + * * @see MaximumInscribedCircle * @see InteriorPoint * @see Centroid @@ -59,38 +68,73 @@ public class LargestEmptyCircle { /** * Computes the center point of the Largest Empty Circle - * within a set of obstacles, up to a given tolerance distance. + * interior-disjoint to a set of obstacles, + * with accuracy to a given tolerance distance. + * The center of the LEC lies within the convex hull of the obstacles. * * @param obstacles a geometry representing the obstacles (points and lines) * @param tolerance the distance tolerance for computing the center point * @return the center point of the Largest Empty Circle */ public static Point getCenter(Geometry obstacles, double tolerance) { - LargestEmptyCircle lec = new LargestEmptyCircle(obstacles, tolerance); - return lec.getCenter(); + return getCenter(obstacles, null, tolerance); } + /** + * Computes the center point of the Largest Empty Circle + * interior-disjoint to a set of obstacles and within a polygonal boundary, + * with accuracy to a given tolerance distance. + * The center of the LEC lies within the boundary. + * + * @param obstacles a geometry representing the obstacles (points and lines) + * @param boundary a polygonal geometry to contain the LEC center + * @param tolerance the distance tolerance for computing the center point + * @return the center point of the Largest Empty Circle + */ + public static Point getCenter(Geometry obstacles, Geometry boundary, double tolerance) { + LargestEmptyCircle lec = new LargestEmptyCircle(obstacles, boundary, tolerance); + return lec.getCenter(); + } + /** * Computes a radius line of the Largest Empty Circle - * within a set of obstacles, up to a given distance tolerance. + * interior-disjoint to a set of obstacles, + * with accuracy to a given tolerance distance. + * The center of the LEC lies within the convex hull of the obstacles. * * @param obstacles a geometry representing the obstacles (points and lines) * @param tolerance the distance tolerance for computing the center point * @return a line from the center of the circle to a point on the edge */ public static LineString getRadiusLine(Geometry obstacles, double tolerance) { - LargestEmptyCircle lec = new LargestEmptyCircle(obstacles, tolerance); + return getRadiusLine(obstacles, null, tolerance); + } + + /** + * Computes a radius line of the Largest Empty Circle + * interior-disjoint to a set of obstacles and within a polygonal boundary, + * with accuracy to a given tolerance distance. + * The center of the LEC lies within the boundary. + * + * @param obstacles a geometry representing the obstacles (points and lines) + * @param boundary a polygonal geometry to contain the LEC center + * @param tolerance the distance tolerance for computing the center point + * @return a line from the center of the circle to a point on the edge + */ + public static LineString getRadiusLine(Geometry obstacles, Geometry boundary, double tolerance) { + LargestEmptyCircle lec = new LargestEmptyCircle(obstacles, boundary, tolerance); return lec.getRadiusLine(); } private Geometry obstacles; + private Geometry boundary; private double tolerance; private GeometryFactory factory; - private Geometry boundary; private IndexedPointInAreaLocator ptLocater; private IndexedFacetDistance obstacleDistance; private IndexedFacetDistance boundaryDistance; + private Envelope gridEnv; private Cell farthestCell; private Cell centerCell = null; @@ -100,37 +144,30 @@ public static LineString getRadiusLine(Geometry obstacles, double tolerance) { private Point radiusPoint = null; /** - * Creates a new instance of a Largest Empty Circle construction. + * Creates a new instance of a Largest Empty Circle construction, + * interior-disjoint to a set of obstacle geometries and within a polygonal boundary. + * If the boundary is null or empty the convex hull + * of the obstacles is used as the boundary. * - * @param obstacles a geometry representing the obstacles (points and lines) - * @param tolerance the distance tolerance for computing the circle center point + * @param obstacles a non-empty geometry representing the obstacles (points and lines) + * @param boundary a polygonal geometry (may be null or empty) + * @param tolerance a distance tolerance for computing the circle center point (a positive value) */ - public LargestEmptyCircle(Geometry obstacles, double tolerance) { - if (obstacles.isEmpty()) { - throw new IllegalArgumentException("Empty obstacles geometry is not supported"); + public LargestEmptyCircle(Geometry obstacles, Geometry boundary, double tolerance) { + if (obstacles == null || obstacles.isEmpty()) { + throw new IllegalArgumentException("Obstacles geometry is empty or null"); + } + if (boundary != null && ! (boundary instanceof Polygonal)) { + throw new IllegalArgumentException("Boundary must be polygonal"); + } + if (tolerance <= 0) { + throw new IllegalArgumentException("Accuracy tolerance is non-positive: " + tolerance); } - this.obstacles = obstacles; + this.boundary = boundary; this.factory = obstacles.getFactory(); this.tolerance = tolerance; obstacleDistance = new IndexedFacetDistance( obstacles ); - setBoundary(obstacles); - } - - /** - * Sets the area boundary as the convex hull - * of the obstacles. - * - * @param obstacles - */ - private void setBoundary(Geometry obstacles) { - // TODO: allow this to be set by client as arbitrary polygon - this.boundary = obstacles.convexHull(); - // if boundary does not enclose an area cannot create a ptLocater - if (boundary.getDimension() >= 2) { - ptLocater = new IndexedPointInAreaLocator(boundary); - boundaryDistance = new IndexedFacetDistance( boundary ); - } } /** @@ -197,7 +234,23 @@ private double distanceToConstraints(double x, double y) { return distanceToConstraints(pt); } + private void initBoundary() { + Geometry bounds = this.boundary; + if (bounds == null || bounds.isEmpty()) { + bounds = obstacles.convexHull(); + } + //-- the centre point must be in the extent of the boundary + gridEnv = bounds.getEnvelopeInternal(); + // if bounds does not enclose an area cannot create a ptLocater + if (bounds.getDimension() >= 2) { + ptLocater = new IndexedPointInAreaLocator( bounds ); + boundaryDistance = new IndexedFacetDistance( bounds ); + } + } + private void compute() { + initBoundary(); + // check if already computed if (centerCell != null) return; @@ -214,7 +267,8 @@ private void compute() { // Priority queue of cells, ordered by decreasing distance from constraints PriorityQueue cellQueue = new PriorityQueue<>(); - createInitialGrid(obstacles.getEnvelopeInternal(), cellQueue); + //-- grid covers extent of obstacles and boundary (if any) + createInitialGrid(gridEnv, cellQueue); // use the area centroid as the initial candidate center point farthestCell = createCentroidCell(obstacles); @@ -260,7 +314,7 @@ private void compute() { radiusPt = nearestPts[0].copy(); radiusPoint = factory.createPoint(radiusPt); } - + /** * Tests whether a cell may contain the circle center, * and thus should be refined (split into subcells diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java index 8b8047a693..318b86a351 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java @@ -60,15 +60,54 @@ public void testLineFlat() { 0.01 ); } + //--------------------------------------------------------- + // Obstacles and Boundary - private void checkCircle(String wkt, double tolerance, + public void testBoundaryEmpty() { + checkCircle("MULTIPOINT ((2 2), (8 8), (7 5))", + "POLYGON EMPTY", + 0.01, 4.127, 4.127, 3 ); + } + + public void testBoundarySquare() { + checkCircle("MULTIPOINT ((2 2), (6 4), (8 8))", + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", + 0.01, 1.00390625, 8.99609375, 7.065 ); + } + + public void testBoundarySquareObstaclesOutside() { + checkCircle("MULTIPOINT ((10 10), (10 0))", + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", + 0.01, 1.0044, 4.997, 10.29 ); + } + + public void testBoundaryMultiSquares() { + checkCircle("MULTIPOINT ((10 10), (10 0), (5 5))", + "MULTIPOLYGON (((1 9, 9 9, 9 1, 1 1, 1 9)), ((15 20, 20 20, 20 15, 15 15, 15 20)))", + 0.01, 19.995, 19.997, 14.137 ); + } + + public void testBoundaryAsObstacle() { + checkCircle("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POINT (4 3), POINT (7 6))", + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", + 0.01, 4, 6, 3 ); + } + + //======================================================== + + private void checkCircle(String wktObstacles, double tolerance, + double x, double y, double expectedRadius) { + checkCircle(read(wktObstacles), null, tolerance, x, y, expectedRadius); + } + + private void checkCircle(String wktObstacles, String wktBoundary, double tolerance, double x, double y, double expectedRadius) { - checkCircle(read(wkt), tolerance, x, y, expectedRadius); + checkCircle(read(wktObstacles), read(wktBoundary), tolerance, x, y, expectedRadius); } - private void checkCircle(Geometry geom, double tolerance, + private void checkCircle(Geometry obstacles, Geometry boundary, double tolerance, double x, double y, double expectedRadius) { - LargestEmptyCircle lec = new LargestEmptyCircle(geom, tolerance); + LargestEmptyCircle lec = new LargestEmptyCircle(obstacles, boundary, tolerance); Geometry centerPoint = lec.getCenter(); Coordinate centerPt = centerPoint.getCoordinate(); Coordinate expectedCenter = new Coordinate(x, y); @@ -88,7 +127,7 @@ private void checkCircleZeroRadius(String wkt, double tolerance) { } private void checkCircleZeroRadius(Geometry geom, double tolerance) { - LargestEmptyCircle lec = new LargestEmptyCircle(geom, tolerance); + LargestEmptyCircle lec = new LargestEmptyCircle(geom, null, tolerance); LineString radiusLine = lec.getRadiusLine(); double actualRadius = radiusLine.getLength(); From 3f2e2ec4381c498c1235926f264abd5060226514 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 30 Mar 2023 13:09:55 -0700 Subject: [PATCH 19/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index fc494fade0..017810d719 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -35,6 +35,7 @@ Distributions for older JTS versions can be obtained at the * Improve `TopologyPreservingSimplifier` to prevent edge-disjoint line collapse (#925) * Improve `OffsetCurve` to return more linework for some input situations (#956) * Reduce buffer curve short fillet segments (#960) +* Added ability to specify boundary for `LargestEmptyCircle` (#973) ### Bug Fixes * Fix `PreparedGeometry` handling of EMPTY elements (#904) From ebf1e8ce47acccf91f97dea97441be78e3a0a7e4 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 10:49:52 -0700 Subject: [PATCH 20/79] Add TPVWSimplifier unit test Signed-off-by: Martin Davis --- .../jts/coverage/TPVWSimplifierTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java new file mode 100644 index 0000000000..6a20b62d09 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -0,0 +1,42 @@ +package org.locationtech.jts.coverage; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiLineString; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class TPVWSimplifierTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(TPVWSimplifierTest.class); + } + + public TPVWSimplifierTest(String name) { + super(name); + } + + public void testSimpleNoop() { + checkNoop("MULTILINESTRING ((9 9, 3 9, 1 4, 4 1, 9 1), (9 1, 2 4, 9 9))", + 2); + } + + public void testSimple() { + checkSimplify("MULTILINESTRING ((9 9, 3 9, 1 4, 4 1, 9 1), (9 1, 6 3, 2 4, 5 7, 9 9))", + 2, + "MULTILINESTRING ((9 9, 3 9, 1 4, 4 1, 9 1), (9 1, 2 4, 9 9))"); + } + + private void checkNoop(String wkt, double tolerance) { + MultiLineString geom = (MultiLineString) read(wkt); + Geometry actual = TPVWSimplifier.simplify(geom, tolerance); + checkEqual(geom, actual); + } + + private void checkSimplify(String wkt, double tolerance, String wktExpected) { + MultiLineString geom = (MultiLineString) read(wkt); + Geometry actual = TPVWSimplifier.simplify(geom, tolerance); + Geometry expected = read(wktExpected); + checkEqual(expected, actual); + } + +} From e7aff2e71c0c42d47aa0f1f53a4377aff5f45156 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 13:13:12 -0700 Subject: [PATCH 21/79] Add TPVWSimplifier unit tests Signed-off-by: Martin Davis --- .../jts/coverage/TPVWSimplifier.java | 2 +- .../jts/coverage/TPVWSimplifierTest.java | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index c2d5725f09..fd8a11f8af 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -69,7 +69,7 @@ public static MultiLineString simplify(MultiLineString lines, double distanceTol * * @param lines the lines to simplify * @param freeRings flags indicating which ring edges do not have node endpoints - * @param constraintLines the linear constraints + * @param constraintLines the linear constraints (may be null) * @param distanceTolerance the simplification tolerance * @return the simplified lines */ diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index 6a20b62d09..1b6bf1a269 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -1,5 +1,7 @@ package org.locationtech.jts.coverage; +import java.util.BitSet; + import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.MultiLineString; @@ -26,6 +28,20 @@ public void testSimple() { "MULTILINESTRING ((9 9, 3 9, 1 4, 4 1, 9 1), (9 1, 2 4, 9 9))"); } + public void testFreeRing() { + checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (7 5, 8 8, 2 8, 2 2, 8 2, 7 5))", + new int[] { 2 }, + 2, + "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (8 8, 2 8, 2 2, 8 2, 8 8))"); + } + + public void testNoFreeRing() { + checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (5 5, 4 8, 2 8, 2 2, 4 2, 5 5), (5 5, 6 8, 8.1 8, 8 8, 8 2, 6 2, 5 5))", + new int[] { }, + 2, + "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (5 5, 2 2, 2 8, 5 5), (5 5, 8 2, 8 8, 5 5))"); + } + private void checkNoop(String wkt, double tolerance) { MultiLineString geom = (MultiLineString) read(wkt); Geometry actual = TPVWSimplifier.simplify(geom, tolerance); @@ -39,4 +55,24 @@ private void checkSimplify(String wkt, double tolerance, String wktExpected) { checkEqual(expected, actual); } + private void checkSimplify(String wkt, int[] freeRingIndex, + double tolerance, String wktExpected) { + checkSimplify(wkt, freeRingIndex, null, tolerance, wktExpected); + } + + private void checkSimplify(String wkt, int[] freeRingIndex, + String wktConstraints, + double tolerance, String wktExpected) { + MultiLineString lines = (MultiLineString) read(wkt); + BitSet freeRings = new BitSet(); + for (int index : freeRingIndex) { + freeRings.set(index); + } + MultiLineString constraints = wktConstraints == null ? null + : (MultiLineString) read(wktConstraints); + Geometry actual = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); + Geometry expected = read(wktExpected); + checkEqual(expected, actual); + } + } From 67c5bdf228f2318679226dfe04321c16ba39cd01 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 14:16:02 -0700 Subject: [PATCH 22/79] Add TPVWSimplifer unit test Signed-off-by: Martin Davis --- .../org/locationtech/jts/coverage/TPVWSimplifierTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index 1b6bf1a269..ed506accf5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -42,6 +42,14 @@ public void testNoFreeRing() { "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (5 5, 2 2, 2 8, 5 5), (5 5, 8 2, 8 8, 5 5))"); } + public void testConstraint() { + checkSimplify("MULTILINESTRING ((6 8, 2 8, 2.1 5, 2 2, 6 2, 5.9 5, 6 8))", + new int[] { }, + "MULTILINESTRING ((1 9, 9 9, 6 5, 9 1), (1 9, 1 1, 9 1))", + 1, + "MULTILINESTRING ((6 8, 2 8, 2 2, 6 2, 5.9 5, 6 8))"); + } + private void checkNoop(String wkt, double tolerance) { MultiLineString geom = (MultiLineString) read(wkt); Geometry actual = TPVWSimplifier.simplify(geom, tolerance); From 2c2f5456adfa6d34655186b04417d5d3826df319 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 16:10:52 -0700 Subject: [PATCH 23/79] Add CoverageRingEdges unit test Signed-off-by: Martin Davis --- .../jts/coverage/CoverageEdge.java | 19 +++++++ .../jts/coverage/CoverageSimplifier.java | 15 +----- .../jts/coverage/CoverageRingEdgesTest.java | 53 +++++++++++++++++++ .../jts/coverage/CoverageSimplifierTest.java | 2 - 4 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index 3a186cb89f..0e5cedb074 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -11,9 +11,14 @@ */ package org.locationtech.jts.coverage; +import java.util.List; + import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; +import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.io.WKTWriter; /** @@ -38,6 +43,16 @@ public static CoverageEdge createEdge(LinearRing ring, int start, int end) { return edge; } + static MultiLineString createLines(List edges, GeometryFactory geomFactory) { + LineString lines[] = new LineString[edges.size()]; + for (int i = 0; i < edges.size(); i++) { + CoverageEdge edge = edges.get(i); + lines[i] = edge.toLineString(geomFactory); + } + MultiLineString mls = geomFactory.createMultiLineString(lines); + return mls; + } + private static Coordinate[] extractEdgePoints(LinearRing ring, int start, int end) { int size = start < end ? end - start + 1 @@ -164,6 +179,10 @@ public Coordinate getStartCoordinate() { return pts[0]; } + public LineString toLineString(GeometryFactory geomFactory) { + return geomFactory.createLineString(getCoordinates()); + } + public String toString() { return WKTWriter.toLineString(pts); } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index 03ba3f6555..9b3a86f0b3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -16,7 +16,6 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; /** @@ -117,7 +116,7 @@ public Geometry[] simplifyInner(double tolerance) { CoverageRingEdges cov = CoverageRingEdges.create(input); List innerEdges = cov.selectEdges(2); List outerEdges = cov.selectEdges(1); - MultiLineString constraintEdges = createLines(outerEdges); + MultiLineString constraintEdges = CoverageEdge.createLines(outerEdges, geomFactory); simplifyEdges(innerEdges, constraintEdges, tolerance); Geometry[] result = cov.buildCoverage(); @@ -125,7 +124,7 @@ public Geometry[] simplifyInner(double tolerance) { } private void simplifyEdges(List edges, MultiLineString constraints, double tolerance) { - MultiLineString lines = createLines(edges); + MultiLineString lines = CoverageEdge.createLines(edges, geomFactory); BitSet freeRings = getFreeRings(edges); MultiLineString linesSimp = TPVWSimplifier.simplify(lines, freeRings, constraints, tolerance); //Assert: mlsSimp.getNumGeometries = edges.length @@ -139,16 +138,6 @@ private void setCoordinates(List edges, MultiLineString lines) { } } - private MultiLineString createLines(List edges) { - LineString lines[] = new LineString[edges.size()]; - for (int i = 0; i < edges.size(); i++) { - CoverageEdge edge = edges.get(i); - lines[i] = geomFactory.createLineString(edge.getCoordinates()); - } - MultiLineString mls = geomFactory.createMultiLineString(lines); - return mls; - } - private BitSet getFreeRings(List edges) { BitSet freeRings = new BitSet(edges.size()); for (int i = 0 ; i < edges.size() ; i++) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java new file mode 100644 index 0000000000..6814b2edfb --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -0,0 +1,53 @@ +package org.locationtech.jts.coverage; + +import java.util.List; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class CoverageRingEdgesTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(CoverageRingEdgesTest.class); + } + + public CoverageRingEdgesTest(String name) { + super(name); + } + + public void testSimple() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((100 100, 200 200, 300 100, 200 101, 100 100)), POLYGON ((150 0, 100 100, 200 101, 300 100, 250 0, 150 0)))", + "MULTILINESTRING ((100 100, 150 0, 250 0, 300 100), (100 100, 200 101, 300 100), (100 100, 200 200, 300 100))"); + } + + private void checkEdges(String wkt, String wktExpected) { + Geometry geom = read(wkt); + Geometry[] polygons = toArray(geom); + List edges = CoverageRingEdges.create(polygons).getEdges(); + MultiLineString edgeLines = toArray(edges, geom.getFactory()); + Geometry expected = read(wktExpected); + checkEqual(expected, edgeLines); + } + + private MultiLineString toArray(List edges, GeometryFactory geomFactory) { + LineString[] lines = new LineString[edges.size()]; + for (int i = 0; i < edges.size(); i++) { + lines[i] = edges.get(i).toLineString(geomFactory); + } + return geomFactory.createMultiLineString(lines); + + } + + private static Geometry[] toArray(Geometry geom) { + Geometry[] geoms = new Geometry[geom.getNumGeometries()]; + for (int i = 0; i < geom.getNumGeometries(); i++) { + geoms[i] = geom.getGeometryN(i); + } + return geoms; + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index 80aa70439b..0d625d4450 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -16,8 +16,6 @@ import junit.textui.TestRunner; import test.jts.GeometryTestCase; -import java.util.Arrays; - public class CoverageSimplifierTest extends GeometryTestCase { public static void main(String args[]) { TestRunner.run(CoverageSimplifierTest.class); From c562afa17273802f271d669803885d566c09ab91 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 16:13:32 -0700 Subject: [PATCH 24/79] Add license header Signed-off-by: Martin Davis --- .../jts/coverage/CoverageRingEdgesTest.java | 11 +++++++++++ .../locationtech/jts/coverage/TPVWSimplifierTest.java | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 6814b2edfb..3a9ee31914 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ package org.locationtech.jts.coverage; import java.util.List; diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index ed506accf5..fdec3dda4b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -1,3 +1,14 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ package org.locationtech.jts.coverage; import java.util.BitSet; From 07ddd04c47d88ddc16e161e21a04f93403a9e109 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 19:09:56 -0700 Subject: [PATCH 25/79] Add CoverageRingEdges unit test Signed-off-by: Martin Davis --- .../jts/coverage/CoverageRingEdgesTest.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 3a9ee31914..8c5d6c1cde 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -30,9 +30,14 @@ public CoverageRingEdgesTest(String name) { super(name); } - public void testSimple() { - checkEdges("GEOMETRYCOLLECTION (POLYGON ((100 100, 200 200, 300 100, 200 101, 100 100)), POLYGON ((150 0, 100 100, 200 101, 300 100, 250 0, 150 0)))", - "MULTILINESTRING ((100 100, 150 0, 250 0, 300 100), (100 100, 200 101, 300 100), (100 100, 200 200, 300 100))"); + public void testTwoAdjacent() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)))", + "MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5), (1 6, 6 5))"); + } + + public void testTwoAdjacentWithFilledHole() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((1 1, 1 6, 6 5, 9 6, 9 1, 1 1), (2 4, 4 4, 4 2, 2 2, 2 4)), POLYGON ((1 9, 6 9, 6 5, 1 6, 1 9)), POLYGON ((4 2, 2 2, 2 4, 4 4, 4 2)))", + "MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5), (1 6, 6 5), (2 4, 2 2, 4 2, 4 4, 2 4))"); } private void checkEdges(String wkt, String wktExpected) { From d5f7de5d8016628fe3ecc00ee26368629ca1ca70 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 5 Apr 2023 19:52:04 -0700 Subject: [PATCH 26/79] Add CoverageRingEdges unit test Signed-off-by: Martin Davis --- .../org/locationtech/jts/coverage/CoverageRingEdgesTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 8c5d6c1cde..61d5c921d7 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -40,6 +40,11 @@ public void testTwoAdjacentWithFilledHole() { "MULTILINESTRING ((1 6, 1 1, 9 1, 9 6, 6 5), (1 6, 1 9, 6 9, 6 5), (1 6, 6 5), (2 4, 2 2, 4 2, 4 4, 2 4))"); } + public void testHolesAndFillWithDifferentEndpoints() { + checkEdges("GEOMETRYCOLLECTION (POLYGON ((0 10, 10 10, 10 0, 0 0, 0 10), (1 9, 4 8, 9 9, 9 1, 1 1, 1 9)), POLYGON ((9 9, 1 1, 1 9, 4 8, 9 9)), POLYGON ((1 1, 9 9, 9 1, 1 1)))", + "MULTILINESTRING ((0 10, 0 0, 10 0, 10 10, 0 10), (1 1, 1 9, 4 8, 9 9), (1 1, 9 1, 9 9), (1 1, 9 9))"); + } + private void checkEdges(String wkt, String wktExpected) { Geometry geom = read(wkt); Geometry[] polygons = toArray(geom); From 5bf817b7ae7628cdaf02ac192043d7bb001ab162 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 9 Apr 2023 10:01:07 -0700 Subject: [PATCH 27/79] Fix Corner to have deterministic ordering Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/simplify/Corner.java | 14 ++++++++++++-- .../jts/coverage/CoverageSimplifierTest.java | 12 ++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java b/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java index 6816861e05..d1b3b5a93b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java +++ b/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java @@ -42,6 +42,10 @@ public int getIndex() { return index; } + public Coordinate getCoordinate() { + return edge.getCoordinate(index); + } + public double getArea() { return area; } @@ -62,11 +66,17 @@ private static double area(LinkedLine edge, int index) { } /** - * Orders corners by increasing area + * Orders corners by increasing area. + * To ensure equal-area corners have a deterministic ordering, + * if area is equal then compares corner index. */ @Override public int compareTo(Corner o) { - return Double.compare(area, o.area); + int comp = Double.compare(area, o.area); + if (comp != 0) + return comp; + //-- ensure equal-area corners have a deterministic ordering + return Integer.compare(index, o.index); } public Envelope envelope() { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index 0d625d4450..aacf5a5582 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -109,9 +109,9 @@ public void testTouchingHoles() { "POLYGON (( 12 6, 12 7, 13 7, 13 9, 14 9, 14 6, 12 6 ))"), 1.0, readArray( - "POLYGON (( 0 0, 0 11, 19 11, 19 0, 0 0 ), ( 12 6, 10 6, 10 8, 7 9, 6 6, 4 5, 12 5, 12 6 ), ( 12 6, 14 6, 14 9, 12 6 ))", - "POLYGON (( 12 6, 10 6, 10 8, 7 9, 6 6, 4 5, 12 5, 12 6 ))", - "POLYGON (( 12 6, 14 6, 14 9, 12 6 ))" ) + "POLYGON ((0 0, 0 11, 19 11, 19 0, 0 0), (4 5, 12 5, 12 6, 10 6, 9 9, 6 8, 6 6, 4 5), (12 6, 14 6, 14 9, 12 6))", + "POLYGON ((4 5, 6 6, 6 8, 9 9, 10 6, 12 6, 12 5, 4 5))", + "POLYGON ((12 6, 14 9, 14 6, 12 6))" ) ); } @@ -122,8 +122,8 @@ public void testHoleTouchingShell() { "POLYGON ((170 250, 200 300, 200 250, 170 250))"), 100.0, readArray( - "POLYGON ((200 300, 300 300, 300 100, 100 100, 100 300, 200 300), (200 250, 170 160, 200 140, 200 250), (200 250, 200 300, 170 250, 200 250))", - "POLYGON ((200 250, 170 160, 200 140, 200 250))", + "POLYGON ((100 100, 100 300, 200 300, 300 300, 300 100, 100 100), (170 160, 200 140, 200 250, 170 160), (170 250, 200 250, 200 300, 170 250))", + "POLYGON ((170 160, 200 250, 200 140, 170 160))", "POLYGON ((200 250, 200 300, 170 250, 200 250))" ) ); } @@ -162,7 +162,7 @@ public void testMultiPolygonWithTouchingShells() { "MULTIPOLYGON ((( 2 7, 2 8, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 0 7, 0 8, 1 8, 1 7, 0 7 )), (( 0 5, 0 6, 1 6, 1 5, 0 5 )), (( 2 5, 2 6, 3 6, 3 5, 2 5 )))"), 1.0, readArray( - "MULTIPOLYGON ((( 2 7, 3 8, 3 7, 2 7 )), (( 1 6, 1 7, 2 7, 2 6, 1 6 )), (( 1 7, 0 8, 1 8, 1 7 )), (( 1 6, 0 5, 0 6, 1 6 )), (( 2 6, 3 5, 2 5, 2 6 )))") + "MULTIPOLYGON (((0 5, 0 6, 1 6, 0 5)), ((0 8, 1 8, 1 7, 0 8)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((2 5, 2 6, 3 5, 2 5)), ((2 7, 3 8, 3 7, 2 7)))") ); } From 047f027ac0a96da78876f8ab880fce8041385b18 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 10 Apr 2023 10:01:26 -0700 Subject: [PATCH 28/79] Fix bad geometry in TPVWSimplifer unit test input Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/coverage/TPVWSimplifierTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java index fdec3dda4b..58bb97f6ec 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/TPVWSimplifierTest.java @@ -47,7 +47,7 @@ public void testFreeRing() { } public void testNoFreeRing() { - checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (5 5, 4 8, 2 8, 2 2, 4 2, 5 5), (5 5, 6 8, 8.1 8, 8 8, 8 2, 6 2, 5 5))", + checkSimplify("MULTILINESTRING ((1 9, 9 9, 9 1), (1 9, 1 1, 9 1), (5 5, 4 8, 2 8, 2 2, 4 2, 5 5), (5 5, 6 8, 8 8, 8 2, 6 2, 5 5))", new int[] { }, 2, "MULTILINESTRING ((1 9, 1 1, 9 1), (1 9, 9 9, 9 1), (5 5, 2 2, 2 8, 5 5), (5 5, 8 2, 8 8, 5 5))"); From 819dc0876ee233dc1d75ff6c7fe870fdc0cfd101 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 11 Apr 2023 07:33:26 -0700 Subject: [PATCH 29/79] Coverage simplifier refactoring and unit tests Signed-off-by: Martin Davis --- .../CoverageBoundarySegmentFinder.java | 9 +++++-- .../jts/coverage/CoverageRingEdges.java | 9 ++----- .../jts/coverage/CoverageRingEdgesTest.java | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java index 24370763e6..02108909a7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java @@ -19,7 +19,7 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineSegment; -public class CoverageBoundarySegmentFinder implements CoordinateSequenceFilter { +class CoverageBoundarySegmentFinder implements CoordinateSequenceFilter { public static Set findBoundarySegments(Geometry[] geoms) { Set segs = new HashSet(); @@ -30,6 +30,11 @@ public static Set findBoundarySegments(Geometry[] geoms) { return segs; } + public static boolean isBoundarySegment(Set boundarySegs, CoordinateSequence seq, int i) { + LineSegment seg = createSegment(seq, i); + return boundarySegs.contains(seg); + } + private Set boundarySegs; public CoverageBoundarySegmentFinder(Set segs) { @@ -50,7 +55,7 @@ public void filter(CoordinateSequence seq, int i) { } } - public static LineSegment createSegment(CoordinateSequence seq, int i) { + private static LineSegment createSegment(CoordinateSequence seq, int i) { LineSegment seg = new LineSegment(seq.getCoordinate(i), seq.getCoordinate(i + 1)); seg.normalize(); return seg; diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index a82cec2a31..a1660d20e3 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -112,10 +112,10 @@ private void addRingEdges(LinearRing ring, Set nodes, Set boundarySegs, Set nodes) { CoordinateSequence seq = ring.getCoordinateSequence(); - boolean isBdyLast = isBoundarySegment(seq, seq.size() - 2, boundarySegs); + boolean isBdyLast = CoverageBoundarySegmentFinder.isBoundarySegment(boundarySegs, seq, seq.size() - 2); boolean isBdyPrev = isBdyLast; for (int i = 0; i < seq.size() - 1; i++) { - boolean isBdy = isBoundarySegment(seq, i, boundarySegs); + boolean isBdy = CoverageBoundarySegmentFinder.isBoundarySegment(boundarySegs, seq, i); if (isBdy != isBdyPrev) { Coordinate nodePt = seq.getCoordinate(i); nodes.add(nodePt); @@ -124,11 +124,6 @@ private void addBoundaryNodes(LinearRing ring, Set boundarySegs, Se } } - private boolean isBoundarySegment(CoordinateSequence seq, int i, Set boundarySegs) { - LineSegment seg = CoverageBoundarySegmentFinder.createSegment(seq, i); - return boundarySegs.contains(seg); - } - private List extractRingEdges(LinearRing ring, HashMap uniqueEdgeMap, Set nodes) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index 61d5c921d7..ccc00bf5f4 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -45,6 +45,22 @@ public void testHolesAndFillWithDifferentEndpoints() { "MULTILINESTRING ((0 10, 0 0, 10 0, 10 10, 0 10), (1 1, 1 9, 4 8, 9 9), (1 1, 9 1, 9 9), (1 1, 9 9))"); } + public void testTouchingSquares() { + String wkt = "MULTIPOLYGON (((2 7, 2 8, 3 8, 3 7, 2 7)), ((1 6, 1 7, 2 7, 2 6, 1 6)), ((0 7, 0 8, 1 8, 1 7, 0 7)), ((0 5, 0 6, 1 6, 1 5, 0 5)), ((2 5, 2 6, 3 6, 3 5, 2 5)))"; + checkEdgesSelected(wkt, 1, + "MULTILINESTRING ((1 6, 0 6, 0 5, 1 5, 1 6), (1 6, 1 7), (1 6, 2 6), (1 7, 0 7, 0 8, 1 8, 1 7), (1 7, 2 7), (2 6, 2 5, 3 5, 3 6, 2 6), (2 6, 2 7), (2 7, 2 8, 3 8, 3 7, 2 7))"); + checkEdgesSelected(wkt, 2, + "MULTILINESTRING EMPTY"); + } + + public void testAdjacentSquares() { + String wkt = "GEOMETRYCOLLECTION (POLYGON ((1 3, 2 3, 2 2, 1 2, 1 3)), POLYGON ((3 3, 3 2, 2 2, 2 3, 3 3)), POLYGON ((3 1, 2 1, 2 2, 3 2, 3 1)), POLYGON ((1 1, 1 2, 2 2, 2 1, 1 1)))"; + checkEdgesSelected(wkt, 1, + "MULTILINESTRING ((1 2, 1 1, 2 1), (1 2, 1 3, 2 3), (2 1, 3 1, 3 2), (2 3, 3 3, 3 2))"); + checkEdgesSelected(wkt, 2, + "MULTILINESTRING ((1 2, 2 2), (2 1, 2 2), (2 2, 2 3), (2 2, 3 2))"); + } + private void checkEdges(String wkt, String wktExpected) { Geometry geom = read(wkt); Geometry[] polygons = toArray(geom); @@ -54,6 +70,16 @@ private void checkEdges(String wkt, String wktExpected) { checkEqual(expected, edgeLines); } + private void checkEdgesSelected(String wkt, int ringCount, String wktExpected) { + Geometry geom = read(wkt); + Geometry[] polygons = toArray(geom); + CoverageRingEdges covEdges = CoverageRingEdges.create(polygons); + List edges = covEdges.selectEdges(ringCount); + MultiLineString edgeLines = toArray(edges, geom.getFactory()); + Geometry expected = read(wktExpected); + checkEqual(expected, edgeLines); + } + private MultiLineString toArray(List edges, GeometryFactory geomFactory) { LineString[] lines = new LineString[edges.size()]; for (int i = 0; i < edges.size(); i++) { From 82a1367ca8aecf45db0812299c7e04d1601e1895 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 11 Apr 2023 08:39:44 -0700 Subject: [PATCH 30/79] Move Corner into coverage package Signed-off-by: Martin Davis --- .../org/locationtech/jts/{simplify => coverage}/Corner.java | 5 +++-- .../java/org/locationtech/jts/coverage/TPVWSimplifier.java | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) rename modules/core/src/main/java/org/locationtech/jts/{simplify => coverage}/Corner.java (96%) diff --git a/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java similarity index 96% rename from modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java rename to modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java index d1b3b5a93b..1f57fa10d5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/simplify/Corner.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/Corner.java @@ -9,15 +9,16 @@ * * http://www.eclipse.org/org/documents/edl-v10.php. */ -package org.locationtech.jts.simplify; +package org.locationtech.jts.coverage; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.Triangle; +import org.locationtech.jts.simplify.LinkedLine; -public class Corner implements Comparable { +class Corner implements Comparable { private LinkedLine edge; private int index; private int prev; diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java index fd8a11f8af..64f37913d6 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/TPVWSimplifier.java @@ -24,7 +24,6 @@ import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.index.VertexSequencePackedRtree; import org.locationtech.jts.index.strtree.STRtree; -import org.locationtech.jts.simplify.Corner; import org.locationtech.jts.simplify.LinkedLine; /** From 9f61e6bb78ad79cb539cc3e0b1565962544e634f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 11 Apr 2023 17:45:31 -0700 Subject: [PATCH 31/79] Fix Javadoc for STRtree.depth() Signed-off-by: Martin Davis --- .../main/java/org/locationtech/jts/index/strtree/STRtree.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/index/strtree/STRtree.java b/modules/core/src/main/java/org/locationtech/jts/index/strtree/STRtree.java index f1f8c2163f..d565f235b8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/index/strtree/STRtree.java +++ b/modules/core/src/main/java/org/locationtech/jts/index/strtree/STRtree.java @@ -263,9 +263,9 @@ public int size() } /** - * Returns the number of items in the tree. + * Returns the number of levels in the tree. * - * @return the number of items in the tree + * @return the number of levels in the tree */ public int depth() { From e1c1426e3af13fab52e69ad2ae41f0f1587180cd Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 11 Apr 2023 20:52:14 -0700 Subject: [PATCH 32/79] Javadoc Signed-off-by: Martin Davis --- .../java/org/locationtech/jts/coverage/package-info.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java b/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java index bcbe3682ec..a163633c3e 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/package-info.java @@ -13,11 +13,12 @@ /** * Classes that operate on polygonal coverages. *

      - * A polygonal coverage is a non-overlapping, fully-noded set of polygons. - * Specifically, a set of polygons is a valid coverage if: + * A polygonal coverage is a set of polygonal geometries which is non-overlapping and edge-matched. + * ({@link Polygon}s or {@link MultiPolygon}s). + * A set of polygonal geometries is a valid coverage if: *

        - *
      1. The polygons are valid - *
      2. The interiors of all polygons are disjoint. + *
      3. Each geometry is valid + *
      4. The interiors of all polygons are disjoint (they are non-overlapping). * This is the case if no polygon has a boundary which intersects the interior of another polygon. *
      5. Where polygons are adjacent (i.e. their boundaries intersect), * they are edge-matched: the vertices From 80b50b82a9aafed99ccb979fbeee77c38458cf52 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 13 Apr 2023 17:01:54 -0700 Subject: [PATCH 33/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 017810d719..c12b3965af 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -70,6 +70,7 @@ Distributions for older JTS versions can be obtained at the * Add `ConcaveHullOfPolygons` class (#870) * Add `PolygonHullSimplifier` class (#861, #880) * TWKB read and write implementation (#854) +* Add `CubicBezierCurve` class ### Functionality Improvements @@ -87,7 +88,7 @@ Distributions for older JTS versions can be obtained at the ### Performance Improvements -* Improve performance of `CoveageUnion` by using boundary chains (#891) +* Improve performance of `CoverageUnion` by using boundary chains (#891) ### Bug Fixes From b182c883d43c970ec3a61cb96250c59c2db50933 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 19 Apr 2023 16:26:14 -0700 Subject: [PATCH 34/79] Fix CoverageSimplifier to handle collapsed rings Signed-off-by: Martin Davis --- .../CoverageBoundarySegmentFinder.java | 16 ++++ .../jts/coverage/CoverageEdge.java | 40 ++++---- .../jts/coverage/CoverageRingEdges.java | 95 ++++++++++++++----- .../jts/coverage/CoverageSimplifier.java | 1 + ...texCounter.java => VertexRingCounter.java} | 26 +++-- .../jts/coverage/CoverageRingEdgesTest.java | 6 ++ .../jts/coverage/CoverageSimplifierTest.java | 49 ++++++++++ 7 files changed, 179 insertions(+), 54 deletions(-) rename modules/core/src/main/java/org/locationtech/jts/coverage/{VertexCounter.java => VertexRingCounter.java} (62%) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java index 02108909a7..08de747d04 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageBoundarySegmentFinder.java @@ -19,6 +19,17 @@ import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineSegment; +/** + * Finds coverage segments which occur in only a single coverage element. + * In a valid coverage, these are exactly the line segments which lie + * on the boundary of the coverage. + *

        + * In an invalid coverage, segments might occur in 3 or more elements. + * This situation is not detected. + * + * @author mdavis + * + */ class CoverageBoundarySegmentFinder implements CoordinateSequenceFilter { public static Set findBoundarySegments(Geometry[] geoms) { @@ -47,6 +58,11 @@ public void filter(CoordinateSequence seq, int i) { if (i >= seq.size() - 1) return; LineSegment seg = createSegment(seq, i); + /** + * Records segments with an odd number of occurrences. + * In a valid coverage each segment can occur only 1 or 2 times. + * This does not detect invalid situations, where a segment might occur 3 or more times. + */ if (boundarySegs.contains(seg)) { boundarySegs.remove(seg); } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index 0e5cedb074..0626ea5705 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -31,13 +31,13 @@ */ class CoverageEdge { - public static CoverageEdge createEdge(LinearRing ring) { - Coordinate[] pts = extractEdgePoints(ring, 0, ring.getNumPoints() - 1); + public static CoverageEdge createEdge(Coordinate[] ring) { + Coordinate[] pts = extractEdgePoints(ring, 0, ring.length - 1); CoverageEdge edge = new CoverageEdge(pts, true); return edge; } - public static CoverageEdge createEdge(LinearRing ring, int start, int end) { + public static CoverageEdge createEdge(Coordinate[] ring, int start, int end) { Coordinate[] pts = extractEdgePoints(ring, start, end); CoverageEdge edge = new CoverageEdge(pts, false); return edge; @@ -53,16 +53,16 @@ static MultiLineString createLines(List edges, GeometryFactory geo return mls; } - private static Coordinate[] extractEdgePoints(LinearRing ring, int start, int end) { + private static Coordinate[] extractEdgePoints(Coordinate[] ring, int start, int end) { int size = start < end ? end - start + 1 - : ring.getNumPoints() - start + end; + : ring.length - start + end; Coordinate[] pts = new Coordinate[size]; int iring = start; for (int i = 0; i < size; i++) { - pts[i] = ring.getCoordinateN(iring).copy(); + pts[i] = ring[iring].copy(); iring += 1; - if (iring >= ring.getNumPoints()) iring = 1; + if (iring >= ring.length) iring = 1; } return pts; } @@ -75,18 +75,17 @@ private static Coordinate[] extractEdgePoints(LinearRing ring, int start, int en * @param ring a linear ring * @return a LineSegment representing the key */ - public static LineSegment key(LinearRing ring) { - Coordinate[] pts = ring.getCoordinates(); - // find lowest vertex index + public static LineSegment key(Coordinate[] ring) { + // find lowest vertex index int indexLow = 0; - for (int i = 1; i < pts.length - 1; i++) { - if (pts[indexLow].compareTo(pts[i]) < 0) + for (int i = 1; i < ring.length - 1; i++) { + if (ring[indexLow].compareTo(ring[i]) < 0) indexLow = i; } - Coordinate key0 = pts[indexLow]; + Coordinate key0 = ring[indexLow]; // find distinct adjacent vertices - Coordinate adj0 = findDistinctPoint(pts, indexLow, true, key0); - Coordinate adj1 = findDistinctPoint(pts, indexLow, false, key0); + Coordinate adj0 = findDistinctPoint(ring, indexLow, true, key0); + Coordinate adj1 = findDistinctPoint(ring, indexLow, false, key0); Coordinate key1 = adj0.compareTo(adj1) < 0 ? adj0 : adj1; return new LineSegment(key0, key1); } @@ -99,20 +98,19 @@ public static LineSegment key(LinearRing ring) { * @param end end index of the end of the section * @return a LineSegment representing the key */ - public static LineSegment key(LinearRing ring, int start, int end) { - Coordinate[] pts = ring.getCoordinates(); + public static LineSegment key(Coordinate[] ring, int start, int end) { //-- endpoints are distinct in a line edge - Coordinate end0 = pts[start]; - Coordinate end1 = pts[end]; + Coordinate end0 = ring[start]; + Coordinate end1 = ring[end]; boolean isForward = 0 > end0.compareTo(end1); Coordinate key0, key1; if (isForward) { key0 = end0; - key1 = findDistinctPoint(pts, start, true, key0); + key1 = findDistinctPoint(ring, start, true, key0); } else { key0 = end1; - key1 = findDistinctPoint(pts, end, false, key0); + key1 = findDistinctPoint(ring, end, false, key0); } return new LineSegment(key0, key1); } diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index a1660d20e3..59c314a02b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateArrays; import org.locationtech.jts.geom.CoordinateList; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.Geometry; @@ -35,6 +36,11 @@ * it is an inner or outer edge of the coverage. * The source coverage is represented as a array of polygonal geometries * (either {@link Polygon}s or {@link MultiPolygon}s). + *

        + * Coverage edges are found by identifying vertices which are nodes in the coverage, + * splitting edges at nodes, and then identifying unique edges. + * The unique edges are associated to their parent ring (in order), + * to allow reforming the coverage polygons. * * @author Martin Davis * @@ -84,7 +90,7 @@ public List selectEdges(int ringCount) { } private void build() { - Set nodes = findNodes(coverage); + Set nodes = findMultiRingNodes(coverage); Set boundarySegs = CoverageBoundarySegmentFinder.findBoundarySegments(coverage); nodes.addAll(findBoundaryNodes(boundarySegs)); HashMap uniqueEdgeMap = new HashMap(); @@ -105,12 +111,23 @@ private void build() { private void addRingEdges(LinearRing ring, Set nodes, Set boundarySegs, HashMap uniqueEdgeMap) { - addBoundaryNodes(ring, boundarySegs, nodes); + addBoundaryInnerNodes(ring, boundarySegs, nodes); List ringEdges = extractRingEdges(ring, uniqueEdgeMap, nodes); - ringEdgesMap.put(ring, ringEdges); + if (ringEdges != null) + ringEdgesMap.put(ring, ringEdges); } - private void addBoundaryNodes(LinearRing ring, Set boundarySegs, Set nodes) { + /** + * Detects nodes occurring at vertices which are between a boundary segment + * and an inner (shared) segment. + * These occur where two polygons are adjacent at the coverage boundary + * (this is not detected by {@link #findMultiRingNodes(Geometry[])}). + * + * @param ring + * @param boundarySegs + * @param nodes + */ + private void addBoundaryInnerNodes(LinearRing ring, Set boundarySegs, Set nodes) { CoordinateSequence seq = ring.getCoordinateSequence(); boolean isBdyLast = CoverageBoundarySegmentFinder.isBoundarySegment(boundarySegs, seq, seq.size() - 2); boolean isBdyPrev = isBdyLast; @@ -127,19 +144,28 @@ private void addBoundaryNodes(LinearRing ring, Set boundarySegs, Se private List extractRingEdges(LinearRing ring, HashMap uniqueEdgeMap, Set nodes) { + // System.out.println(ring); List ringEdges = new ArrayList(); - int first = findNextNodeIndex(ring, -1, nodes); + + Coordinate[] pts = ring.getCoordinates(); + pts = CoordinateArrays.removeRepeatedPoints(pts); + //-- if compacted ring is too short, don't process it + if (pts.length < 3) + return null; + + int first = findNextNodeIndex(pts, -1, nodes); if (first < 0) { //-- ring does not contain a node, so edge is entire ring - CoverageEdge edge = createEdge(ring, uniqueEdgeMap); + CoverageEdge edge = createEdge(pts, uniqueEdgeMap); ringEdges.add(edge); } else { int start = first; int end = start; do { - end = findNextNodeIndex(ring, start, nodes); - CoverageEdge edge = createEdge(ring, start, end, uniqueEdgeMap); + end = findNextNodeIndex(pts, start, nodes); + CoverageEdge edge = createEdge(pts, start, end, uniqueEdgeMap); +// System.out.println(ringEdges.size() + " : " + edge); ringEdges.add(edge); start = end; } while (end != first); @@ -147,7 +173,7 @@ private List extractRingEdges(LinearRing ring, return ringEdges; } - private CoverageEdge createEdge(LinearRing ring, HashMap uniqueEdgeMap) { + private CoverageEdge createEdge(Coordinate[] ring, HashMap uniqueEdgeMap) { CoverageEdge edge; LineSegment edgeKey = CoverageEdge.key(ring); if (uniqueEdgeMap.containsKey(edgeKey)) { @@ -162,7 +188,7 @@ private CoverageEdge createEdge(LinearRing ring, HashMap uniqueEdgeMap) { + private CoverageEdge createEdge(Coordinate[] ring, int start, int end, HashMap uniqueEdgeMap) { CoverageEdge edge; LineSegment edgeKey = (end == start) ? CoverageEdge.key(ring) : CoverageEdge.key(ring, start, end); if (uniqueEdgeMap.containsKey(edgeKey)) { @@ -177,7 +203,7 @@ private CoverageEdge createEdge(LinearRing ring, int start, int end, HashMap nodes) { + private int findNextNodeIndex(Coordinate[] ring, int start, Set nodes) { int index = start; boolean isScanned0 = false; do { @@ -187,7 +213,7 @@ private int findNextNodeIndex(LinearRing ring, int start, Set nodes) return -1; isScanned0 = true; } - Coordinate pt = ring.getCoordinateN(index); + Coordinate pt = ring[index]; if (nodes.contains(pt)) { return index; } @@ -195,33 +221,50 @@ private int findNextNodeIndex(LinearRing ring, int start, Set nodes) return -1; } - private static int next(int index, LinearRing ring) { + private static int next(int index, Coordinate[] ring) { index = index + 1; - if (index >= ring.getNumPoints() - 1) + if (index >= ring.length - 1) index = 0; return index; } - private Set findNodes(Geometry[] coverage) { - Map vertexCount = VertexCounter.count(coverage); + /** + * Finds nodes in a coverage at vertices which are shared by 3 or more rings. + * + * @param coverage a list of polygonal geometries + * @return the set of nodes contained in 3 or more rings + */ + private Set findMultiRingNodes(Geometry[] coverage) { + Map vertexRingCount = VertexRingCounter.count(coverage); Set nodes = new HashSet(); - for (Coordinate v : vertexCount.keySet()) { - if (vertexCount.get(v) > 2) { + for (Coordinate v : vertexRingCount.keySet()) { + if (vertexRingCount.get(v) >= 3) { nodes.add(v); } } return nodes; } - - private Set findBoundaryNodes(Set lineSegments) { + /** + * Finds nodes occurring between boundary segments. + * Nodes on boundaries occur at vertices which have + * 3 or more incident boundary segments. + * This detects situations where two rings touch only at a vertex + * (i.e. two polygons touch, or a polygon shell touches a hole) + * These nodes lie in only 2 adjacent rings, + * so are not detected by {@link #findMultiRingNodes(Geometry[])}. + * + * @param boundarySegments + * @return a set of vertices which are nodes where two rings touch + */ + private Set findBoundaryNodes(Set boundarySegments) { Map counter = new HashMap<>(); - for (LineSegment line : lineSegments) { - counter.put(line.p0, counter.getOrDefault(line.p0, 0) + 1); - counter.put(line.p1, counter.getOrDefault(line.p1, 0) + 1); + for (LineSegment seg : boundarySegments) { + counter.put(seg.p0, counter.getOrDefault(seg.p0, 0) + 1); + counter.put(seg.p1, counter.getOrDefault(seg.p1, 0) + 1); } return counter.entrySet().stream() - .filter(e->e.getValue()>2) + .filter(e->e.getValue() > 2) .map(Map.Entry::getKey).collect(Collectors.toSet()); } @@ -270,6 +313,10 @@ private Polygon buildPolygon(Polygon polygon) { private LinearRing buildRing(LinearRing ring) { List ringEdges = ringEdgesMap.get(ring); + //-- if ring is not in map, must have been invalid. Just copy original + if (ringEdges == null) + return (LinearRing) ring.copy(); + CoordinateList ptsList = new CoordinateList(); for (int i = 0; i < ringEdges.size(); i++) { Coordinate lastPt = ptsList.size() > 0 diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java index 9b3a86f0b3..6a0134e814 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageSimplifier.java @@ -46,6 +46,7 @@ * subset of a coverage still matches the remainder of the coverage. *

        * The input coverage should be valid according to {@link CoverageValidator}. + * Invalid coverages may still be simplified, but the result will still be invalid. * * @author Martin Davis */ diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/VertexCounter.java b/modules/core/src/main/java/org/locationtech/jts/coverage/VertexRingCounter.java similarity index 62% rename from modules/core/src/main/java/org/locationtech/jts/coverage/VertexCounter.java rename to modules/core/src/main/java/org/locationtech/jts/coverage/VertexRingCounter.java index 2304549ec8..7a0f51f524 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/VertexCounter.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/VertexRingCounter.java @@ -20,21 +20,29 @@ import org.locationtech.jts.geom.CoordinateSequences; import org.locationtech.jts.geom.Geometry; -class VertexCounter implements CoordinateSequenceFilter { +/** + * Counts the number of rings containing each vertex. + * Vertices which are contained by 3 or more rings are nodes in the coverage topology + * (although not the only ones - + * boundary vertices with 3 or more incident edges are also nodes). + * @author mdavis + * + */ +class VertexRingCounter implements CoordinateSequenceFilter { public static Map count(Geometry[] geoms) { - Map vertexCount = new HashMap(); - VertexCounter counter = new VertexCounter(vertexCount); + Map vertexRingCount = new HashMap(); + VertexRingCounter counter = new VertexRingCounter(vertexRingCount); for (Geometry geom : geoms) { geom.apply(counter); } - return vertexCount; + return vertexRingCount; } - private Map vertexCount; + private Map vertexRingCount; - public VertexCounter(Map vertexCount) { - this.vertexCount = vertexCount; + public VertexRingCounter(Map vertexRingCount) { + this.vertexRingCount = vertexRingCount; } @Override @@ -43,9 +51,9 @@ public void filter(CoordinateSequence seq, int i) { if (CoordinateSequences.isRing(seq) && i == 0) return; Coordinate v = seq.getCoordinate(i); - int count = vertexCount.containsKey(v) ? vertexCount.get(v) : 0; + int count = vertexRingCount.containsKey(v) ? vertexRingCount.get(v) : 0; count++; - vertexCount.put(v, count); + vertexRingCount.put(v, count); } @Override diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java index ccc00bf5f4..2289e5a5b2 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageRingEdgesTest.java @@ -61,6 +61,12 @@ public void testAdjacentSquares() { "MULTILINESTRING ((1 2, 2 2), (2 1, 2 2), (2 2, 2 3), (2 2, 3 2))"); } + public void testMultiPolygons() { + checkEdges("GEOMETRYCOLLECTION (MULTIPOLYGON (((5 9, 2.5 7.5, 1 5, 5 5, 5 9)), ((5 5, 9 5, 7.5 2.5, 5 1, 5 5))), MULTIPOLYGON (((5 9, 6.5 6.5, 9 5, 5 5, 5 9)), ((1 5, 5 5, 5 1, 3.5 3.5, 1 5))))", + "MULTILINESTRING ((1 5, 2.5 7.5, 5 9), (1 5, 3.5 3.5, 5 1), (1 5, 5 5), (5 1, 5 5), (5 1, 7.5 2.5, 9 5), (5 5, 5 9), (5 5, 9 5), (5 9, 6.5 6.5, 9 5))" + ); + } + private void checkEdges(String wkt, String wktExpected) { Geometry geom = read(wkt); Geometry[] polygons = toArray(geom); diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index aacf5a5582..36bab9e640 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -56,6 +56,44 @@ public void testNoopMulti() { //--------------------------------------------- + public void testRepeatedPointRemoved() { + checkResult(readArray( + "POLYGON ((5 9, 6.5 6.5, 9 5, 5 5, 5 5, 5 9))" ), + 2, + readArray( + "POLYGON ((5 5, 5 9, 9 5, 5 5))" ) + ); + } + + public void testRepeatedPointCollapseToLine() { + checkResult(readArray( + "MULTIPOLYGON (((10 10, 10 20, 20 19, 30 20, 30 10, 10 10)), ((10 30, 20 29, 30 30, 30 20, 20 19, 10 20, 10 30)), ((10 20, 20 19, 20 19, 10 20)))" ), + 5, + readArray( + "MULTIPOLYGON (((10 20, 20 19, 30 20, 30 10, 10 10, 10 20)), ((30 20, 20 19, 10 20, 10 30, 30 30, 30 20)), ((10 20, 20 19, 10 20)))" ) + ); + } + + public void testRepeatedPointCollapseToPoint() { + checkResult(readArray( + "MULTIPOLYGON (((10 10, 10 20, 20 19, 30 20, 30 10, 10 10)), ((10 30, 20 29, 30 30, 30 20, 20 19, 10 20, 10 30)), ((20 19, 20 19, 20 19)))" ), + 5, + readArray( + "MULTIPOLYGON (((10 10, 10 20, 20 19, 30 20, 30 10, 10 10)), ((10 20, 10 30, 30 30, 30 20, 20 19, 10 20)), ((20 19, 20 19, 20 19)))" ) + ); + } + + public void testRepeatedPointCollapseToPoint2() { + checkResult(readArray( + "MULTIPOLYGON (((100 200, 150 195, 200 200, 200 100, 100 100, 100 200)), ((150 195, 150 195, 150 195, 150 195)))" ), + 40, + readArray( + "MULTIPOLYGON (((10 10, 10 20, 20 19, 30 20, 30 10, 10 10)), ((10 20, 10 30, 30 30, 30 20, 20 19, 10 20)), ((20 19, 20 19, 20 19)))" ) + ); + } + + //--------------------------------------------- + public void testSimple2() { checkResult(readArray( "POLYGON ((100 100, 200 200, 300 100, 200 101, 100 100))", @@ -67,6 +105,17 @@ public void testSimple2() { ); } + public void testMultiPolygons() { + checkResult(readArray( + "MULTIPOLYGON (((5 9, 2.5 7.5, 1 5, 5 5, 5 9)), ((5 5, 9 5, 7.5 2.5, 5 1, 5 5)))", + "MULTIPOLYGON (((5 9, 6.5 6.5, 9 5, 5 5, 5 9)), ((1 5, 5 5, 5 1, 3.5 3.5, 1 5)))" ), + 3, + readArray( + "MULTIPOLYGON (((1 5, 5 9, 5 5, 1 5)), ((5 1, 5 5, 9 5, 5 1))))", + "MULTIPOLYGON (((1 5, 5 5, 5 1, 1 5)), ((5 5, 5 9, 9 5, 5 5)))" ) + ); + } + public void testSingleRingNoCollapse() { checkResult(readArray( "POLYGON ((10 50, 60 90, 70 50, 60 10, 10 50))" ), From 68ea7264c3846fc4657e47a225ab37bf46516f4d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 19 Apr 2023 16:35:26 -0700 Subject: [PATCH 35/79] Fix CoverageSimplifier unit test Signed-off-by: Martin Davis --- .../org/locationtech/jts/coverage/CoverageSimplifierTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index 36bab9e640..a90f47dcc4 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -88,7 +88,7 @@ public void testRepeatedPointCollapseToPoint2() { "MULTIPOLYGON (((100 200, 150 195, 200 200, 200 100, 100 100, 100 200)), ((150 195, 150 195, 150 195, 150 195)))" ), 40, readArray( - "MULTIPOLYGON (((10 10, 10 20, 20 19, 30 20, 30 10, 10 10)), ((10 20, 10 30, 30 30, 30 20, 20 19, 10 20)), ((20 19, 20 19, 20 19)))" ) + "MULTIPOLYGON (((150 195, 200 200, 200 100, 100 100, 100 200, 150 195)), ((150 195, 150 195, 150 195, 150 195)))" ) ); } From 0b26c6f35e8102371a51b438cc7f8da7db31b047 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 24 Apr 2023 19:43:13 -0700 Subject: [PATCH 36/79] Add CoverageSimplifier handling for empty things Signed-off-by: Martin Davis --- .../jts/coverage/CoverageRingEdges.java | 9 +++++ .../jts/coverage/CoverageSimplifierTest.java | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java index 59c314a02b..ba5f852219 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRingEdges.java @@ -97,12 +97,20 @@ private void build() { for (Geometry geom : coverage) { for (int ipoly = 0; ipoly < geom.getNumGeometries(); ipoly++) { Polygon poly = (Polygon) geom.getGeometryN(ipoly); + + //-- skip empty elements. Missing elements are copied in result + if (poly.isEmpty()) + continue; + //-- extract shell LinearRing shell = poly.getExteriorRing(); addRingEdges(shell, nodes, boundarySegs, uniqueEdgeMap); //-- extract holes for (int ihole = 0; ihole < poly.getNumInteriorRing(); ihole++) { LinearRing hole = poly.getInteriorRingN(ihole); + //-- skip empty rings. Missing rings are copied in result + if (hole.isEmpty()) + continue; addRingEdges(hole, nodes, boundarySegs, uniqueEdgeMap); } } @@ -300,6 +308,7 @@ private Geometry buildMultiPolygon(MultiPolygon geom) { private Polygon buildPolygon(Polygon polygon) { LinearRing shell = buildRing(polygon.getExteriorRing()); + if (polygon.getNumInteriorRing() == 0) { return polygon.getFactory().createPolygon(shell); } diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java index a90f47dcc4..a5822e956d 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageSimplifierTest.java @@ -270,6 +270,42 @@ public void testInnerSimple() { ); } + + //--------------------------------- + + public void testAllEmpty() { + checkResult(readArray( + "POLYGON EMPTY", + "POLYGON EMPTY" ), + 1, + readArray( + "POLYGON EMPTY", + "POLYGON EMPTY" ) + ); + } + + public void testOneEmpty() { + checkResult(readArray( + "POLYGON ((1 9, 5 9.1, 9 9, 9 1, 1 1, 1 9))", + "POLYGON EMPTY" ), + 1, + readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", + "POLYGON EMPTY" ) + ); + } + + public void testEmptyHole() { + checkResult(readArray( + "POLYGON ((1 9, 5 9.1, 9 9, 9 1, 1 1, 1 9), EMPTY)", + "POLYGON EMPTY" ), + 1, + readArray( + "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), EMPTY)", + "POLYGON EMPTY" ) + ); + } + //================================= From 883dedf21e07f40c7af1e37a0f21501a0c881da7 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 25 Apr 2023 21:33:43 -0700 Subject: [PATCH 37/79] Fix CoverageValidator for EMPTY Signed-off-by: Martin Davis --- .../jts/coverage/CoverageRing.java | 12 +++++- .../jts/coverage/CoverageValidatorTest.java | 37 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java index a3cbc3dfd3..82ce9dfc3a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageRing.java @@ -42,12 +42,20 @@ public static List createRings(List polygons) { } private static void createRings(Polygon poly, List rings) { - rings.add( createRing(poly.getExteriorRing(), true)); + if (poly.isEmpty()) + return; + addRing(poly.getExteriorRing(), true, rings); for (int i = 0; i < poly.getNumInteriorRing(); i++) { - rings.add( createRing(poly.getInteriorRingN(i), false)); + addRing( poly.getInteriorRingN(i), false, rings); } } + private static void addRing(LinearRing ring, boolean isShell, List rings) { + if (ring.isEmpty()) + return; + rings.add( createRing(ring, isShell)); + } + private static CoverageRing createRing(LinearRing ring, boolean isShell) { Coordinate[] pts = ring.getCoordinates(); if (CoordinateArrays.hasRepeatedOrInvalidPoints(pts)) { diff --git a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java index e52b0de9cf..5af4bac3cf 100644 --- a/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/coverage/CoverageValidatorTest.java @@ -113,6 +113,43 @@ public void testGrid() { "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))" )); } + public void testMultiPolygon() { + checkValid(readArray( + "MULTIPOLYGON (((1 9, 5 9, 5 5, 1 5, 1 9)), ((9 1, 5 1, 5 5, 9 5, 9 1)))", + "MULTIPOLYGON (((1 1, 1 5, 5 5, 5 1, 1 1)), ((9 9, 9 5, 5 5, 5 9, 9 9)))" )); + } + + public void testValidDuplicatePoints() { + checkValid(readArray( + "POLYGON ((1 9, 5 9, 5 5, 1 5, 1 5, 1 5, 1 9))", + "POLYGON ((9 9, 9 5, 5 5, 5 9, 9 9))", + "POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1))", + "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))" )); + } + + public void testRingCollapse() { + checkValid(readArray( + "POLYGON ((1 9, 5 9, 1 9))", + "POLYGON ((9 9, 9 5, 5 5, 5 9, 9 9))", + "POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1))", + "POLYGON ((9 1, 5 1, 5 5, 9 5, 9 1))" )); + } + + //======== Valid cases with EMPTY ============================= + + public void testPolygonEmpty() { + checkValid(readArray( + "POLYGON ((1 9, 5 9, 5 5, 1 5, 1 9))", + "POLYGON ((9 9, 9 5, 5 5, 5 9, 9 9))", + "POLYGON ((1 1, 1 5, 5 5, 5 1, 1 1))", + "POLYGON EMPTY" )); + } + + public void testMultiPolygonWithEmptyRing() { + checkValid(readArray( + "MULTIPOLYGON (((9 9, 9 1, 1 1, 2 4, 7 7, 9 9)), EMPTY)" )); + } + //------------------------------------------------------------ private void checkValid(Geometry[] coverage) { From 333afbc551f19f88700daf675d83be1b0ecfd77e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 26 Apr 2023 19:54:09 -0700 Subject: [PATCH 38/79] Fix MIC and LEC for thin geometries (#978) Signed-off-by: Martin Davis --- .../construct/LargestEmptyCircle.java | 14 ++++--- .../construct/MaximumInscribedCircle.java | 41 +++++++++++++++++-- .../construct/LargestEmptyCircleTest.java | 31 ++++++++++++-- ...t.java => MaximumInscribedCircleTest.java} | 41 ++++++++++++++++--- 4 files changed, 109 insertions(+), 18 deletions(-) rename modules/core/src/test/java/org/locationtech/jts/algorithm/construct/{MaximumInscibedCircleTest.java => MaximumInscribedCircleTest.java} (68%) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java index 0296a1b17e..e0a976609b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java @@ -142,6 +142,7 @@ public static LineString getRadiusLine(Geometry obstacles, Geometry boundary, do private Point centerPoint = null; private Coordinate radiusPt; private Point radiusPoint = null; + private Geometry bounds; /** * Creates a new instance of a Largest Empty Circle construction, @@ -235,7 +236,7 @@ private double distanceToConstraints(double x, double y) { } private void initBoundary() { - Geometry bounds = this.boundary; + bounds = this.boundary; if (bounds == null || bounds.isEmpty()) { bounds = obstacles.convexHull(); } @@ -278,7 +279,10 @@ private void compute() { * Carry out the branch-and-bound search * of the cell space */ - while (! cellQueue.isEmpty()) { + long maxIter = MaximumInscribedCircle.computeMaximumIterations(bounds, tolerance); + long iter = 0; + while (! cellQueue.isEmpty() && iter < maxIter) { + iter++; // pick the cell with greatest distance from the queue Cell cell = cellQueue.remove(); @@ -351,6 +355,8 @@ private boolean mayContainCircleCenter(Cell cell) { return potentialIncrease > tolerance; } + private static final int INITIAL_GRID_SIDE = 25; + /** * Initializes the queue with a grid of cells covering * the extent of the area. @@ -363,9 +369,7 @@ private void createInitialGrid(Envelope env, PriorityQueue cellQueue) { double maxX = env.getMaxX(); double minY = env.getMinY(); double maxY = env.getMaxY(); - double width = env.getWidth(); - double height = env.getHeight(); - double cellSize = Math.min(width, height); + double cellSize = env.getDiameter() / INITIAL_GRID_SIDE; double hSize = cellSize / 2.0; // compute initial grid of cells to cover area diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java index 93b11dff6d..29bd7a1ba5 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java @@ -90,6 +90,35 @@ public static LineString getRadiusLine(Geometry polygonal, double tolerance) { return mic.getRadiusLine(); } + /** + * Computes the maximum number of iterations allowed. + * Uses a heuristic based on the area of the input geometry + * and the tolerance distance. + * The number of tolerance-sized cells that cover the input geometry area + * is computed, times a safety factor. + * This prevents massive numbers of iterations and created cells + * for casees where the input geometry has extremely small area + * (e.g. is very thin). + * + * @param geom the input geometry + * @param toleranceDist the tolerance distance + * @return the maximum number of iterations allowed + */ + static long computeMaximumIterations(Geometry geom, double toleranceDist) { + int safetyFactor = 100; + int maximumIter = 1_000_000; + //-- use FP in case values are way big or small + double maxCellCount = geom.getArea() / toleranceDist / toleranceDist; + //-- enforce an absolute maximum + if (maxCellCount > (maximumIter / safetyFactor) ) + return maximumIter; + long maxIter = safetyFactor * (long) maxCellCount; + //-- ensure a reasonable number of iterations + if (maxIter < 100) + return 100; + return maxIter; + } + private Geometry inputGeom; private double tolerance; @@ -204,10 +233,14 @@ private void compute() { * Carry out the branch-and-bound search * of the cell space */ - while (! cellQueue.isEmpty()) { + long maxIter = computeMaximumIterations(inputGeom, tolerance); + long iter = 0; + while (! cellQueue.isEmpty() && iter < maxIter) { + iter++; // pick the most promising cell from the queue Cell cell = cellQueue.remove(); //System.out.println(factory.toGeometry(cell.getEnvelope())); + //System.out.println(iter + "] Dist: " + cell.getDistance() + " size: " + cell.getHSide()); // update the center cell if the candidate is further from the boundary if (cell.getDistance() > farthestCell.getDistance()) { @@ -241,6 +274,8 @@ private void compute() { radiusPoint = factory.createPoint(radiusPt); } + private static final int INITIAL_GRID_SIDE = 25; + /** * Initializes the queue with a grid of cells covering * the extent of the area. @@ -253,9 +288,7 @@ private void createInitialGrid(Envelope env, PriorityQueue cellQueue) { double maxX = env.getMaxX(); double minY = env.getMinY(); double maxY = env.getMaxY(); - double width = env.getWidth(); - double height = env.getHeight(); - double cellSize = Math.min(width, height); + double cellSize = env.getDiameter() / INITIAL_GRID_SIDE; // Check for flat collapsed input and if so short-circuit // Result will just be centroid diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java index 318b86a351..5d4ef20491 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java @@ -41,8 +41,8 @@ public void testLinesCrossed() { } public void testLinesZigzag() { - checkCircle("MULTILINESTRING ((100 100, 200 150, 100 200, 250 250, 100 300, 300 350, 100 400), (50 400, 0 350, 50 300, 0 250, 50 200, 0 150, 50 100))", - 0.01, 77.52, 349.99, 54.81 ); + checkCircle("MULTILINESTRING ((100 100, 200 150, 100 200, 250 250, 100 300, 300 350, 100 400), (70 380, 0 350, 50 300, 0 250, 50 200, 0 150, 50 120))", + 0.01, 77.52, 249.99, 54.81 ); } public void testPointsLinesTriangle() { @@ -59,6 +59,11 @@ public void testLineFlat() { checkCircleZeroRadius("LINESTRING (0 0, 50 50)", 0.01 ); } + + public void testPolygonThin() { + checkCircle("MULTIPOINT ((100 100), (300 100), (200 100.1))", + 0.01 ); + } //--------------------------------------------------------- // Obstacles and Boundary @@ -95,6 +100,24 @@ public void testBoundaryAsObstacle() { //======================================================== + /** + * A coarse distance check, mainly testing + * that there is not a huge number of iterations. + * (This will be revealed by CI taking a very long time!) + * + * @param wkt + * @param tolerance + */ + private void checkCircle(String wkt, double tolerance) { + Geometry geom = read(wkt); + LargestEmptyCircle lec = new LargestEmptyCircle(geom, null, tolerance); + Geometry centerPoint = lec.getCenter(); + double dist = geom.distance(centerPoint); + LineString radiusLine = lec.getRadiusLine(); + double actualRadius = radiusLine.getLength(); + assertTrue(Math.abs(actualRadius - dist) < 2 * tolerance); + } + private void checkCircle(String wktObstacles, double tolerance, double x, double y, double expectedRadius) { checkCircle(read(wktObstacles), null, tolerance, x, y, expectedRadius); @@ -111,11 +134,11 @@ private void checkCircle(Geometry obstacles, Geometry boundary, double tolerance Geometry centerPoint = lec.getCenter(); Coordinate centerPt = centerPoint.getCoordinate(); Coordinate expectedCenter = new Coordinate(x, y); - checkEqualXY(expectedCenter, centerPt, tolerance); + checkEqualXY(expectedCenter, centerPt, 2 * tolerance); LineString radiusLine = lec.getRadiusLine(); double actualRadius = radiusLine.getLength(); - assertEquals("Radius: ", expectedRadius, actualRadius, tolerance); + assertEquals("Radius: ", expectedRadius, actualRadius, 2 * tolerance); checkEqualXY("Radius line center point: ", centerPt, radiusLine.getCoordinateN(0)); Coordinate radiusPt = lec.getRadiusPoint().getCoordinate(); diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscibedCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java similarity index 68% rename from modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscibedCircleTest.java rename to modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java index 975fa056bc..f2d69b698f 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscibedCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java @@ -7,13 +7,13 @@ import junit.textui.TestRunner; import test.jts.GeometryTestCase; -public class MaximumInscibedCircleTest extends GeometryTestCase { +public class MaximumInscribedCircleTest extends GeometryTestCase { public static void main(String args[]) { - TestRunner.run(MaximumInscibedCircleTest.class); + TestRunner.run(MaximumInscribedCircleTest.class); } - public MaximumInscibedCircleTest(String name) { super(name); } + public MaximumInscribedCircleTest(String name) { super(name); } public void testSquare() { checkCircle("POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200))", @@ -72,6 +72,37 @@ public void testCollapsedPoint() { 0.01, 100, 100, 0 ); } + /** + * Tests that a nearly flat geometry doesn't make the initial cell grid huge. + * + * See https://github.com/libgeos/geos/issues/875 + */ + public void testNearlyFlat() { + checkCircle("POLYGON ((59.3 100.00000000000001, 99.7 100.00000000000001, 99.7 100, 59.3 100, 59.3 100.00000000000001))", + 0.01 ); + } + + public void testVeryThin() { + checkCircle("POLYGON ((100 100, 200 300, 300 100, 450 250, 300 99.999999, 200 299.99999, 100 100))", + 0.01 ); + } + + /** + * A coarse distance check, mainly testing + * that there is not a huge number of iterations. + * (This will be revealed by CI taking a very long time!) + * + * @param wkt + * @param tolerance + */ + private void checkCircle(String wkt, double tolerance) { + Geometry geom = read(wkt); + MaximumInscribedCircle mic = new MaximumInscribedCircle(geom, tolerance); + Geometry centerPoint = mic.getCenter(); + double dist = geom.distance(centerPoint); + assertTrue(dist < 2 * tolerance); + } + private void checkCircle(String wkt, double tolerance, double x, double y, double expectedRadius) { checkCircle(read(wkt), tolerance, x, y, expectedRadius); @@ -83,11 +114,11 @@ private void checkCircle(Geometry geom, double tolerance, Geometry centerPoint = mic.getCenter(); Coordinate centerPt = centerPoint.getCoordinate(); Coordinate expectedCenter = new Coordinate(x, y); - checkEqualXY(expectedCenter, centerPt, tolerance); + checkEqualXY(expectedCenter, centerPt, 2 * tolerance); LineString radiusLine = mic.getRadiusLine(); double actualRadius = radiusLine.getLength(); - assertEquals("Radius: ", expectedRadius, actualRadius, tolerance); + assertEquals("Radius: ", expectedRadius, actualRadius, 2 * tolerance); checkEqualXY("Radius line center point: ", centerPt, radiusLine.getCoordinateN(0)); Coordinate radiusPt = mic.getRadiusPoint().getCoordinate(); From a1b39e59ffb189d98a3bfeab007c31002c0a9527 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 26 Apr 2023 19:58:42 -0700 Subject: [PATCH 39/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index c12b3965af..5d83b7ba8a 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -51,6 +51,7 @@ Distributions for older JTS versions can be obtained at the (allows `PolygonTriangulator`, `ConstrainedDelaunayTriangulator`, and `ConcaveHullOfPolygons` to work correctly) (#946) * Fix `OffsetCurve` handling of input with repeated points (#956) * Fix `OffsetCurve` handling zero offset distance (#971) +* Fix `MaximumInscribedCircle` and `LargestEmptyCircle` to avoid long looping for thin inputs (#978) ### Performance Improvements From a45d9888a28cdb069a9de7674012a92f204770da Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 28 Apr 2023 13:33:00 -0700 Subject: [PATCH 40/79] Fix MaxInscribedCircle and LargestEmptyCircle Signed-off-by: Martin Davis --- .../construct/LargestEmptyCircle.java | 30 ++++---- .../construct/MaximumInscribedCircle.java | 74 ++++++++----------- .../construct/MaximumInscribedCircleTest.java | 6 +- 3 files changed, 46 insertions(+), 64 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java index e0a976609b..3d23c7a630 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java @@ -285,6 +285,7 @@ private void compute() { iter++; // pick the cell with greatest distance from the queue Cell cell = cellQueue.remove(); + //System.out.println(iter + "] Dist: " + cell.getDistance() + " Max D: " + cell.getMaxDistance() + " size: " + cell.getHSide()); // update the center cell if the candidate is further from the constraints if (cell.getDistance() > farthestCell.getDistance()) { @@ -355,29 +356,23 @@ private boolean mayContainCircleCenter(Cell cell) { return potentialIncrease > tolerance; } - private static final int INITIAL_GRID_SIDE = 25; - /** - * Initializes the queue with a grid of cells covering + * Initializes the queue with a cell covering * the extent of the area. * * @param env the area extent to cover * @param cellQueue the queue to initialize */ private void createInitialGrid(Envelope env, PriorityQueue cellQueue) { - double minX = env.getMinX(); - double maxX = env.getMaxX(); - double minY = env.getMinY(); - double maxY = env.getMaxY(); - double cellSize = env.getDiameter() / INITIAL_GRID_SIDE; - double hSize = cellSize / 2.0; + double cellSize = Math.max(env.getWidth(), env.getHeight()); + double hSide = cellSize / 2.0; - // compute initial grid of cells to cover area - for (double x = minX; x < maxX; x += cellSize) { - for (double y = minY; y < maxY; y += cellSize) { - cellQueue.add(createCell(x + hSize, y + hSize, hSize)); - } - } + // Check for flat collapsed input and if so short-circuit + // Result will just be centroid + if (cellSize == 0) return; + + Coordinate centre = env.centre(); + cellQueue.add(createCell(centre.x, centre.y, hSide)); } private Cell createCell(double x, double y, double h) { @@ -453,10 +448,11 @@ public double getY() { } /** - * A cell is greater if its maximum distance is larger. + * For maximum efficieny sort the PriorityQueue with largest maxDistance at front. + * Since Java PQ sorts least-first, need to invert the comparison */ public int compareTo(Cell o) { - return (int) (o.maxDist - this.maxDist); + return -Double.compare(maxDist, o.maxDist); } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java index 29bd7a1ba5..973a307301 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java @@ -92,31 +92,23 @@ public static LineString getRadiusLine(Geometry polygonal, double tolerance) { /** * Computes the maximum number of iterations allowed. - * Uses a heuristic based on the area of the input geometry + * Uses a heuristic based on the size of the input geometry * and the tolerance distance. - * The number of tolerance-sized cells that cover the input geometry area - * is computed, times a safety factor. - * This prevents massive numbers of iterations and created cells - * for casees where the input geometry has extremely small area - * (e.g. is very thin). + * A smaller tolerance distance allows more iterations. + * This is a rough heuristic, intended + * to prevent huge iterations for very thin geometries. * * @param geom the input geometry * @param toleranceDist the tolerance distance * @return the maximum number of iterations allowed */ static long computeMaximumIterations(Geometry geom, double toleranceDist) { - int safetyFactor = 100; - int maximumIter = 1_000_000; - //-- use FP in case values are way big or small - double maxCellCount = geom.getArea() / toleranceDist / toleranceDist; - //-- enforce an absolute maximum - if (maxCellCount > (maximumIter / safetyFactor) ) - return maximumIter; - long maxIter = safetyFactor * (long) maxCellCount; - //-- ensure a reasonable number of iterations - if (maxIter < 100) - return 100; - return maxIter; + double diam = geom.getEnvelopeInternal().getDiameter(); + double ncells = diam / toleranceDist; + //-- Using log of ncells allows control over number of iterations + int factor = (int) Math.log(ncells); + if (factor < 1) factor = 1; + return 2000 + 2000 * factor; } private Geometry inputGeom; @@ -225,8 +217,8 @@ private void compute() { createInitialGrid(inputGeom.getEnvelopeInternal(), cellQueue); - // use the area centroid as the initial candidate center point - Cell farthestCell = createCentroidCell(inputGeom); + // initial candidate center point + Cell farthestCell = createInterorPointCell(inputGeom); //int totalCells = cellQueue.size(); /** @@ -240,9 +232,13 @@ private void compute() { // pick the most promising cell from the queue Cell cell = cellQueue.remove(); //System.out.println(factory.toGeometry(cell.getEnvelope())); - //System.out.println(iter + "] Dist: " + cell.getDistance() + " size: " + cell.getHSide()); + //System.out.println(iter + "] Dist: " + cell.getDistance() + " Max D: " + cell.getMaxDistance() + " size: " + cell.getHSide()); - // update the center cell if the candidate is further from the boundary + //-- if cell must be closer than furthest, terminate since all remaining cells in queue are even closer. + if (cell.getMaxDistance() < farthestCell.getDistance()) + break; + + // update the circle center cell if the candidate is further from the boundary if (cell.getDistance() > farthestCell.getDistance()) { farthestCell = cell; } @@ -274,43 +270,32 @@ private void compute() { radiusPoint = factory.createPoint(radiusPt); } - private static final int INITIAL_GRID_SIDE = 25; - /** - * Initializes the queue with a grid of cells covering + * Initializes the queue with a cell covering * the extent of the area. * * @param env the area extent to cover * @param cellQueue the queue to initialize */ private void createInitialGrid(Envelope env, PriorityQueue cellQueue) { - double minX = env.getMinX(); - double maxX = env.getMaxX(); - double minY = env.getMinY(); - double maxY = env.getMaxY(); - double cellSize = env.getDiameter() / INITIAL_GRID_SIDE; - + double cellSize = Math.max(env.getWidth(), env.getHeight()); + double hSide = cellSize / 2.0; + // Check for flat collapsed input and if so short-circuit // Result will just be centroid if (cellSize == 0) return; - double hSide = cellSize / 2.0; - - // compute initial grid of cells to cover area - for (double x = minX; x < maxX; x += cellSize) { - for (double y = minY; y < maxY; y += cellSize) { - cellQueue.add(createCell(x + hSide, y + hSide, hSide)); - } - } + Coordinate centre = env.centre(); + cellQueue.add(createCell(centre.x, centre.y, hSide)); } private Cell createCell(double x, double y, double hSide) { return new Cell(x, y, hSide, distanceToBoundary(x, y)); } - // create a cell centered on area centroid - private Cell createCentroidCell(Geometry geom) { - Point p = geom.getCentroid(); + // create a cell at an interior point + private Cell createInterorPointCell(Geometry geom) { + Point p = geom.getInteriorPoint(); return new Cell(p.getX(), p.getY(), 0, distanceToBoundary(p)); } @@ -371,10 +356,11 @@ public double getY() { } /** - * A cell is greater if its maximum possible distance is larger. + * For maximum efficieny sort the PriorityQueue with largest maxDistance at front. + * Since Java PQ sorts least-first, need to invert the comparison */ public int compareTo(Cell o) { - return (int) (o.maxDist - this.maxDist); + return -Double.compare(maxDist, o.maxDist); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java index f2d69b698f..bb114a8bf5 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircleTest.java @@ -52,7 +52,7 @@ public void testDoubleKite() { */ public void testCollapsedLine() { checkCircle("POLYGON ((100 100, 200 200, 100 100, 100 100))", - 0.01, 150, 150, 0 ); + 0.01); } /** @@ -61,7 +61,7 @@ public void testCollapsedLine() { */ public void testCollapsedLineFlat() { checkCircle("POLYGON((1 2, 1 2, 1 2, 1 2, 3 2, 1 2))", - 0.01, 2, 2, 0 ); + 0.01); } /** @@ -99,7 +99,7 @@ private void checkCircle(String wkt, double tolerance) { Geometry geom = read(wkt); MaximumInscribedCircle mic = new MaximumInscribedCircle(geom, tolerance); Geometry centerPoint = mic.getCenter(); - double dist = geom.distance(centerPoint); + double dist = geom.getBoundary().distance(centerPoint); assertTrue(dist < 2 * tolerance); } From 0889cddae23fcb0ce8b9af9f8fb986af5f8b4e34 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 28 Apr 2023 14:03:29 -0700 Subject: [PATCH 41/79] Rename LEC unit test Signed-off-by: Martin Davis --- .../jts/algorithm/construct/LargestEmptyCircleTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java index 5d4ef20491..7382bf66a9 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java @@ -60,7 +60,7 @@ public void testLineFlat() { 0.01 ); } - public void testPolygonThin() { + public void testThinExtent() { checkCircle("MULTIPOINT ((100 100), (300 100), (200 100.1))", 0.01 ); } From 71fc02eb2cc66b9f185ed14c66bce405213b83f0 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 30 Apr 2023 11:19:37 -0700 Subject: [PATCH 42/79] Add MinimumAreaRectangle (#977) Signed-off-by: Martin Davis --- .../function/ConstructionFunctions.java | 6 +- .../locationtech/jts/algorithm/Distance.java | 17 ++ .../jts/algorithm/MinimumAreaRectangle.java | 252 ++++++++++++++++++ .../jts/algorithm/MinimumDiameter.java | 32 ++- .../locationtech/jts/algorithm/Rectangle.java | 129 +++++++++ .../locationtech/jts/geom/LineSegment.java | 28 +- .../algorithm/MinimumAreaRectanglelTest.java | 96 +++++++ .../jts/algorithm/MinimumRectanglelTest.java | 66 ----- .../jts/algorithm/RectangleTest.java | 58 ++++ .../jts/geom/LineSegmentTest.java | 35 +++ 10 files changed, 638 insertions(+), 81 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumAreaRectangle.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/algorithm/Rectangle.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumAreaRectanglelTest.java delete mode 100644 modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java create mode 100644 modules/core/src/test/java/org/locationtech/jts/algorithm/RectangleTest.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java index 52fa580b2a..a560ca3ebd 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ConstructionFunctions.java @@ -14,6 +14,7 @@ import org.locationtech.jts.algorithm.Angle; import org.locationtech.jts.algorithm.MinimumBoundingCircle; import org.locationtech.jts.algorithm.MinimumDiameter; +import org.locationtech.jts.algorithm.MinimumAreaRectangle; import org.locationtech.jts.algorithm.construct.LargestEmptyCircle; import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle; import org.locationtech.jts.algorithm.hull.ConcaveHull; @@ -29,9 +30,10 @@ public class ConstructionFunctions { public static Geometry minimumDiameter(Geometry g) { return (new MinimumDiameter(g)).getDiameter(); } public static double minimumDiameterLength(Geometry g) { return (new MinimumDiameter(g)).getDiameter().getLength(); } + public static Geometry minimumDiameterRectangle(Geometry g) { return MinimumDiameter.getMinimumRectangle(g); } - public static Geometry minimumRectangle(Geometry g) { return (new MinimumDiameter(g)).getMinimumRectangle(); } - + public static Geometry minimumAreaRectangle(Geometry g) { return MinimumAreaRectangle.getMinimumRectangle(g); } + public static Geometry minimumBoundingCircle(Geometry g) { return (new MinimumBoundingCircle(g)).getCircle(); } public static double minimumBoundingCircleDiameterLen(Geometry g) { return 2 * (new MinimumBoundingCircle(g)).getRadius(); } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Distance.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Distance.java index 1aacad396a..22eb7cb887 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Distance.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Distance.java @@ -219,4 +219,21 @@ public static double pointToLinePerpendicular(Coordinate p, return Math.abs(s) * Math.sqrt(len2); } + public static double pointToLinePerpendicularSigned(Coordinate p, + Coordinate A, Coordinate B) + { + // use comp.graphics.algorithms Frequently Asked Questions method + /* + * (2) s = (Ay-Cy)(Bx-Ax)-(Ax-Cx)(By-Ay) + * ----------------------------- + * L^2 + * + * Then the distance from C to P = |s|*L. + */ + double len2 = (B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y); + double s = ((A.y - p.y) * (B.x - A.x) - (A.x - p.x) * (B.y - A.y)) + / len2; + + return s * Math.sqrt(len2); + } } diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumAreaRectangle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumAreaRectangle.java new file mode 100644 index 0000000000..351877eb4b --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumAreaRectangle.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineSegment; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; + +/** + * Computes the minimum-area rectangle enclosing a {@link Geometry}. + * Unlike the {@link Envelope}, the rectangle may not be axis-parallel. + *

        + * The first step in the algorithm is computing the convex hull of the Geometry. + * If the input Geometry is known to be convex, a hint can be supplied to + * avoid this computation. + *

        + * In degenerate cases the minimum enclosing geometry + * may be a {@link LineString} or a {@link Point}. + *

        + * The minimum-area enclosing rectangle does not necessarily + * have the minimum possible width. + * Use {@link MinimumDiameter} to compute this. + * + * @see MinimumDiameter + * @see ConvexHull + * + */ +public class MinimumAreaRectangle +{ + /** + * Gets the minimum-area rectangular {@link Polygon} which encloses the input geometry. + * If the convex hull of the input is degenerate (a line or point) + * a {@link LineString} or {@link Point} is returned. + * + * @param geom the geometry + * @return the minimum rectangle enclosing the geometry + */ + public static Geometry getMinimumRectangle(Geometry geom) { + return (new MinimumAreaRectangle(geom)).getMinimumRectangle(); + } + + private final Geometry inputGeom; + private final boolean isConvex; + + /** + * Compute a minimum-area rectangle for a given {@link Geometry}. + * + * @param inputGeom a Geometry + */ + public MinimumAreaRectangle(Geometry inputGeom) + { + this(inputGeom, false); + } + + /** + * Compute a minimum rectangle for a {@link Geometry}, + * with a hint if the geometry is convex + * (e.g. a convex Polygon or LinearRing, + * or a two-point LineString, or a Point). + * + * @param inputGeom a Geometry which is convex + * @param isConvex true if the input geometry is convex + */ + public MinimumAreaRectangle(Geometry inputGeom, boolean isConvex) + { + this.inputGeom = inputGeom; + this.isConvex = isConvex; + } + + private Geometry getMinimumRectangle() + { + if (inputGeom.isEmpty()) { + return inputGeom.getFactory().createPolygon(); + } + if (isConvex) { + return computeConvex(inputGeom); + } + Geometry convexGeom = (new ConvexHull(inputGeom)).getConvexHull(); + return computeConvex(convexGeom); + } + + private Geometry computeConvex(Geometry convexGeom) + { +//System.out.println("Input = " + geom); + Coordinate[] convexHullPts = null; + if (convexGeom instanceof Polygon) + convexHullPts = ((Polygon) convexGeom).getExteriorRing().getCoordinates(); + else + convexHullPts = convexGeom.getCoordinates(); + + // special cases for lines or points or degenerate rings + if (convexHullPts.length == 0) { + } + else if (convexHullPts.length == 1) { + return inputGeom.getFactory().createPoint(convexHullPts[0].copy()); + } + else if (convexHullPts.length == 2 || convexHullPts.length == 3) { + //-- Min rectangle is a line. Use the diagonal of the extent + return computeMaximumLine(convexHullPts, inputGeom.getFactory()); + } + //TODO: ensure ring is CW + return computeConvexRing(convexHullPts); + } + + /** + * Computes the minimum-area rectangle for a convex ring of {@link Coordinate}s. + *

        + * This algorithm uses the "dual rotating calipers" technique. + * Performance is linear in the number of segments. + * + * @param ring the convex ring to scan + */ + private Polygon computeConvexRing(Coordinate[] ring) + { + // Assert: ring is oriented CW + + double minRectangleArea = Double.MAX_VALUE; + int minRectangleBaseIndex = -1; + int minRectangleDiamIndex = -1; + int minRectangleLeftIndex = -1; + int minRectangleRightIndex = -1; + + //-- start at vertex after first one + int diameterIndex = 1; + int leftSideIndex = 1; + int rightSideIndex = -1; // initialized once first diameter is found + + LineSegment segBase = new LineSegment(); + LineSegment segDiam = new LineSegment(); + // for each segment, find the next vertex which is at maximum distance + for (int i = 0; i < ring.length - 1; i++) { + segBase.p0 = ring[i]; + segBase.p1 = ring[i + 1]; + diameterIndex = findFurthestVertex(ring, segBase, diameterIndex, 0); + + Coordinate diamPt = ring[diameterIndex]; + Coordinate diamBasePt = segBase.project(diamPt); + segDiam.p0 = diamBasePt; + segDiam.p1 = diamPt; + + leftSideIndex = findFurthestVertex(ring, segDiam, leftSideIndex, 1); + + //-- init the max right index + if (i == 0) { + rightSideIndex = diameterIndex; + } + rightSideIndex = findFurthestVertex(ring, segDiam, rightSideIndex, -1); + + double rectWidth = segDiam.distancePerpendicular(ring[leftSideIndex]) + + segDiam.distancePerpendicular(ring[rightSideIndex]); + double rectArea = segDiam.getLength() * rectWidth; + + if (rectArea < minRectangleArea) { + minRectangleArea = rectArea; + minRectangleBaseIndex = i; + minRectangleDiamIndex = diameterIndex; + minRectangleLeftIndex = leftSideIndex; + minRectangleRightIndex = rightSideIndex; + } + } + return Rectangle.createFromSidePts( + ring[minRectangleBaseIndex], ring[minRectangleBaseIndex + 1], + ring[minRectangleDiamIndex], + ring[minRectangleLeftIndex], ring[minRectangleRightIndex], + inputGeom.getFactory()); + } + + private int findFurthestVertex(Coordinate[] pts, LineSegment baseSeg, int startIndex, int orient) + { + double maxDistance = orientedDistance(baseSeg, pts[startIndex], orient); + double nextDistance = maxDistance; + int maxIndex = startIndex; + int nextIndex = maxIndex; + //-- rotate "caliper" while distance from base segment is non-decreasing + while (isFurtherOrEqual(nextDistance, maxDistance, orient)) { + maxDistance = nextDistance; + maxIndex = nextIndex; + + nextIndex = nextIndex(pts, maxIndex); + if (nextIndex == startIndex) + break; + nextDistance = orientedDistance(baseSeg, pts[nextIndex], orient); + } + return maxIndex; + } + + private boolean isFurtherOrEqual(double d1, double d2, int orient) { + switch (orient) { + case 0: return Math.abs(d1) >= Math.abs(d2); + case 1: return d1 >= d2; + case -1: return d1 <= d2; + } + throw new IllegalArgumentException("Invalid orientation index: " + orient); + } + + private static double orientedDistance(LineSegment seg, Coordinate p, int orient) { + double dist = seg.distancePerpendicularOriented(p); + if (orient == 0) { + return Math.abs(dist); + } + return dist; + } + + private static int nextIndex(Coordinate[] ring, int index) + { + index++; + if (index >= ring.length - 1) index = 0; + return index; + } + + /** + * Creates a line of maximum extent from the provided vertices + * @param pts the vertices + * @param factory the geometry factory + * @return the line of maximum extent + */ + private static LineString computeMaximumLine(Coordinate[] pts, GeometryFactory factory) { + //-- find max and min pts for X and Y + Coordinate ptMinX = null; + Coordinate ptMaxX = null; + Coordinate ptMinY = null; + Coordinate ptMaxY = null; + for (Coordinate p : pts) { + if (ptMinX == null || p.getX() < ptMinX.getX()) ptMinX = p; + if (ptMaxX == null || p.getX() > ptMaxX.getX()) ptMaxX = p; + if (ptMinY == null || p.getY() < ptMinY.getY()) ptMinY = p; + if (ptMaxY == null || p.getY() > ptMaxY.getY()) ptMaxY = p; + } + Coordinate p0 = ptMinX; + Coordinate p1 = ptMaxX; + //-- line is vertical - use Y pts + if (p0.getX() == p1.getX()) { + p0 = ptMinY; + p1 = ptMaxY; + } + return factory.createLineString(new Coordinate[] { p0.copy(), p1.copy() }); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java index 91d63375df..e1ea2a7767 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/MinimumDiameter.java @@ -36,31 +36,37 @@ *

          *
        • a line segment representing the minimum diameter *
        • the supporting line segment of the minimum diameter - *
        • the minimum enclosing rectangle of the input geometry. + *
        • the minimum-width rectangle of the input geometry. * The rectangle has width equal to the minimum diameter, and has one side * parallel to the supporting segment. - * In degenerate cases the minimum enclosing geometry may be a LineString or a Point. + * In degenerate cases the rectangle may be a LineString or a Point. + * (Note that this may not be the enclosing rectangle with minimum area; + * use {@link MinimumAreaRectangle} to compute this.) *
        * * * @see ConvexHull + * @see MinimumAreaRectangle * * @version 1.7 */ public class MinimumDiameter { /** - * Gets the minimum rectangular {@link Polygon} which encloses the input geometry. + * Gets the minimum-width rectangular {@link Polygon} which encloses the input geometry + * and is based along the supporting segment. * The rectangle has width equal to the minimum diameter, * and a longer length. * If the convex hull of the input is degenerate (a line or point) * a {@link LineString} or {@link Point} is returned. *

        - * The minimum rectangle can be used as an extremely generalized representation - * for the given geometry. + * This is not necessarily the rectangle with minimum area. + * Use {@link MinimumAreaRectangle} to compute this. * * @param geom the geometry - * @return the minimum rectangle enclosing the geometry + * @return the minimum-width rectangle enclosing the geometry + * + * @see MinimumAreaRectangle */ public static Geometry getMinimumRectangle(Geometry geom) { return (new MinimumDiameter(geom)).getMinimumRectangle(); @@ -217,7 +223,7 @@ private void computeConvexRingMinDiameter(Coordinate[] pts) int currMaxIndex = 1; LineSegment seg = new LineSegment(); - // compute the max distance for all segments in the ring, and pick the minimum + // for each segment, find a vertex at max distance, and pick the minimum for (int i = 0; i < pts.length - 1; i++) { seg.p0 = pts[i]; seg.p1 = pts[i + 1]; @@ -260,16 +266,18 @@ private static int nextIndex(Coordinate[] pts, int index) } /** - * Gets the minimum rectangular {@link Polygon} which encloses the input geometry. + * Gets the rectangular {@link Polygon} which encloses the input geometry + * and is based on the minimum diameter supporting segment. * The rectangle has width equal to the minimum diameter, * and a longer length. * If the convex hull of the input is degenerate (a line or point) * a {@link LineString} or {@link Point} is returned. *

        - * The minimum rectangle can be used as an extremely generalized representation - * for the given geometry. + * This is not necessarily the enclosing rectangle with minimum area. + * + * @return a rectangle enclosing the input (or a line or point if degenerate) * - * @return the minimum rectangle enclosing the input (or a line or point if degenerate) + * @see MinimumAreaRectangle */ public Geometry getMinimumRectangle() { @@ -279,7 +287,7 @@ public Geometry getMinimumRectangle() if (minWidth == 0.0) { //-- Min rectangle is a point if (minBaseSeg.p0.equals2D(minBaseSeg.p1)) { - return inputGeom.getFactory().createPoint(minBaseSeg.p0); + return inputGeom.getFactory().createPoint(minBaseSeg.p0.copy()); } //-- Min rectangle is a line. Use the diagonal of the extent return computeMaximumLine(convexHullPts, inputGeom.getFactory()); diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Rectangle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Rectangle.java new file mode 100644 index 0000000000..a07cbc0bc4 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Rectangle.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineSegment; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; + +class Rectangle { + + /** + * Creates a rectangular {@link Polygon} from a base segment + * defining the position and orientation of one side of the rectangle, and + * three points defining the locations of the line segments + * forming the opposite, left and right sides of the rectangle. + * The base segment and side points must be presented so that the + * rectangle has CW orientation. + *

        + * The rectangle corners are computed as intersections of + * lines, which generally cannot produce exact values. + * If a rectangle corner is determined to coincide with a side point + * the side point value is used to avoid numerical inaccuracy. + *

        + * The first side of the constructed rectangle contains the base segment. + * + * @param baseRightPt the right point of the base segment + * @param baseLeftPt the left point of the base segment + * @param oppositePt the point defining the opposite side + * @param leftSidePt the point defining the left side + * @param rightSidePt the point defining the right side + * @param factory the geometry factory to use + * @return the rectangular polygon + */ + public static Polygon createFromSidePts(Coordinate baseRightPt, Coordinate baseLeftPt, + Coordinate oppositePt, + Coordinate leftSidePt, Coordinate rightSidePt, + GeometryFactory factory) + { + //-- deltas for the base segment provide slope + double dx = baseLeftPt.x - baseRightPt.x; + double dy = baseLeftPt.y - baseRightPt.y; + // Assert: dx and dy are not both zero + + double baseC = computeLineEquationC(dx, dy, baseRightPt); + double oppC = computeLineEquationC(dx, dy, oppositePt); + double leftC = computeLineEquationC(-dy, dx, leftSidePt); + double rightC = computeLineEquationC(-dy, dx, rightSidePt); + + //-- compute lines along edges of rectangle + LineSegment baseLine = createLineForStandardEquation(-dy, dx, baseC); + LineSegment oppLine = createLineForStandardEquation(-dy, dx, oppC); + LineSegment leftLine = createLineForStandardEquation(-dx, -dy, leftC); + LineSegment rightLine = createLineForStandardEquation(-dx, -dy, rightC); + + /** + * Corners of rectangle are the intersections of the + * base and opposite, and left and right lines. + * The rectangle is constructed with CW orientation. + * The first side of the constructed rectangle contains the base segment. + * + * If a corner coincides with a input point + * the exact value is used to avoid numerical inaccuracy. + */ + Coordinate p0 = rightSidePt.equals2D(baseRightPt) ? baseRightPt.copy() + : baseLine.lineIntersection(rightLine); + Coordinate p1 = leftSidePt.equals2D(baseLeftPt) ? baseLeftPt.copy() + : baseLine.lineIntersection(leftLine); + Coordinate p2 = leftSidePt.equals2D(oppositePt) ? oppositePt.copy() + : oppLine.lineIntersection(leftLine); + Coordinate p3 = rightSidePt.equals2D(oppositePt) ? oppositePt.copy() + : oppLine.lineIntersection(rightLine); + + LinearRing shell = factory.createLinearRing( + new Coordinate[] { p0, p1, p2, p3, p0.copy() }); + return factory.createPolygon(shell); + } + + /** + * Computes the constant C in the standard line equation Ax + By = C + * from A and B and a point on the line. + * + * @param a the X coefficient + * @param b the Y coefficient + * @param p a point on the line + * @return the constant C + */ + private static double computeLineEquationC(double a, double b, Coordinate p) + { + return a * p.y - b * p.x; + } + + private static LineSegment createLineForStandardEquation(double a, double b, double c) + { + Coordinate p0; + Coordinate p1; + /* + * Line equation is ax + by = c + * Slope m = -a/b. + * Y-intercept = c/b + * X-intercept = c/a + * + * If slope is low, use constant X values; if high use Y values. + * This handles lines that are vertical (b = 0, m = Inf ) + * and horizontal (a = 0, m = 0). + */ + if (Math.abs(b) > Math.abs(a)) { + //-- abs(m) < 1 + p0 = new Coordinate(0.0, c/b); + p1 = new Coordinate(1.0, c/b - a/b); + } + else { + //-- abs(m) >= 1 + p0 = new Coordinate(c/a, 0.0); + p1 = new Coordinate(c/a - b/a, 1.0); + } + return new LineSegment(p0, p1); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java b/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java index 4a608f1bb1..9dd72a6d1f 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/LineSegment.java @@ -260,14 +260,40 @@ public double distance(Coordinate p) /** * Computes the perpendicular distance between the (infinite) line defined * by this line segment and a point. + * If the segment has zero length this returns the distance between + * the segment and the point. * - * @return the perpendicular distance between the defined line and the given point + * @param p the point to compute the distance to + * @return the perpendicular distance between the line and point */ public double distancePerpendicular(Coordinate p) { + if (p0.equals2D(p1)) + return p0.distance(p); return Distance.pointToLinePerpendicular(p, p0, p1); } + /** + * Computes the oriented perpendicular distance between the (infinite) line + * defined by this line segment and a point. + * The oriented distance is positive if the point on the left of the line, + * and negative if it is on the right. + * If the segment has zero length this returns the distance between + * the segment and the point. + * + * @param p the point to compute the distance to + * @return the oriented perpendicular distance between the line and point + */ + public double distancePerpendicularOriented(Coordinate p) + { + if (p0.equals2D(p1)) + return p0.distance(p); + double dist = distancePerpendicular(p); + if (orientationIndex(p) < 0) + return -dist; + return dist; + } + /** * Computes the {@link Coordinate} that lies a given * fraction along the line defined by this segment. diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumAreaRectanglelTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumAreaRectanglelTest.java new file mode 100644 index 0000000000..48b6746ac4 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumAreaRectanglelTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm; + +import org.locationtech.jts.geom.Geometry; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + + +/** + * @version 1.7 + */ +public class MinimumAreaRectanglelTest extends GeometryTestCase { + + private static final double TOL = 1e-10; + + public static void main(String args[]) { + TestRunner.run(MinimumAreaRectanglelTest.class); + } + + public MinimumAreaRectanglelTest(String name) { super(name); } + + public void testEmpty() { + checkMinRectangle("POLYGON EMPTY", "POLYGON EMPTY"); + } + + public void testLineLengthZero() { + checkMinRectangle("LINESTRING (1 1, 1 1)", "POINT (1 1)"); + } + + public void testLineHorizontal() { + checkMinRectangle("LINESTRING (1 1, 3 1, 5 1, 7 1)", "LINESTRING (1 1, 7 1)"); + } + + public void testLineVertical() { + checkMinRectangle("LINESTRING (1 1, 1 4, 1 7, 1 9)", "LINESTRING (1 1, 1 9)"); + } + + public void testLineObtuseAngle() { + checkMinRectangle("LINESTRING (1 2, 3 8, 9 8)", + "POLYGON ((9 8, 1 2, -1.16 4.88, 6.84 10.88, 9 8))"); + } + + public void testLineAcuteAngle() { + checkMinRectangle("LINESTRING (5 2, 3 8, 9 8)", + "POLYGON ((5 2, 3 8, 8.4 9.8, 10.4 3.8, 5 2))"); + } + + public void testNotMinDiameter() { + checkMinRectangle("POLYGON ((150 300, 200 300, 300 300, 300 250, 280 120, 210 100, 100 100, 100 240, 150 300))", + "POLYGON ((100 100, 100 300, 300 300, 300 100, 100 100))"); + } + + public void testTriangle() { + checkMinRectangle("POLYGON ((100 100, 200 200, 160 240, 100 100))", + "POLYGON ((100 100, 160 240, 208.2758620689651 219.31034482758352, 148.2758620689666 79.31034482758756, 100 100))"); + } + + public void testConvex() { + checkMinRectangle("POLYGON ((3 8, 6 8, 9 5, 7 3, 3 1, 2 4, 3 8))", + "POLYGON ((0.2 6.6, 6.6 9.8, 9.4 4.2, 3 1, 0.2 6.6))"); + } + + /** + * Failure case from https://trac.osgeo.org/postgis/ticket/5163 + * @throws Exception + */ + public void testFlatDiagonal() throws Exception { + checkMinRectangle("LINESTRING(-99.48710639268086 34.79029839231914,-99.48370699999998 34.78689899963806,-99.48152167568102 34.784713675318976)", + "POLYGON ((-99.48710639268066 34.790298392318675, -99.48710639268066 34.790298392318675, -99.48152167568082 34.78471367531866, -99.48152167568082 34.78471367531866, -99.48710639268066 34.790298392318675))"); + } + + public void testBadRectl() throws Exception { + checkMinRectangle("POLYGON ((-5.21175 49.944633, -5.77435 50.021367, -5.7997 50.0306, -5.81815 50.0513, -5.82625 50.073567, -5.83085 50.1173, -6.2741 56.758767, -5.93245 57.909, -5.1158 58.644533, -5.07915 58.661733, -3.42575 58.686633, -3.1392 58.6685, -3.12495 58.666233, -1.88745 57.6444, 1.68845 52.715133, 1.7057 52.6829, 1.70915 52.6522, 1.7034 52.585433, 1.3867 51.214033, 1.36595 51.190267, 1.30485 51.121967, 0.96365 50.928567, 0.93025 50.912433, 0.1925 50.7436, -5.21175 49.944633))", + "POLYGON ((1.8583607388069103 50.41649058582797, -5.816631979932251 49.904263313964535, -6.395241388167441 58.57389735949991, 1.2797513305717105 59.08612463136336, 1.8583607388069103 50.41649058582797))"); + } + + private void checkMinRectangle(String wkt, String wktExpected) { + Geometry geom = read(wkt); + Geometry actual = MinimumAreaRectangle.getMinimumRectangle(geom); + Geometry expected = read(wktExpected); + checkEqual(expected, actual, TOL); + } + + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java deleted file mode 100644 index 5538493a0e..0000000000 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/MinimumRectanglelTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2022 Martin Davis. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * and Eclipse Distribution License v. 1.0 which accompanies this distribution. - * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html - * and the Eclipse Distribution License is available at - * - * http://www.eclipse.org/org/documents/edl-v10.php. - */ -package org.locationtech.jts.algorithm; - -import org.locationtech.jts.geom.Geometry; - -import junit.textui.TestRunner; -import test.jts.GeometryTestCase; - - -/** - * @version 1.7 - */ -public class MinimumRectanglelTest extends GeometryTestCase { - - private static final double TOL = 1e-10; - - public static void main(String args[]) { - TestRunner.run(MinimumRectanglelTest.class); - } - - public MinimumRectanglelTest(String name) { super(name); } - - public void testLengthZero() { - checkMinRectangle("LINESTRING (1 1, 1 1)", "POINT (1 1)"); - } - - public void testHorizontal() { - checkMinRectangle("LINESTRING (1 1, 3 1, 5 1, 7 1)", "LINESTRING (1 1, 7 1)"); - } - - public void testVertical() { - checkMinRectangle("LINESTRING (1 1, 1 4, 1 7, 1 9)", "LINESTRING (1 1, 1 9)"); - } - - public void testBentLine() { - checkMinRectangle("LINESTRING (1 2, 3 8, 9 6)", "POLYGON ((9 6, 7 10, -1 6, 1 2, 9 6))"); - } - - /** - * Failure case from https://trac.osgeo.org/postgis/ticket/5163 - * @throws Exception - */ - public void testFlatDiagonal() throws Exception { - checkMinRectangle("LINESTRING(-99.48710639268086 34.79029839231914,-99.48370699999998 34.78689899963806,-99.48152167568102 34.784713675318976)", - "LINESTRING (-99.48710639268086 34.79029839231914, -99.48152167568102 34.784713675318976)"); - } - - private void checkMinRectangle(String wkt, String wktExpected) { - Geometry geom = read(wkt); - Geometry actual = MinimumDiameter.getMinimumRectangle(geom); - Geometry expected = read(wktExpected); - checkEqual(expected, actual, TOL); - } - - -} diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/RectangleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/RectangleTest.java new file mode 100644 index 0000000000..398efc8a05 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/RectangleTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.LineString; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +public class RectangleTest extends GeometryTestCase +{ + private static final double TOL = 1e-10; + + public static void main(String args[]) { + TestRunner.run(RectangleTest.class); + } + + public RectangleTest(String name) { super(name); } + + public void testOrthogonal() { + checkRectangle("LINESTRING (9 1, 1 1, 0 5, 7 10, 10 6)", + "POLYGON ((0 1, 0 10, 10 10, 10 1, 0 1))"); + } + + public void test45() { + checkRectangle("LINESTRING (10 5, 5 0, 2 1, 2 7, 9 9)", + "POLYGON ((-1 4, 6.5 11.5, 11.5 6.5, 4 -1, -1 4))"); + } + + public void testCoincidentBaseSides() { + checkRectangle("LINESTRING (10 5, 7 0, 7 0, 2 7, 10 5)", + "POLYGON ((0.2352941176470591 4.0588235294117645, 3.2352941176470598 9.058823529411764, 10 5, 7 0, 0.2352941176470591 4.0588235294117645))"); + } + + private void checkRectangle(String wkt, String wktExpected) { + LineString line = (LineString) read(wkt); + Coordinate baseRightPt = line.getCoordinateN(0); + Coordinate baseLeftPt = line.getCoordinateN(1); + Coordinate leftSidePt = line.getCoordinateN(2); + Coordinate oppositePt = line.getCoordinateN(3); + Coordinate rightSidePt = line.getCoordinateN(4); + Geometry actual = Rectangle.createFromSidePts(baseRightPt, baseLeftPt, + oppositePt, leftSidePt, rightSidePt, line.getFactory()); + Geometry expected = read(wktExpected); + checkEqual(expected, actual, TOL); + } +} diff --git a/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java b/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java index ce6c42393a..cfc6115ca8 100644 --- a/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/geom/LineSegmentTest.java @@ -82,6 +82,41 @@ private void checkLineIntersection(double p1x, double p1y, double p2x, double p2 assertTrue(dist <= MAX_ABS_ERROR_INTERSECTION); } + public void testDistancePerpendicular() { + checkDistancePerpendicular(1,1, 1,3, 2,4, 1); + checkDistancePerpendicular(1,1, 1,3, 0,4, 1); + checkDistancePerpendicular(1,1, 1,3, 1,4, 0); + checkDistancePerpendicular(1,1, 2,2, 4,4, 0); + //-- zero-length line segment + checkDistancePerpendicular(1,1, 1,1, 1,2, 1); + } + + public void testDistancePerpendicularOriented() { + //-- right of line + checkDistancePerpendicularOriented(1,1, 1,3, 2,4, -1); + //-- left of line + checkDistancePerpendicularOriented(1,1, 1,3, 0,4, 1); + //-- on line + checkDistancePerpendicularOriented(1,1, 1,3, 1,4, 0); + checkDistancePerpendicularOriented(1,1, 2,2, 4,4, 0); + //-- zero-length segment + checkDistancePerpendicularOriented(1,1, 1,1, 1,2, 1); + } + + private void checkDistancePerpendicular(double x0, double y0, double x1, double y1, double px, double py, + double expected) { + LineSegment seg = new LineSegment(x0, y0, x1, y1); + double dist = seg.distancePerpendicular(new Coordinate(px, py)); + assertEquals(expected, dist, 0.000001); + } + + private void checkDistancePerpendicularOriented(double x0, double y0, double x1, double y1, double px, double py, + double expected) { + LineSegment seg = new LineSegment(x0, y0, x1, y1); + double dist = seg.distancePerpendicularOriented(new Coordinate(px, py)); + assertEquals(expected, dist, 0.000001); + } + public void testOffsetPoint() throws Exception { checkOffsetPoint(0, 0, 10, 10, 0.0, ROOT2, -1, 1); From 762955444259890d2673730065494e29f5d1b6a9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 2 May 2023 20:53:28 -0700 Subject: [PATCH 43/79] Add TestBuilder Save Indicators Signed-off-by: Martin Davis --- .../jtstest/function/FunctionsUtil.java | 10 +--- .../jtstest/testbuilder/AppConstants.java | 4 +- .../jtstest/testbuilder/AppStrings.java | 2 + .../testbuilder/GeometryEditPanel.java | 5 ++ .../testbuilder/JTSTestBuilderFrame.java | 15 ++++- .../testbuilder/JTSTestBuilderMenuBar.java | 22 ++++--- .../jtstest/testbuilder/LayerListPanel.java | 22 +++++-- .../controller/JTSTestBuilderController.java | 24 ++++++++ .../testbuilder/model/GeometryContainer.java | 4 ++ .../jtstest/testbuilder/model/Layer.java | 11 ++++ .../jtstest/testbuilder/model/LayerList.java | 18 ++++++ .../model/ListGeometryContainer.java | 57 +++++++++++++++++++ .../model/StaticGeometryContainer.java | 5 ++ .../testbuilder/model/TestBuilderModel.java | 42 +++++++++++++- .../jts/util/TestBuilderProxy.java | 32 ++++++++--- 15 files changed, 239 insertions(+), 34 deletions(-) create mode 100644 modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/ListGeometryContainer.java diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java b/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java index ce4a683605..f4ee726b45 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java @@ -61,15 +61,7 @@ public static void showIndicator(Geometry geom) public static void showIndicator(Geometry geom, Color lineClr) { - if (! isShowingIndicators()) return; - - GeometryEditPanel panel = JTSTestBuilderFrame - .instance().getTestCasePanel() - .getGeometryEditPanel(); - Graphics2D gr = (Graphics2D) panel.getGraphics(); - GeometryPainter.paint(geom, panel.getViewport(), gr, - lineClr, - AppConstants.INDICATOR_FILL_CLR); + JTSTestBuilder.controller().indicatorShow(geom, lineClr); } public static Geometry buildGeometry(List geoms, Geometry parentGeom) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java index f3fff1d8f3..b927e7c223 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppConstants.java @@ -39,10 +39,10 @@ public class AppConstants public static final Color HIGHLIGHT_FILL_CLR = new Color(255, 240, 192, 200); public static final Color BAND_CLR = new Color(255, 0, 0, 255); - public static final Color INDICATOR_FILL_CLR = GeometryDepiction.GEOM_RESULT_FILL_CLR; + public static final Color INDICATOR_FILL_CLR = new Color(255, 200, 255, 100); + public static final Color INDICATOR_LINE_CLR = new Color(150, 0, 150); //public static final Color INDICATOR_LINE_COLOR = new Color(255, 0, 0, 255); //public static final Color INDICATOR_FILL_COLOR = new Color(255, 200, 200, 200); - public static final Color INDICATOR_LINE_CLR = GeometryDepiction.GEOM_RESULT_LINE_CLR; public static final int AXIS_WIDTH = 3; public static final Color AXIS_CLR = Color.lightGray; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java index 320a09263f..9c8ff00154 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/AppStrings.java @@ -92,6 +92,8 @@ public class AppStrings { public static final String TIP_ALLOW_INVERTED_RINGS = "Allow valid inverted shells and exverted holes"; + public static final String LYR_INDICATORS = "Indicators"; + diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java index 61e30d54d2..3ca39506c5 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryEditPanel.java @@ -434,6 +434,11 @@ private void drawRevealMask(Graphics2D g) { g.fill(mask); } + public void draw(Geometry geom, Color lineClr, Color fillClr) { + Graphics2D gr = (Graphics2D) getGraphics(); + GeometryPainter.paint(geom, getViewport(), gr, lineClr, fillClr); + } + public void flash(Geometry g) { Graphics2D gr = (Graphics2D) getGraphics(); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java index 740e7c7f5f..7e4a44e9c3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderFrame.java @@ -58,6 +58,7 @@ public class JTSTestBuilderFrame extends JFrame private static JTSTestBuilderFrame singleton = null; static boolean isShowingIndicators = true; + static boolean isSavingIndicators = false; TestBuilderModel tbModel; @@ -79,7 +80,6 @@ public class JTSTestBuilderFrame extends JFrame CommandPanel commandPanel = new CommandPanel(); InspectorPanel inspectPanel = new InspectorPanel(); TestListPanel testListPanel = new TestListPanel(this); - //LayerListPanel layerListPanel = new LayerListPanel(); LayerListPanel layerListPanel = new LayerListPanel(); GridBagLayout gridBagLayout2 = new GridBagLayout(); GridLayout gridLayout1 = new GridLayout(); @@ -166,7 +166,9 @@ public static boolean isRunning() { public static boolean isShowingIndicators() { return isRunning() && isShowingIndicators; } - + public static boolean isSavingIndicators() { + return isRunning() && isSavingIndicators; + } public static GeometryEditPanel getGeometryEditPanel() { return instance().getTestCasePanel().getGeometryEditPanel(); @@ -337,6 +339,11 @@ public void inspectGeometry() { inspectGeometry(geometry, geomIndex, tag, true); } + public void inspectGeometry(String tag, Geometry geometry) { + inspectPanel.setGeometry( tag, geometry, 0, false); + showTab(AppStrings.TAB_LABEL_INSPECT); + } + private void inspectGeometry(Geometry geometry, int geomIndex, String tag, boolean isEditable) { inspectPanel.setGeometry( tag, geometry, geomIndex, isEditable); showTab(AppStrings.TAB_LABEL_INSPECT); @@ -513,6 +520,10 @@ public void updateLayerList() { layerListPanel.updateList(); } + public void refreshLayerList() { + layerListPanel.populateList(); + } + private void reportProblemsParsingXmlTestFile(List parsingProblems) { if (parsingProblems.isEmpty()) { return; diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderMenuBar.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderMenuBar.java index e088d9e160..a59ecf46c9 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderMenuBar.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderMenuBar.java @@ -82,13 +82,20 @@ public void actionPerformed(ActionEvent e) { JTSTestBuilder.controller().inspectGeometryDialogForCurrentCase(); } }); - JMenuItem menuShowIndicators = menuItemCheck("ShowIndicators", - JTSTestBuilderFrame.isShowingIndicators, - new java.awt.event.ActionListener() { - public void actionPerformed(ActionEvent e) { - JTSTestBuilderFrame.isShowingIndicators = ! JTSTestBuilderFrame.isShowingIndicators; - } - }); + JMenuItem menuShowIndicators = menuItemCheck("Show Indicators", + JTSTestBuilderFrame.isShowingIndicators, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + JTSTestBuilderFrame.isShowingIndicators = ! JTSTestBuilderFrame.isShowingIndicators; + } + }); + JMenuItem menuSaveIndicators = menuItemCheck("Save Indicators", + JTSTestBuilderFrame.isSavingIndicators, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + JTSTestBuilderFrame.isSavingIndicators = ! JTSTestBuilderFrame.isSavingIndicators; + } + }); menuLoadXmlTestFile.setText("Open XML File(s)..."); menuLoadXmlTestFile.addActionListener( new java.awt.event.ActionListener() { @@ -192,6 +199,7 @@ public void actionPerformed(ActionEvent e) { //----------------------- jMenuEdit.addSeparator(); jMenuView.add(menuShowIndicators); + jMenuView.add(menuSaveIndicators); //========================== jMenuEdit.setText("Edit"); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerListPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerListPanel.java index 1a6c058edf..1894ce3c76 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerListPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/LayerListPanel.java @@ -57,13 +57,10 @@ public class LayerListPanel extends JPanel { List layerItems = new ArrayList(); private JButton btnCopy; - + private JButton btnInspect; private JButton btnUp; - private JButton btnDown; - private JButton btnDelete; - private JButton btnPaste; private Layer focusLayer; @@ -106,6 +103,15 @@ public void actionPerformed(ActionEvent e) { }); buttonPanel.add(btnCopy); + btnInspect = SwingUtil.createButton(AppIcons.GEOM_INSPECT, + "Inspect layer geometry", + new ActionListener() { + public void actionPerformed(ActionEvent e) { + layerInspect(); + } + }); + buttonPanel.add(btnInspect); + btnPaste = SwingUtil.createButton(AppIcons.PASTE, "Paste geometry into layer", new ActionListener() { @@ -149,6 +155,7 @@ public void actionPerformed(ActionEvent e) { lyrStylePanel = new LayerStylePanel(); GeometryViewStylePanel viewStylePanel = new GeometryViewStylePanel(); + //add(lyrStylePanel, BorderLayout.CENTER); //tabFunctions.setBackground(jTabbedPane1.getBackground()); @@ -228,6 +235,10 @@ private void layerCopy() { JTSTestBuilder.controller().geometryViewChanged(); } + private void layerInspect() { + JTSTestBuilder.controller().inspectGeometry(focusLayer.getName(), focusLayer.getGeometry()); + } + private void layerDelete(Layer lyr) { // don't remove if non-empty if (lyr.hasGeometry()) return; @@ -252,8 +263,7 @@ private void layerDown(Layer lyr) { } private void layerClear(Layer lyr) { - StaticGeometryContainer src = (StaticGeometryContainer) lyr.getSource(); - src.setGeometry(null); + lyr.getSource().clear(); updateButtons(focusLayer); JTSTestBuilder.controller().geometryViewChanged(); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java index abf3e26d54..38f1d8e866 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java @@ -13,12 +13,16 @@ package org.locationtech.jtstest.testbuilder.controller; +import java.awt.Color; + import javax.swing.JFileChooser; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.util.LinearComponentExtracter; import org.locationtech.jtstest.clean.CleanDuplicatePoints; +import org.locationtech.jtstest.testbuilder.AppConstants; +import org.locationtech.jtstest.testbuilder.AppStrings; import org.locationtech.jtstest.testbuilder.GeometryEditPanel; import org.locationtech.jtstest.testbuilder.JTSTestBuilder; import org.locationtech.jtstest.testbuilder.JTSTestBuilderFrame; @@ -173,6 +177,10 @@ public void inspectResult() JTSTestBuilderFrame.instance().inspectResult(); } + public void inspectGeometry(String tag, Geometry geometry) { + JTSTestBuilderFrame.instance().inspectGeometry(tag, geometry); + } + public void inspectGeometryDialogForCurrentCase() { int geomIndex = JTSTestBuilder.model().getGeometryEditModel().getGeomIndex(); @@ -368,4 +376,20 @@ public void changeToLines() { model().getCurrentCase().setGeometry(0, cleanGeom); frame().geometryChanged(); } + + //============================================= + + public void indicatorShow(Geometry geom, Color lineClr) + { + if (! JTSTestBuilderFrame.isShowingIndicators()) return; + + if (JTSTestBuilderFrame.isSavingIndicators()) { + //-- refresh layer list panel only when indicator layer is created + boolean refreshLayerList = ! model().hasLayer(AppStrings.LYR_INDICATORS); + model().addIndicator(geom); + if (refreshLayerList) + frame().refreshLayerList(); + } + editPanel().draw(geom, lineClr, AppConstants.INDICATOR_FILL_CLR); + } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/GeometryContainer.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/GeometryContainer.java index b4f903d72e..61502f3592 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/GeometryContainer.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/GeometryContainer.java @@ -16,4 +16,8 @@ public interface GeometryContainer { Geometry getGeometry(); + + default void clear() { + + } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/Layer.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/Layer.java index 0b017a0e81..1d03e33885 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/Layer.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/Layer.java @@ -31,6 +31,12 @@ public Layer(String name) { this.name = name; } + public Layer(String name, GeometryContainer source, BasicStyle style) { + this.name = name; + setSource(source); + setGeometryStyle(style); + } + public Layer(Layer layer) { this.name = layer.name + "Copy"; this.layerStyle = layer.layerStyle.copy(); @@ -97,6 +103,11 @@ public Geometry getGeometry() return geomCont.getGeometry(); } + public void setGeometry(Geometry geom) + { + this.geomCont = new StaticGeometryContainer(geom); + } + public Envelope getEnvelope() { if (hasGeometry()) return getGeometry().getEnvelopeInternal(); return new Envelope(); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java index 6176d544c7..ecf525e61e 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/LayerList.java @@ -129,6 +129,16 @@ private List extractLocationGeometry(List locs) return geoms; } + public Layer add(Layer lyr, boolean atTop) { + if (atTop) { + layers.add(0, lyr); + } + else { + layers.add(lyr); + } + return lyr; + } + public Layer copy(Layer focusLayer) { Layer lyr = new Layer(focusLayer); layers.add(lyr); @@ -181,4 +191,12 @@ public void moveDown(Layer lyr) { layers.set(i+1, lyr); layers.set(i, tmp); } + + public Layer find(String name) { + for (Layer lyr : layers) { + if (lyr.getName().equals(name)) + return lyr; + } + return null; + } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/ListGeometryContainer.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/ListGeometryContainer.java new file mode 100644 index 0000000000..4ebd23c1eb --- /dev/null +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/ListGeometryContainer.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jtstest.testbuilder.model; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; + +public class ListGeometryContainer implements GeometryContainer { + + private List geomList = new ArrayList(); + private Geometry cache; + + public ListGeometryContainer() { + } + + public void add(Geometry geom) { + geomList.add(geom); + cache = null; + } + + public void clear() { + cache = null; + geomList.clear(); + } + + public Geometry getGeometry() { + if ( cache == null ) { + cache = createCache(geomList); + } + return cache; + } + + private static Geometry createCache(List geomList) { + if (geomList.size() == 0) + return null; + if ( geomList.size() == 1 ) { + return geomList.get(0); + } + // TODO: use common TestBuilder factory + GeometryFactory geomFact = new GeometryFactory(); + return geomFact.createGeometryCollection(GeometryFactory.toGeometryArray(geomList)); + } + +} diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/StaticGeometryContainer.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/StaticGeometryContainer.java index 6a96f44ec1..219d5aa3e8 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/StaticGeometryContainer.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/StaticGeometryContainer.java @@ -31,4 +31,9 @@ public void setGeometry(Geometry geom) { geometry = geom;; } + @Override + public void clear() { + geometry = null; + } + } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java index 6f39c81820..5d09c5ca04 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/model/TestBuilderModel.java @@ -30,6 +30,8 @@ import org.locationtech.jts.util.Assert; import org.locationtech.jtstest.test.TestCaseList; import org.locationtech.jtstest.test.Testable; +import org.locationtech.jtstest.testbuilder.AppConstants; +import org.locationtech.jtstest.testbuilder.AppStrings; import org.locationtech.jtstest.testbuilder.GeometryDepiction; import org.locationtech.jtstest.testbuilder.ui.SwingUtil; import org.locationtech.jtstest.testbuilder.ui.style.BasicStyle; @@ -112,6 +114,44 @@ private void addLegendLayers(LayerList layerList, List layers) { } } + public Layer getLayerIndicators() { + Layer ind = layerListTop.find(AppStrings.LYR_INDICATORS); + if (ind == null) + ind = layerListBase.find(AppStrings.LYR_INDICATORS); + if (ind == null) { + ind = createIndicatorLayer(); + layerListBase.add(ind, true); + } + return ind; + } + + private Layer createIndicatorLayer() { + Layer ind = new Layer(AppStrings.LYR_INDICATORS, + new ListGeometryContainer(), + new BasicStyle(AppConstants.INDICATOR_LINE_CLR, + AppConstants.INDICATOR_FILL_CLR)); + ind.getLayerStyle().setVertices(false); + return ind; + } + + public void addIndicator(Geometry geom) { + Layer lyr = getLayerIndicators(); + ListGeometryContainer src = (ListGeometryContainer) lyr.getSource(); + src.add(geom); + } + + public boolean hasLayer(String name) { + return findLayer(name) != null; + } + + private Layer findLayer(String name) { + Layer lyr = layerListTop.find(name); + if (lyr != null) return lyr; + lyr = layerListBase.find(name); + if (lyr != null) return lyr; + return layerList.find(name); + } + private void initLayers() { GeometryContainer geomCont0 = new IndexedGeometryContainer(geomEditModel, 0); @@ -516,7 +556,7 @@ public void deleteCase() { } } - + public Layer layerCopy(Layer lyr) { if (layerListTop.contains(lyr)) { return layerListTop.copy(lyr); diff --git a/modules/core/src/main/java/org/locationtech/jts/util/TestBuilderProxy.java b/modules/core/src/main/java/org/locationtech/jts/util/TestBuilderProxy.java index 07cf1824ea..599cc9afcd 100644 --- a/modules/core/src/main/java/org/locationtech/jts/util/TestBuilderProxy.java +++ b/modules/core/src/main/java/org/locationtech/jts/util/TestBuilderProxy.java @@ -17,10 +17,14 @@ import org.locationtech.jts.geom.Geometry; /** - * A proxy to call TestBuilder functions dynamically. - * If TestBuilder is not present, functions act as a no-op. + * A proxy to call TestBuilder functions. + * If the code is not being run in the context of the + * TestBuilder, functions act as a no-op. *

        - * This class is somewhat experimental at the moment, so + * It is recommended that functions only be inserted into + * code temporarily (i.e. in a development environment). + *

        + * This class is experimental, and * is not recommended for production use. * * @author Martin Davis @@ -48,7 +52,7 @@ private static void init() { /** * Tests whether the proxy is active (i.e. the TestBuilder is available). - * This allows avoiding expensive geometry materialization if not needed. + * This allows avoiding expensive geometry creation if not needed. * * @return true if the proxy is active */ @@ -57,9 +61,14 @@ public static boolean isActive() { return tbClass != null; } - // TODO: expose an option in the TestBuilder to make this inactive - // This will avoid a huge performance hit if the visualization is not needed - + /** + * Shows a geometry as an indicator in the TestBuilder Edit panel. + * The geometry is only displayed until the next screen refresh. + * The TestBuilder also provides a menu option to capture + * indicators on a layer. + * + * @param geom the geometry to display + */ public static void showIndicator(Geometry geom) { init(); if (methodShowIndicator == null) return; @@ -71,6 +80,15 @@ public static void showIndicator(Geometry geom) { // Or perhaps should fail noisy, since at this point the function should be working? } } + + /** + * Shows a geometry as an indicator in the TestBuilder Edit panel. + * The geometry is only displayed until the next screen refresh. + * The TestBuilder also provides a menu option to capture + * indicators on a layer. + * + * @param geom the geometry to display + */ public static void showIndicator(Geometry geom, Color lineClr) { init(); if (methodShowIndicatorLine == null) return; From ba7fe45797eb11635c1256b63aaeda5a8eb3fcaf Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 3 May 2023 07:03:46 -0700 Subject: [PATCH 44/79] Improve TestBuilder Inspector Signed-off-by: Martin Davis --- .../testbuilder/GeometryTreeModel.java | 24 ++--- .../testbuilder/GeometryTreePanel.java | 9 +- .../jtstest/testbuilder/InspectorPanel.java | 99 ++++++++++--------- .../controller/JTSTestBuilderController.java | 5 + 4 files changed, 77 insertions(+), 60 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java index 6912e715b0..cfcb246a1e 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreeModel.java @@ -37,10 +37,10 @@ public class GeometryTreeModel implements TreeModel { - public static Comparator SORT_AREA_ASC = new AreaComparator(false); - public static Comparator SORT_AREA_DESC = new AreaComparator(true); - public static Comparator SORT_LEN_ASC = new LengthComparator(false); - public static Comparator SORT_LEN_DESC = new LengthComparator(true); + public static Comparator SORT_AREA_ASC = new AreaComparator(false); + public static Comparator SORT_AREA_DESC = new AreaComparator(true); + public static Comparator SORT_LEN_ASC = new LengthComparator(false); + public static Comparator SORT_LEN_DESC = new LengthComparator(true); private Vector treeModelListeners = new Vector(); @@ -123,7 +123,7 @@ public void valueForPathChanged(TreePath path, Object newValue) .println("*** valueForPathChanged : " + path + " --> " + newValue); } - public static class AreaComparator implements Comparator { + public static class AreaComparator implements Comparator { private int dirFactor; @@ -132,13 +132,13 @@ public AreaComparator(boolean direction) { } @Override - public int compare(Object o1, Object o2) { - double area1 = ((GeometricObjectNode) o1).getGeometry().getArea(); - double area2 = ((GeometricObjectNode) o2).getGeometry().getArea(); + public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { + double area1 = o1.getGeometry().getArea(); + double area2 = o2.getGeometry().getArea(); return dirFactor * Double.compare(area1, area2); } } - public static class LengthComparator implements Comparator { + public static class LengthComparator implements Comparator { private int dirFactor; @@ -147,9 +147,9 @@ public LengthComparator(boolean direction) { } @Override - public int compare(Object o1, Object o2) { - double area1 = ((GeometricObjectNode) o1).getGeometry().getLength(); - double area2 = ((GeometricObjectNode) o2).getGeometry().getLength(); + public int compare(GeometricObjectNode o1, GeometricObjectNode o2) { + double area1 = o1.getGeometry().getLength(); + double area2 = o2.getGeometry().getLength(); return dirFactor * Double.compare(area1, area2); } } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreePanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreePanel.java index b4fb0cf134..6d8ab5ae75 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreePanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/GeometryTreePanel.java @@ -103,7 +103,7 @@ public void mouseClicked(MouseEvent e) { } // would be nice to flash as well as zoom, but zooming drawing is too slow if (e.getClickCount() == 1) { - JTSTestBuilderFrame.getGeometryEditPanel().flash(geom); + JTSTestBuilder.controller().flash(geom); } } }); @@ -115,12 +115,19 @@ public void valueChanged(TreeSelectionEvent e) { } }); } + /** + * Gets currently selected geometry, if any. + * + * @return selected geometry, or null if none selected + */ public Geometry getSelectedGeometry() { return getGeometryFromNode(tree.getLastSelectedPathComponent()); } public void moveToNextNode(int direction) { direction = (int) Math.signum(direction); TreePath path = tree.getSelectionPath(); + if (path == null) + return; TreePath nextPath2 = nextPath(path, 2 * direction); tree.scrollPathToVisible(nextPath2); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java index b6ef94ea12..62fd8f2162 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/InspectorPanel.java @@ -46,9 +46,9 @@ public class InspectorPanel extends TestBuilderPanel { private Geometry geometry; - private Comparator sorterArea; + private Comparator sorterArea; - private Comparator sorterLen; + private Comparator sorterLen; public InspectorPanel() { this(true); @@ -69,22 +69,22 @@ protected void uiInit() { JButton btnZoom = SwingUtil.createButton(AppIcons.ZOOM, "Zoom to component", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - btnZoom_actionPerformed(e); + actionZoom(e); } }); - JButton btnCopy = SwingUtil.createButton(AppIcons.COPY, "Copy (Ctl-click to copy formatted", new java.awt.event.ActionListener() { + JButton btnCopy = SwingUtil.createButton(AppIcons.COPY, "Copy (Ctl-click to Copy formatted", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - btnCopy_actionPerformed(e); + actionCopy(e); } }); - JButton btnNext = SwingUtil.createButton(AppIcons.DOWN, "Zoom to Next", new java.awt.event.ActionListener() { + JButton btnNext = SwingUtil.createButton(AppIcons.DOWN, "Next (Ctl-click to Zoom)", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - btnZoomNext_actionPerformed(e, 1); + actionZoomNext(e, 1); } }); - JButton btnPrev = SwingUtil.createButton(AppIcons.UP, "Zoom to Previous", new java.awt.event.ActionListener() { + JButton btnPrev = SwingUtil.createButton(AppIcons.UP, "Previous (Ctl-click to Zoom)", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - btnZoomNext_actionPerformed(e, -1); + actionZoomNext(e, -1); } }); btnDelete = SwingUtil.createButton(AppIcons.DELETE, "Delete", new java.awt.event.ActionListener() { @@ -113,24 +113,6 @@ public void actionPerformed(ActionEvent e) { btnPanel.add(btnDelete); this.add(btnPanel, BorderLayout.WEST); - if (showExpand) { - JPanel btn2Panel = new JPanel(); - btn2Panel.setLayout(new BoxLayout(btn2Panel, BoxLayout.PAGE_AXIS)); - btn2Panel.setPreferredSize(new java.awt.Dimension(30, 30)); - btnExpand.setEnabled(true); - btnExpand.setMaximumSize(new Dimension(30, 30)); - btnExpand.setText("..."); - btnExpand.setToolTipText("Display in window"); - btnExpand.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(ActionEvent e) - { - btnExpand_actionPerformed(); - } - }); - btn2Panel.add(btnExpand); - this.add(btn2Panel, BorderLayout.EAST); - } - JButton btnSortNone = SwingUtil.createButton(AppIcons.CLEAR, "Unsorted", new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { sortNone(); @@ -147,33 +129,55 @@ public void actionPerformed(ActionEvent e) { } }); - JPanel sortPanel = new JPanel(); - sortPanel.setLayout(new BoxLayout(sortPanel, BoxLayout.LINE_AXIS)); - sortPanel.add(Box.createRigidArea(new Dimension(160, 0))); - sortPanel.add(new JLabel("Sort")); - sortPanel.add(Box.createRigidArea(new Dimension(10, 0))); - sortPanel.add(btnSortNone); - //sortPanel.add(new JLabel(AppIcons.ICON_LINESTRING)); - sortPanel.add(Box.createRigidArea(new Dimension(10, 0))); - sortPanel.add(btnSortByLen); - //sortPanel.add(new JLabel(AppIcons.ICON_POLYGON)); - sortPanel.add(Box.createRigidArea(new Dimension(10, 0))); - sortPanel.add(btnSortByArea); - this.add(sortPanel, BorderLayout.NORTH); - + JPanel btn2Panel = new JPanel(); + btn2Panel.setLayout(new BoxLayout(btn2Panel, BoxLayout.PAGE_AXIS)); + btn2Panel.setPreferredSize(new java.awt.Dimension(30, 30)); + btnExpand.setMaximumSize(new Dimension(30, 30)); + btnExpand.setText("..."); + btnExpand.setToolTipText("Display in window"); + btnExpand.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) + { + btnExpand_actionPerformed(); + } + }); + if (showExpand) { + btnExpand.setEnabled(true); + } + btn2Panel.add(btnExpand); + + btn2Panel.add(Box.createRigidArea(new Dimension(0, 10))); + btn2Panel.add(new JLabel("Sort")); + btn2Panel.add(btnSortByLen); + btn2Panel.add(btnSortByArea); + btn2Panel.add(btnSortNone); + this.add(btn2Panel, BorderLayout.EAST); } private void btnExpand_actionPerformed() { JTSTestBuilder.controller().inspectGeometryDialogForCurrentCase(); } - private void btnZoom_actionPerformed(ActionEvent e) { - JTSTestBuilderFrame.getGeometryEditPanel().zoom(geomTreePanel.getSelectedGeometry()); + private void actionZoom(ActionEvent e) { + Geometry geom = geomTreePanel.getSelectedGeometry(); + JTSTestBuilderFrame.getGeometryEditPanel().zoom(geom); + //-- would be nice to flash, but zoom is too slow + //JTSTestBuilder.controller().flash(geom); } - private void btnZoomNext_actionPerformed(ActionEvent e, int direction) { + private void actionZoomNext(ActionEvent e, int direction) { + boolean isZoom = SwingUtil.isCtlKeyPressed(e); geomTreePanel.moveToNextNode(direction); - JTSTestBuilderFrame.getGeometryEditPanel().zoom(geomTreePanel.getSelectedGeometry()); + Geometry geom = geomTreePanel.getSelectedGeometry(); + if (geom == null) + return; + if (isZoom) { + JTSTestBuilderFrame.getGeometryEditPanel().zoom(geom); + //-- would be nice to flash, but zoom is too slow + } + else { + JTSTestBuilder.controller().flash(geom); + } } - private void btnCopy_actionPerformed(ActionEvent e) { - boolean isFormatted = 0 != (e.getModifiers() & ActionEvent.CTRL_MASK); + private void actionCopy(ActionEvent e) { + boolean isFormatted = SwingUtil.isCtlKeyPressed(e); Geometry geom = geomTreePanel.getSelectedGeometry(); if (geom == null) return; SwingUtil.copyToClipboard(geom, isFormatted); @@ -194,6 +198,7 @@ public void setGeometry(String tag, Geometry geom, int source, boolean isEditabl btnDelete.setEnabled(isEditable); lblGeom.setText(tag); + lblGeom.setToolTipText(tag); lblGeom.setForeground(source == 0 ? Color.BLUE : Color.RED); sortNone(); diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java index 38f1d8e866..222a5fde2d 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java @@ -167,6 +167,11 @@ public void setFocusGeometry(int index) { toolbar().setFocusGeometry(index); } + public void flash(Geometry geom) + { + JTSTestBuilderFrame.getGeometryEditPanel().flash(geom); + } + public void inspectGeometry() { JTSTestBuilderFrame.instance().inspectGeometry(); From efb3a90ed87a034470be89782909985ca4bd60fb Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 4 May 2023 21:11:37 -0700 Subject: [PATCH 45/79] Add commented Indicator display Signed-off-by: Martin Davis --- .../jts/algorithm/construct/MaximumInscribedCircle.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java index 973a307301..52cff69fb4 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/MaximumInscribedCircle.java @@ -231,8 +231,10 @@ private void compute() { iter++; // pick the most promising cell from the queue Cell cell = cellQueue.remove(); + //System.out.println(factory.toGeometry(cell.getEnvelope())); //System.out.println(iter + "] Dist: " + cell.getDistance() + " Max D: " + cell.getMaxDistance() + " size: " + cell.getHSide()); + //TestBuilderProxy.showIndicator(inputGeom.getFactory().toGeometry(cell.getEnvelope())); //-- if cell must be closer than furthest, terminate since all remaining cells in queue are even closer. if (cell.getMaxDistance() < farthestCell.getDistance()) From 9830c1bb1478c23fcb694fb98a4dd1a11327d57f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 4 May 2023 21:24:32 -0700 Subject: [PATCH 46/79] Refactor OverlayNG unit test Signed-off-by: Martin Davis --- .../overlayng/OverlayNGInvalidTest.java | 68 +++++-------------- .../overlayng/OverlayNGTestCase.java | 44 ++++++++++++ 2 files changed, 62 insertions(+), 50 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGInvalidTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGInvalidTest.java index 38f08efd96..0dbfc8848c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGInvalidTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGInvalidTest.java @@ -11,16 +11,7 @@ */ package org.locationtech.jts.operation.overlayng; -import static org.locationtech.jts.operation.overlayng.OverlayNG.DIFFERENCE; -import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; -import static org.locationtech.jts.operation.overlayng.OverlayNG.SYMDIFFERENCE; -import static org.locationtech.jts.operation.overlayng.OverlayNG.UNION; - -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.PrecisionModel; - import junit.textui.TestRunner; -import test.jts.GeometryTestCase; /** * Tests OverlayNG handling invalid geometry. @@ -29,7 +20,7 @@ * @author mdavis * */ -public class OverlayNGInvalidTest extends GeometryTestCase { +public class OverlayNGInvalidTest extends OverlayNGTestCase { public static void main(String args[]) { TestRunner.run(OverlayNGInvalidTest.class); @@ -38,55 +29,32 @@ public static void main(String args[]) { public OverlayNGInvalidTest(String name) { super(name); } public void testPolygonFlatIntersection() { - Geometry a = read("POLYGON ((10 40, 40 40, 40 10, 10 10, 10 40))"); - Geometry b = read("POLYGON ((50 30, 19 30, 50 30))"); - Geometry expected = read("LINESTRING (40 30, 19 30)"); - checkEqualExact(expected, intersection(a, b)); + checkIntersection( + "POLYGON ((10 40, 40 40, 40 10, 10 10, 10 40))", + "POLYGON ((50 30, 19 30, 50 30))", + "LINESTRING (40 30, 19 30)"); } public void testPolygonAdjacentElementIntersection() { - Geometry a = read("MULTIPOLYGON (((10 10, 10 40, 40 40, 40 10, 10 10)), ((70 10, 40 10, 40 40, 70 40, 70 10)))"); - Geometry b = read("POLYGON ((20 50, 60 50, 60 20, 20 20, 20 50))"); - Geometry expected = read("POLYGON ((40 40, 60 40, 60 20, 40 20, 20 20, 20 40, 40 40))"); - checkEqualExact(expected, intersection(a, b)); + checkIntersection( + "MULTIPOLYGON (((10 10, 10 40, 40 40, 40 10, 10 10)), ((70 10, 40 10, 40 40, 70 40, 70 10)))", + "POLYGON ((20 50, 60 50, 60 20, 20 20, 20 50))", + "POLYGON ((40 40, 60 40, 60 20, 40 20, 20 20, 20 40, 40 40))"); } public void testPolygonInvertedIntersection() { - Geometry a = read("POLYGON ((10 40, 70 40, 70 0, 40 0, 50 20, 30 20, 40 0, 10 0, 10 40))"); - Geometry b = read("POLYGON ((20 50, 60 50, 60 10, 20 10, 20 50))"); - Geometry expected = read("POLYGON ((60 40, 60 10, 45 10, 50 20, 30 20, 35 10, 20 10, 20 40, 60 40))"); - checkEqualExact(expected, intersection(a, b)); + checkIntersection( + "POLYGON ((10 40, 70 40, 70 0, 40 0, 50 20, 30 20, 40 0, 10 0, 10 40))", + "POLYGON ((20 50, 60 50, 60 10, 20 10, 20 50))", + "POLYGON ((60 40, 60 10, 45 10, 50 20, 30 20, 35 10, 20 10, 20 40, 60 40))"); } // AKA self-touching polygon public void testPolygonExvertedIntersection() { - Geometry a = read("POLYGON ((10 30, 70 30, 70 0, 40 30, 10 0, 10 30))"); - Geometry b = read("POLYGON ((20 50, 60 50, 60 10, 20 10, 20 50))"); - Geometry expected = read("MULTIPOLYGON (((40 30, 20 10, 20 30, 40 30)), ((60 30, 60 10, 40 30, 60 30)))"); - checkEqualExact(expected, intersection(a, b)); - } - - //============================================================ - - - public static Geometry difference(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, DIFFERENCE, pm); - } - - public static Geometry symDifference(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, SYMDIFFERENCE, pm); - } - - public static Geometry intersection(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, INTERSECTION, pm); + checkIntersection( + "POLYGON ((10 30, 70 30, 70 0, 40 30, 10 0, 10 30))", + "POLYGON ((20 50, 60 50, 60 10, 20 10, 20 50))", + "MULTIPOLYGON (((40 30, 20 10, 20 30, 40 30)), ((60 30, 60 10, 40 30, 60 30)))"); } - - public static Geometry union(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, UNION, pm); - } - + } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTestCase.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTestCase.java index 19e69e8032..1b15e78c5a 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTestCase.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTestCase.java @@ -1,6 +1,8 @@ package org.locationtech.jts.operation.overlayng; +import static org.locationtech.jts.operation.overlayng.OverlayNG.DIFFERENCE; import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; +import static org.locationtech.jts.operation.overlayng.OverlayNG.SYMDIFFERENCE; import static org.locationtech.jts.operation.overlayng.OverlayNG.UNION; import org.locationtech.jts.geom.Geometry; @@ -30,4 +32,46 @@ protected void checkOverlay(String wktA, String wktB, int overlayOp, String wktE Geometry expected = read(wktExpected); checkEqual(expected, actual); } + + static Geometry difference(Geometry a, Geometry b) { + PrecisionModel pm = new PrecisionModel(); + return OverlayNG.overlay(a, b, DIFFERENCE, pm); + } + + static Geometry symDifference(Geometry a, Geometry b) { + PrecisionModel pm = new PrecisionModel(); + return OverlayNG.overlay(a, b, SYMDIFFERENCE, pm); + } + + static Geometry intersection(Geometry a, Geometry b) { + PrecisionModel pm = new PrecisionModel(); + return OverlayNG.overlay(a, b, INTERSECTION, pm); + } + + static Geometry union(Geometry a, Geometry b) { + PrecisionModel pm = new PrecisionModel(); + return OverlayNG.overlay(a, b, UNION, pm); + } + + public static Geometry difference(Geometry a, Geometry b, double scaleFactor) { + PrecisionModel pm = new PrecisionModel(scaleFactor); + return OverlayNG.overlay(a, b, DIFFERENCE, pm); + } + + public static Geometry symDifference(Geometry a, Geometry b, double scaleFactor) { + PrecisionModel pm = new PrecisionModel(scaleFactor); + return OverlayNG.overlay(a, b, SYMDIFFERENCE, pm); + } + + public static Geometry intersection(Geometry a, Geometry b, double scaleFactor) { + PrecisionModel pm = new PrecisionModel(scaleFactor); + return OverlayNG.overlay(a, b, INTERSECTION, pm); + } + + public static Geometry union(Geometry a, Geometry b, double scaleFactor) { + PrecisionModel pm = new PrecisionModel(scaleFactor); + return OverlayNG.overlay(a, b, UNION, pm); + } + + } From 198c49c9fa7a65a2f42a8fc651fd0525b149a553 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 5 May 2023 14:58:12 -0700 Subject: [PATCH 47/79] Update DEVELOPING.md --- DEVELOPING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DEVELOPING.md b/DEVELOPING.md index 15cd946ae7..b43a8dd447 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -93,7 +93,7 @@ The XML test format can be executed using the **JTS TestRunner**, or imported in ## Eclipse Configuration -Project: +### Project 1. Startup eclipse, creating a new `jts-workspace` location. This folder is used by eclipse to keep track of settings alongside your jts source code. @@ -107,7 +107,7 @@ Project: Do not try the *maven-checkstyle-plugin* connector as it fails to install. -Plugins: +### Plugins * Install *Eclipse-CS* from the market place. @@ -149,7 +149,7 @@ Plugins: You can use *PMD > Check code* to list errors and warnings. The results are shown in their own view, and quickfixes are not available. -Run Configurations: +### Run Configurations * **JTS TestRunner** - for executing XML tests: From 78feb6c98c332eb1d1c6da2c584bbc51aae47dc7 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Sun, 7 May 2023 10:06:31 -0700 Subject: [PATCH 48/79] Refactor OverlayNG unit tests --- .../overlayng/OverlayNGEmptyDisjointTest.java | 132 ++++++++++++++++++ .../operation/overlayng/OverlayNGTest.java | 103 +------------- 2 files changed, 133 insertions(+), 102 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGEmptyDisjointTest.java diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGEmptyDisjointTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGEmptyDisjointTest.java new file mode 100644 index 0000000000..5913856342 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGEmptyDisjointTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.operation.overlayng; + +import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.PrecisionModel; + +import junit.textui.TestRunner; + +public class OverlayNGEmptyDisjointTest extends OverlayNGTestCase { + + public static void main(String args[]) { + TestRunner.run(OverlayNGEmptyDisjointTest.class); + } + + public OverlayNGEmptyDisjointTest(String name) { super(name); } + + public void testEmptyGCBothIntersection() { + Geometry a = read("GEOMETRYCOLLECTION EMPTY"); + Geometry b = read("GEOMETRYCOLLECTION EMPTY"); + Geometry expected = read("GEOMETRYCOLLECTION EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyAPolygonIntersection() { + Geometry a = read("POLYGON EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyBIntersection() { + Geometry a = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry b = read("POLYGON EMPTY"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyABIntersection() { + Geometry a = read("POLYGON EMPTY"); + Geometry b = read("POLYGON EMPTY"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyADifference() { + Geometry a = read("POLYGON EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = difference(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyAUnion() { + Geometry a = read("POLYGON EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry actual = union(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyASymDifference() { + Geometry a = read("POLYGON EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry actual = symDifference(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyLinePolygonIntersection() { + Geometry a = read("LINESTRING EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("LINESTRING EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyLinePolygonDifference() { + Geometry a = read("LINESTRING EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("LINESTRING EMPTY"); + Geometry actual = difference(a, b, 1); + checkEqual(expected, actual); + } + + public void testEmptyPointPolygonIntersection() { + Geometry a = read("POINT EMPTY"); + Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); + Geometry expected = read("POINT EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testDisjointIntersection() { + Geometry a = read("POLYGON ((60 90, 90 90, 90 60, 60 60, 60 90))"); + Geometry b = read("POLYGON ((200 300, 300 300, 300 200, 200 200, 200 300))"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = intersection(a, b, 1); + checkEqual(expected, actual); + } + + public void testDisjointIntersectionNoOpt() { + Geometry a = read("POLYGON ((60 90, 90 90, 90 60, 60 60, 60 90))"); + Geometry b = read("POLYGON ((200 300, 300 300, 300 200, 200 200, 200 300))"); + Geometry expected = read("POLYGON EMPTY"); + Geometry actual = intersectionNoOpt(a, b, 1); + checkEqual(expected, actual); + } + + public static Geometry intersectionNoOpt(Geometry a, Geometry b, double scaleFactor) { + PrecisionModel pm = new PrecisionModel(scaleFactor); + OverlayNG ov = new OverlayNG(a, b, pm, INTERSECTION); + ov.setOptimized(false); + return ov.getResult(); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java index 6243434783..cfb5395e8b 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java @@ -30,102 +30,6 @@ public static void main(String args[]) { public OverlayNGTest(String name) { super(name); } - public void testEmptyGCBothIntersection() { - Geometry a = read("GEOMETRYCOLLECTION EMPTY"); - Geometry b = read("GEOMETRYCOLLECTION EMPTY"); - Geometry expected = read("GEOMETRYCOLLECTION EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyAPolygonIntersection() { - Geometry a = read("POLYGON EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyBIntersection() { - Geometry a = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry b = read("POLYGON EMPTY"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyABIntersection() { - Geometry a = read("POLYGON EMPTY"); - Geometry b = read("POLYGON EMPTY"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyADifference() { - Geometry a = read("POLYGON EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = difference(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyAUnion() { - Geometry a = read("POLYGON EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry actual = union(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyASymDifference() { - Geometry a = read("POLYGON EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry actual = symDifference(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyLinePolygonIntersection() { - Geometry a = read("LINESTRING EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("LINESTRING EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyLinePolygonDifference() { - Geometry a = read("LINESTRING EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("LINESTRING EMPTY"); - Geometry actual = difference(a, b, 1); - checkEqual(expected, actual); - } - - public void testEmptyPointPolygonIntersection() { - Geometry a = read("POINT EMPTY"); - Geometry b = read("POLYGON ((1 0, 2 5, 3 0, 1 0))"); - Geometry expected = read("POINT EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testDisjointIntersection() { - Geometry a = read("POLYGON ((60 90, 90 90, 90 60, 60 60, 60 90))"); - Geometry b = read("POLYGON ((200 300, 300 300, 300 200, 200 200, 200 300))"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = intersection(a, b, 1); - checkEqual(expected, actual); - } - - public void testDisjointIntersectionNoOpt() { - Geometry a = read("POLYGON ((60 90, 90 90, 90 60, 60 60, 60 90))"); - Geometry b = read("POLYGON ((200 300, 300 300, 300 200, 200 200, 200 300))"); - Geometry expected = read("POLYGON EMPTY"); - Geometry actual = intersectionNoOpt(a, b, 1); - checkEqual(expected, actual); - } - public void testAreaLineIntersection() { Geometry a = read("POLYGON ((360 200, 220 200, 220 180, 300 180, 300 160, 300 140, 360 200))"); Geometry b = read("MULTIPOLYGON (((280 180, 280 160, 300 160, 300 180, 280 180)), ((220 230, 240 230, 240 180, 220 180, 220 230)))"); @@ -636,12 +540,7 @@ public static Geometry union(Geometry a, Geometry b) { return OverlayNG.overlay(a, b, UNION, pm); } - public static Geometry intersectionNoOpt(Geometry a, Geometry b, double scaleFactor) { - PrecisionModel pm = new PrecisionModel(scaleFactor); - OverlayNG ov = new OverlayNG(a, b, pm, INTERSECTION); - ov.setOptimized(false); - return ov.getResult(); - } + } From 80d2340483d1a97c59f66602fa13f7c1b8c1ab5b Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 8 May 2023 14:41:15 -0700 Subject: [PATCH 49/79] Fix visibility of EdgeNodingBuilder instance var --- .../locationtech/jts/operation/overlayng/EdgeNodingBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java index 73d59e264c..3dcfce4e94 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java @@ -87,7 +87,7 @@ private static Noder createFloatingPrecisionNoder(boolean doValidation) { } private PrecisionModel pm; - List inputEdges = new ArrayList(); + private List inputEdges = new ArrayList(); private Noder customNoder; private Envelope clipEnv = null; From 2867127264c30cd15681736cb35378ba41ea2b2f Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 8 May 2023 14:41:31 -0700 Subject: [PATCH 50/79] Refactor OverlayNGTest --- .../operation/overlayng/OverlayNGTest.java | 56 +------------------ 1 file changed, 2 insertions(+), 54 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java index cfb5395e8b..adc1686fbf 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGTest.java @@ -11,18 +11,11 @@ */ package org.locationtech.jts.operation.overlayng; -import static org.locationtech.jts.operation.overlayng.OverlayNG.DIFFERENCE; -import static org.locationtech.jts.operation.overlayng.OverlayNG.INTERSECTION; -import static org.locationtech.jts.operation.overlayng.OverlayNG.SYMDIFFERENCE; -import static org.locationtech.jts.operation.overlayng.OverlayNG.UNION; - import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.PrecisionModel; import junit.textui.TestRunner; -import test.jts.GeometryTestCase; -public class OverlayNGTest extends GeometryTestCase { +public class OverlayNGTest extends OverlayNGTestCase { public static void main(String args[]) { TestRunner.run(OverlayNGTest.class); @@ -496,51 +489,6 @@ public void testPolygonLineHorizontalIntersection() { Geometry actual = intersection(a, b); checkEqual(expected, actual); } - - //============================================================ - - - public static Geometry difference(Geometry a, Geometry b, double scaleFactor) { - PrecisionModel pm = new PrecisionModel(scaleFactor); - return OverlayNG.overlay(a, b, DIFFERENCE, pm); - } - - public static Geometry symDifference(Geometry a, Geometry b, double scaleFactor) { - PrecisionModel pm = new PrecisionModel(scaleFactor); - return OverlayNG.overlay(a, b, SYMDIFFERENCE, pm); - } - - public static Geometry intersection(Geometry a, Geometry b, double scaleFactor) { - PrecisionModel pm = new PrecisionModel(scaleFactor); - return OverlayNG.overlay(a, b, INTERSECTION, pm); - } - - public static Geometry union(Geometry a, Geometry b, double scaleFactor) { - PrecisionModel pm = new PrecisionModel(scaleFactor); - return OverlayNG.overlay(a, b, UNION, pm); - } - - public static Geometry difference(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, DIFFERENCE, pm); - } - - public static Geometry symDifference(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, SYMDIFFERENCE, pm); - } - - public static Geometry intersection(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, INTERSECTION, pm); - } - - public static Geometry union(Geometry a, Geometry b) { - PrecisionModel pm = new PrecisionModel(); - return OverlayNG.overlay(a, b, UNION, pm); - } - - - + } From 30e3be446aebbd3a6aa6e545d429c4a5d1bf9a97 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 8 May 2023 16:57:36 -0700 Subject: [PATCH 51/79] Formatting --- .../jts/operation/overlayng/EdgeNodingBuilder.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java index 3dcfce4e94..86514aa4f8 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java @@ -187,11 +187,7 @@ private List node(List segStrings) { @SuppressWarnings("unchecked") Collection nodedSS = noder.getNodedSubstrings(); - - //scanForEdges(nodedSS); - List edges = createEdges(nodedSS); - return edges; } @@ -201,7 +197,8 @@ private List createEdges(Collection segStrings) { Coordinate[] pts = ss.getCoordinates(); // don't create edges from collapsed lines - if ( Edge.isCollapsed(pts) ) continue; + if ( Edge.isCollapsed(pts) ) + continue; EdgeSourceInfo info = (EdgeSourceInfo) ss.getData(); /** From b5bc9c58ba2e46b7b0575721321e9b030f446d2b Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 8 May 2023 17:00:13 -0700 Subject: [PATCH 52/79] Formatting --- .../jts/operation/overlayng/EdgeNodingBuilder.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java index 86514aa4f8..252cb67e45 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/EdgeNodingBuilder.java @@ -176,7 +176,7 @@ public List build(Geometry geom0, Geometry geom1) { * Nodes a set of segment strings and creates {@link Edge}s from the result. * The input segment strings each carry a {@link EdgeSourceInfo} object, * which is used to provide source topology info to the constructed Edges - * (and is then discarded). + * (and then is discarded). * * @param segStrings * @return @@ -196,16 +196,13 @@ private List createEdges(Collection segStrings) { for (SegmentString ss : segStrings) { Coordinate[] pts = ss.getCoordinates(); - // don't create edges from collapsed lines + //-- don't create edges from collapsed lines if ( Edge.isCollapsed(pts) ) continue; EdgeSourceInfo info = (EdgeSourceInfo) ss.getData(); - /** - * Record that a non-collapsed edge exists for the parent geometry - */ + //-- Record that a non-collapsed edge exists for the parent geometry hasEdges[ info.getIndex() ] = true; - edges.add(new Edge(ss.getCoordinates(), info)); } return edges; From 123203e54ebe6c5a291710e0d95e1966eaa4f6e5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 9 May 2023 13:00:22 -0700 Subject: [PATCH 53/79] Improve TestBuilder Noding functions --- .../jtstest/function/NodingFunctions.java | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/NodingFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/NodingFunctions.java index 0c8156e006..96337ad3c3 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/NodingFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/NodingFunctions.java @@ -26,6 +26,7 @@ import org.locationtech.jts.noding.FastNodingValidator; import org.locationtech.jts.noding.IntersectionAdder; import org.locationtech.jts.noding.MCIndexNoder; +import org.locationtech.jts.noding.NodedSegmentString; import org.locationtech.jts.noding.Noder; import org.locationtech.jts.noding.NodingIntersectionFinder; import org.locationtech.jts.noding.SegmentStringUtil; @@ -107,51 +108,66 @@ private static void processNodes(Geometry geom, NodingIntersectionFinder intFind noder.computeNodes( SegmentStringUtil.extractBasicSegmentStrings(geom) ); } - public static Geometry MCIndexNodingWithPrecision(Geometry geom, double scaleFactor) + public static Geometry MCIndexNodingWithPrecision(Geometry geom, + @Metadata(isRequired=false) + Geometry geom2, + @Metadata(title="Precision Scale") + double scaleFactor) { + List segs = extractNodedSegmentStrings(geom, geom2); PrecisionModel fixedPM = new PrecisionModel(scaleFactor); LineIntersector li = new RobustLineIntersector(); li.setPrecisionModel(fixedPM); Noder noder = new MCIndexNoder(new IntersectionAdder(li)); - noder.computeNodes( SegmentStringUtil.extractNodedSegmentStrings(geom) ); + noder.computeNodes( segs ); return SegmentStringUtil.toGeometry( noder.getNodedSubstrings(), FunctionsUtil.getFactoryOrDefault(geom) ); } - public static Geometry MCIndexNoding(Geometry geom) + public static Geometry MCIndexNoding(Geometry geom, + @Metadata(isRequired=false) + Geometry geom2) { + List segs = extractNodedSegmentStrings(geom, geom2); Noder noder = new MCIndexNoder(new IntersectionAdder(new RobustLineIntersector())); - noder.computeNodes( SegmentStringUtil.extractNodedSegmentStrings(geom) ); + noder.computeNodes( segs ); return SegmentStringUtil.toGeometry(noder.getNodedSubstrings(), FunctionsUtil.getFactoryOrDefault(geom)); } @Metadata(description="Nodes input using the SnappingNoder") - public static Geometry snappingNoder(Geometry geom, Geometry geom2, + public static Geometry snappingNoder(Geometry geom, + @Metadata(isRequired=false) + Geometry geom2, @Metadata(title="Snap distance") double snapDistance) { - List segs = SegmentStringUtil.extractNodedSegmentStrings(geom); - if (geom2 != null) { - List segs2 = SegmentStringUtil.extractNodedSegmentStrings(geom2); - segs.addAll(segs2); - } + List segs = extractNodedSegmentStrings(geom, geom2); Noder noder = new SnappingNoder(snapDistance); noder.computeNodes(segs); Collection nodedSegStrings = noder.getNodedSubstrings(); return SegmentStringUtil.toGeometry(nodedSegStrings, FunctionsUtil.getFactoryOrDefault(geom)); } - @Metadata(description="Nodes input using the SnapRoundingNoder") - public static Geometry snapRoundingNoder(Geometry geom, Geometry geom2, - @Metadata(title="Scale factor") - double scaleFactor) - { - List segs = SegmentStringUtil.extractNodedSegmentStrings(geom); + private static List extractNodedSegmentStrings(Geometry geom1, Geometry geom2) { + @SuppressWarnings("unchecked") + List segs = SegmentStringUtil.extractNodedSegmentStrings(geom1); if (geom2 != null) { - List segs2 = SegmentStringUtil.extractNodedSegmentStrings(geom2); + @SuppressWarnings("unchecked") + List segs2 = SegmentStringUtil.extractNodedSegmentStrings(geom2); segs.addAll(segs2); } + return segs; + } + + @Metadata(description="Nodes input using the SnapRoundingNoder") + public static Geometry snapRoundingNoder(Geometry geom, + @Metadata(isRequired=false) + Geometry geom2, + @Metadata(title="Precision Scale") + double scaleFactor) + { + List segs = extractNodedSegmentStrings(geom, geom2); PrecisionModel pm = new PrecisionModel(scaleFactor); Noder noder = new SnapRoundingNoder(pm); noder.computeNodes(segs); From 1c4d330309c055bb3edeb592168865af88e8745d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 19 May 2023 16:21:46 -0700 Subject: [PATCH 54/79] Add TestBuilder function fixIfInvalid --- .../locationtech/jtstest/function/ValidationFunctions.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/ValidationFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/ValidationFunctions.java index d7fcad1926..92953a1616 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/ValidationFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/ValidationFunctions.java @@ -73,6 +73,12 @@ public static Geometry fixInvalid(Geometry geom) { return GeometryFixer.fix(geom); } + public static Geometry fixIfInvalid(Geometry geom) { + if (geom.isValid()) + return geom.copy(); + return GeometryFixer.fix(geom); + } + public static Geometry fixInvalidKeepCollapse(Geometry geom) { GeometryFixer fixer = new GeometryFixer(geom); fixer.setKeepCollapsed(true); From abbd9e13e2cfad48cf4f36e534fff8df1b95b294 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 22 May 2023 15:34:25 -0700 Subject: [PATCH 55/79] Refactor TestBuilder toolbar --- .../testbuilder/JTSTestBuilderToolBar.java | 254 +++++------------- .../controller/JTSTestBuilderController.java | 13 +- 2 files changed, 71 insertions(+), 196 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderToolBar.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderToolBar.java index 0bff376fb2..3a644a842b 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderToolBar.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/JTSTestBuilderToolBar.java @@ -34,20 +34,6 @@ public class JTSTestBuilderToolBar { JToolBar toolbar = new JToolBar(); ButtonGroup toolButtonGroup = new ButtonGroup(); - JButton previousButton = new JButton(); - JButton nextButton = new JButton(); - JButton newButton = new JButton(); - JButton copyButton = new JButton(); - JButton deleteButton = new JButton(); - JButton exchangeButton = new JButton(); - - JButton oneToOneButton = new JButton(); - JButton zoomToFullExtentButton = new JButton(); - JButton zoomToInputButton = new JButton(); - JButton zoomToInputAButton = new JButton(); - JButton zoomToInputBButton = new JButton(); - JButton zoomToResultButton = new JButton(); - JToggleButton drawRectangleButton; JToggleButton drawPolygonButton; JToggleButton drawLineStringButton; @@ -115,192 +101,85 @@ public JToolBar getToolBar() * Buttons * -------------------------------------------------- */ - previousButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - previousButton.setMaximumSize(new Dimension(30, 30)); - previousButton.setMinimumSize(new Dimension(30, 30)); - previousButton.setPreferredSize(new Dimension(30, 30)); - previousButton.setToolTipText(AppStrings.TIP_PREV); - previousButton.setHorizontalTextPosition(SwingConstants.CENTER); - previousButton.setIcon(leftIcon); - previousButton.setMargin(new Insets(0, 0, 0, 0)); - previousButton.setVerticalTextPosition(SwingConstants.BOTTOM); - previousButton.addActionListener( + JButton previousButton = createButton( + AppStrings.TIP_PREV, leftIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + boolean isZoom = 0 == (e.getModifiers() & ActionEvent.CTRL_MASK); + controller().caseMoveTo(-1, isZoom); + } + }); + JButton nextButton = createButton( + AppStrings.TIP_NEXT, rightIcon, new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { boolean isZoom = 0 == (e.getModifiers() & ActionEvent.CTRL_MASK); - controller().caseMoveToPrev(isZoom); + controller().caseMoveTo(1, isZoom); } }); - - nextButton.setMargin(new Insets(0, 0, 0, 0)); - nextButton.setVerticalTextPosition(SwingConstants.BOTTOM); - nextButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - nextButton.setMaximumSize(new Dimension(30, 30)); - nextButton.setMinimumSize(new Dimension(30, 30)); - nextButton.setPreferredSize(new Dimension(30, 30)); - nextButton.setToolTipText(AppStrings.TIP_NEXT); - nextButton.setHorizontalTextPosition(SwingConstants.CENTER); - nextButton.setIcon(rightIcon); - nextButton.addActionListener( + JButton newButton = createButton( + AppStrings.TIP_CASE_ADD_NEW, plusIcon, new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - boolean isZoom = 0 == (e.getModifiers() & ActionEvent.CTRL_MASK); - controller().caseMoveToNext(isZoom); + controller().caseCreateNew(); } }); - - newButton.setMargin(new Insets(0, 0, 0, 0)); - newButton.setVerticalTextPosition(SwingConstants.BOTTOM); - newButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - newButton.setMaximumSize(new Dimension(30, 30)); - newButton.setMinimumSize(new Dimension(30, 30)); - newButton.setPreferredSize(new Dimension(30, 30)); - newButton.setToolTipText(AppStrings.TIP_CASE_ADD_NEW); - newButton.setHorizontalTextPosition(SwingConstants.CENTER); - newButton.setIcon(plusIcon); - newButton.addActionListener( + JButton copyButton = createButton( + AppStrings.TIP_CASE_DUP, copyCaseIcon, new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - controller().caseCreateNew(); + controller().caseCopy(); } }); - - copyButton.setMargin(new Insets(0, 0, 0, 0)); - copyButton.setVerticalTextPosition(SwingConstants.BOTTOM); - copyButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - copyButton.setMaximumSize(new Dimension(30, 30)); - copyButton.setMinimumSize(new Dimension(30, 30)); - copyButton.setPreferredSize(new Dimension(30, 30)); - copyButton.setToolTipText(AppStrings.TIP_CASE_DUP); - copyButton.setHorizontalTextPosition(SwingConstants.CENTER); - copyButton.setIcon(copyCaseIcon); - copyButton.addActionListener( + JButton deleteButton = createButton( + AppStrings.TIP_CASE_DELETE, deleteIcon, new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - controller().caseCopy(); + controller().caseDelete(); } }); - - deleteButton.setMargin(new Insets(0, 0, 0, 0)); - deleteButton.setVerticalTextPosition(SwingConstants.BOTTOM); - deleteButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - deleteButton.setMaximumSize(new Dimension(30, 30)); - deleteButton.setMinimumSize(new Dimension(30, 30)); - deleteButton.setPreferredSize(new Dimension(30, 30)); - deleteButton.setToolTipText(AppStrings.TIP_CASE_DELETE); - deleteButton.setHorizontalTextPosition(SwingConstants.CENTER); - deleteButton.setIcon(deleteIcon); - deleteButton.addActionListener( + JButton oneToOneButton = createButton( + AppStrings.TIP_ZOOM_1_1, zoomOneToOneIcon, new java.awt.event.ActionListener() { public void actionPerformed(ActionEvent e) { - controller().caseDelete(); + controller().zoomOneToOne(); + } + }); + JButton zoomToInputButton = createButton( + "Zoom To Input", zoomToInputIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + controller().zoomToInput(); + } + }); + JButton zoomToInputAButton = createButton( + AppStrings.TIP_ZOOM_TO_A, zoomToInputAIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + controller().zoomToInputA(); + } + }); + JButton zoomToInputBButton = createButton( + AppStrings.TIP_ZOOM_TO_B, zoomToInputBIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + controller().zoomToInputB(); + } + }); + JButton zoomToResultButton = createButton( + AppStrings.TIP_ZOOM_TO_RESULT, zoomToResultIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + controller().zoomToResult(); + } + }); + JButton zoomToFullExtentButton = createButton( + AppStrings.TIP_ZOOM_TO_FULL_EXTENT, zoomToFullExtentIcon, + new java.awt.event.ActionListener() { + public void actionPerformed(ActionEvent e) { + controller().zoomToFullExtent(); } }); - - - oneToOneButton.setMargin(new Insets(0, 0, 0, 0)); - oneToOneButton.setIcon(zoomOneToOneIcon); - oneToOneButton.setPreferredSize(new Dimension(30, 30)); - oneToOneButton.setMinimumSize(new Dimension(30, 30)); - oneToOneButton.setVerticalTextPosition(SwingConstants.BOTTOM); - oneToOneButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomOneToOne(); - } - }); - oneToOneButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - oneToOneButton.setToolTipText(AppStrings.TIP_ZOOM_1_1); - oneToOneButton.setHorizontalTextPosition(SwingConstants.CENTER); - oneToOneButton.setMaximumSize(new Dimension(30, 30)); - - zoomToInputButton.setMargin(new Insets(0, 0, 0, 0)); - zoomToInputButton.setIcon(zoomToInputIcon); - zoomToInputButton.setPreferredSize(new Dimension(30, 30)); - zoomToInputButton.setMaximumSize(new Dimension(30, 30)); - zoomToInputButton.setVerticalTextPosition(SwingConstants.BOTTOM); - zoomToInputButton.setMinimumSize(new Dimension(30, 30)); - zoomToInputButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - zoomToInputButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomToInputButton.setToolTipText("Zoom To Input"); - zoomToInputButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomToInput(); - } - }); - - zoomToInputAButton.setMargin(new Insets(0, 0, 0, 0)); - zoomToInputAButton.setIcon(zoomToInputAIcon); - zoomToInputAButton.setPreferredSize(new Dimension(30, 30)); - zoomToInputAButton.setMaximumSize(new Dimension(30, 30)); - zoomToInputAButton.setVerticalTextPosition(SwingConstants.BOTTOM); - zoomToInputAButton.setMinimumSize(new Dimension(30, 30)); - zoomToInputAButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - zoomToInputAButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomToInputAButton.setToolTipText(AppStrings.TIP_ZOOM_TO_A); - zoomToInputAButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomToInputA(); - } - }); - - zoomToInputBButton.setMargin(new Insets(0, 0, 0, 0)); - zoomToInputBButton.setIcon(zoomToInputBIcon); - zoomToInputBButton.setPreferredSize(new Dimension(30, 30)); - zoomToInputBButton.setMaximumSize(new Dimension(30, 30)); - zoomToInputBButton.setVerticalTextPosition(SwingConstants.BOTTOM); - zoomToInputBButton.setMinimumSize(new Dimension(30, 30)); - zoomToInputBButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - zoomToInputBButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomToInputBButton.setToolTipText(AppStrings.TIP_ZOOM_TO_B); - zoomToInputBButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomToInputB(); - } - }); - zoomToInputButton.setMaximumSize(new Dimension(30, 30)); - - zoomToResultButton.setMargin(new Insets(0, 0, 0, 0)); - zoomToResultButton.setIcon(zoomToResultIcon); - zoomToResultButton.setPreferredSize(new Dimension(30, 30)); - zoomToResultButton.setMaximumSize(new Dimension(30, 30)); - zoomToResultButton.setVerticalTextPosition(SwingConstants.BOTTOM); - zoomToResultButton.setMinimumSize(new Dimension(30, 30)); - zoomToResultButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - zoomToResultButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomToResultButton.setToolTipText(AppStrings.TIP_ZOOM_TO_RESULT); - zoomToResultButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomToResult(); - } - }); - zoomToResultButton.setMaximumSize(new Dimension(30, 30)); - - zoomToFullExtentButton.setMargin(new Insets(0, 0, 0, 0)); - zoomToFullExtentButton.setIcon(zoomToFullExtentIcon); - zoomToFullExtentButton.setPreferredSize(new Dimension(30, 30)); - zoomToFullExtentButton.setVerticalTextPosition(SwingConstants.BOTTOM); - zoomToFullExtentButton.setMinimumSize(new Dimension(30, 30)); - zoomToFullExtentButton.setFont(new java.awt.Font("SansSerif", 0, 10)); - zoomToFullExtentButton.setHorizontalTextPosition(SwingConstants.CENTER); - zoomToFullExtentButton.setToolTipText(AppStrings.TIP_ZOOM_TO_FULL_EXTENT); - zoomToFullExtentButton.addActionListener( - new java.awt.event.ActionListener() { - - public void actionPerformed(ActionEvent e) { - controller().zoomToFullExtent(); - } - }); - zoomToFullExtentButton.setMaximumSize(new Dimension(30, 30)); drawRectangleButton = createToggleButton( AppStrings.TIP_DRAW_RECTANGLE, drawRectangleIcon, @@ -386,7 +265,8 @@ public void actionPerformed(ActionEvent e) { controller().modeDeleteVertex(); }}); - group(drawRectangleButton + group(toolButtonGroup, + drawRectangleButton ,drawPolygonButton ,drawLineStringButton ,drawPointButton @@ -399,20 +279,16 @@ public void actionPerformed(ActionEvent e) { ,extractComponentButton ); - - add( + add(toolbar, newButton, copyButton, previousButton, nextButton, strut(8), deleteButton, strut(8), - exchangeButton, - strut(8), oneToOneButton, zoomToInputAButton, zoomToInputBButton, zoomToInputButton, zoomToResultButton, zoomToFullExtentButton, strut(20), zoomButton, - //jToolBar1.add(panButton // remove in favour of using Zoom tool right-drag infoButton, extractComponentButton, @@ -429,18 +305,18 @@ public void actionPerformed(ActionEvent e) { return toolbar; } - private Component strut(int width) { + private static Component strut(int width) { return Box.createHorizontalStrut(width); } - private void add(Component ... comps) { + private static void add(JToolBar toolbar, Component ... comps) { for (Component comp : comps) { toolbar.add(comp); } } - private void group(AbstractButton ... btns) { + private static void group(ButtonGroup group, AbstractButton ... btns) { for (AbstractButton btn : btns) { - toolButtonGroup.add(btn); + group.add(btn); } } public void setFocusGeometry(int index) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java index 222a5fde2d..60a8c8566c 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/controller/JTSTestBuilderController.java @@ -314,14 +314,13 @@ public void zoomToInputB() { editPanel().zoomToGeometry(1); } - public void caseMoveToPrev(boolean isZoom) { - model().cases().prevCase(); - frame().updateTestCaseView(); - if (isZoom) zoomToInput(); - } - - public void caseMoveToNext(boolean isZoom) { + public void caseMoveTo(int dir, boolean isZoom) { + if (dir < 1) { + model().cases().prevCase(); + } + else { model().cases().nextCase(); + } frame().updateTestCaseView(); if (isZoom) zoomToInput(); } From 15ef53578a99b3f7a165e44ecedbd8b74b5fcb84 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 23 May 2023 17:55:59 -0700 Subject: [PATCH 56/79] Fix imports --- .../main/java/org/locationtech/jts/coverage/CoverageEdge.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java index 0626ea5705..2208200b29 100644 --- a/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java +++ b/modules/core/src/main/java/org/locationtech/jts/coverage/CoverageEdge.java @@ -17,7 +17,6 @@ import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineSegment; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.io.WKTWriter; From db6d5350550b341d2bfbf1b55e3708e0e1a35f65 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 24 May 2023 11:13:28 -0700 Subject: [PATCH 57/79] Testbuilder - switch direction of wheel zooming --- .../jtstest/testbuilder/ui/tools/ZoomTool.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ui/tools/ZoomTool.java b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ui/tools/ZoomTool.java index c3c56808f1..e1c8748ae1 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ui/tools/ZoomTool.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/testbuilder/ui/tools/ZoomTool.java @@ -112,9 +112,12 @@ private void drawBand(Graphics g) { } public void mouseWheelMoved(MouseWheelEvent e) { + /** + * Rolling wheel forward zooms in, backward zooms out + */ double notches = e.getPreciseWheelRotation(); - double zoomFactor = Math.abs(notches) * 2; - if (notches > 0 && zoomFactor > 0) zoomFactor = 1.0 / zoomFactor; + double zoomFactor = Math.abs(notches) * 4; + if (notches < 0 && zoomFactor > 0) zoomFactor = 1.0 / zoomFactor; panel().zoom(toModel(e.getPoint()), zoomFactor); } From 68c892b7c0886288a1ec12d9603e8376193fb722 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 25 May 2023 17:13:23 -0700 Subject: [PATCH 58/79] Enforce OffsetCurve to use a minimum QuadrantSegs value (#981) --- .../jts/operation/buffer/OffsetCurve.java | 16 +++++++++++++- .../jts/operation/buffer/OffsetCurveTest.java | 22 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java index 4564fdc5c9..79c598af06 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/buffer/OffsetCurve.java @@ -76,6 +76,12 @@ public class OffsetCurve { * The nearness tolerance for matching the the raw offset linework and the buffer curve. */ private static final int MATCH_DISTANCE_FACTOR = 10000; + + /** + * A QuadSegs minimum value that will prevent generating + * unwanted offset curve artifacts near end caps. + */ + private static final int MIN_QUADRANT_SEGMENTS = 8; /** * Computes the offset curve of a geometry at a given distance. @@ -164,7 +170,15 @@ public OffsetCurve(Geometry geom, double distance, BufferParameters bufParams) { //-- make new buffer params since the end cap style must be the default this.bufferParams = new BufferParameters(); if (bufParams != null) { - bufferParams.setQuadrantSegments(bufParams.getQuadrantSegments()); + /** + * Prevent using a very small QuadSegs value, to avoid + * offset curve artifacts near the end caps. + */ + int quadSegs = bufParams.getQuadrantSegments(); + if (quadSegs < MIN_QUADRANT_SEGMENTS) { + quadSegs = MIN_QUADRANT_SEGMENTS; + } + bufferParams.setQuadrantSegments(quadSegs); bufferParams.setJoinStyle(bufParams.getJoinStyle()); bufferParams.setMitreLimit(bufParams.getMitreLimit()); } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java index f3ad6ae94f..f4cc9bb177 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/buffer/OffsetCurveTest.java @@ -298,8 +298,8 @@ public void testInfiniteLoop() { public void testQuadSegs() { checkOffsetCurve( "LINESTRING (20 20, 50 50, 80 20)", - 10, 2, -1, -1, - "LINESTRING (12.93 27.07, 42.93 57.07, 50 60, 57.07 57.07, 87.07 27.07)" + 10, 10, -1, -1, + "LINESTRING (12.93 27.07, 42.93 57.07, 44.12 58.09, 45.46 58.91, 46.91 59.51, 48.44 59.88, 50 60, 51.56 59.88, 53.09 59.51, 54.54 58.91, 55.88 58.09, 57.07 57.07, 87.07 27.07)" ); } @@ -319,6 +319,24 @@ public void testJoinMitre() { ); } + // See https://github.com/qgis/QGIS/issues/53165 + public void testMinQuadrantSegments() { + checkOffsetCurve( + "LINESTRING (553772.0645892698 177770.05079236583, 553780.9235869241 177768.99614978794, 553781.8325485934 177768.41771963477)", + -11, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (553770.76 177759.13, 553777.54 177758.32)" + ); + } + + // See https://github.com/qgis/QGIS/issues/53165#issuecomment-1563214857 + public void testMinQuadrantSegments_QGIS() { + checkOffsetCurve( + "LINESTRING (421 622, 446 625, 449 627)", + 133, 0, BufferParameters.JOIN_MITRE, -1, + "LINESTRING (405.15 754.05, 416.3 755.39)" + ); + } + //======================================= private static final double EQUALS_TOL = 0.05; From 5d3de32bd361e19a9d5660958c1ca48ce8d06970 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Thu, 25 May 2023 17:14:38 -0700 Subject: [PATCH 59/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 5d83b7ba8a..067cda679c 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -52,6 +52,7 @@ Distributions for older JTS versions can be obtained at the * Fix `OffsetCurve` handling of input with repeated points (#956) * Fix `OffsetCurve` handling zero offset distance (#971) * Fix `MaximumInscribedCircle` and `LargestEmptyCircle` to avoid long looping for thin inputs (#978) +* Fix OffsetCurve to use a minimum QuadrantSegs value (#981) ### Performance Improvements From 2d2a1a966491c3dd1381bd4fcf2923b5872ef53c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 19 Jun 2023 11:42:56 -0700 Subject: [PATCH 60/79] Fix HilbertEncoder Y extent handling --- .../locationtech/jts/index/hprtree/HilbertEncoder.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HilbertEncoder.java b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HilbertEncoder.java index 81ed8dcea7..33729c9399 100644 --- a/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HilbertEncoder.java +++ b/modules/core/src/main/java/org/locationtech/jts/index/hprtree/HilbertEncoder.java @@ -26,12 +26,10 @@ public HilbertEncoder(int level, Envelope extent) { int hside = (int) Math.pow(2, level) - 1; minx = extent.getMinX(); - double extentX = extent.getWidth(); - strideX = extentX / hside; + strideX = extent.getWidth() / hside; - miny = extent.getMinX(); - double extentY = extent.getHeight(); - strideY = extentY / hside; + miny = extent.getMinY(); + strideY = extent.getHeight() / hside; } public int encode(Envelope env) { From 670fd1625410a9be3e022e6423db110ecd585005 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 19 Jun 2023 11:44:04 -0700 Subject: [PATCH 61/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 067cda679c..20564a634f 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -52,7 +52,8 @@ Distributions for older JTS versions can be obtained at the * Fix `OffsetCurve` handling of input with repeated points (#956) * Fix `OffsetCurve` handling zero offset distance (#971) * Fix `MaximumInscribedCircle` and `LargestEmptyCircle` to avoid long looping for thin inputs (#978) -* Fix OffsetCurve to use a minimum QuadrantSegs value (#981) +* Fix `OffsetCurve` to use a minimum QuadrantSegs value (#981) +* Fix `HilbertEncoder` Y extent handling ### Performance Improvements From b7fcb00ecf09741a3a135228e0c38283f8da28f4 Mon Sep 17 00:00:00 2001 From: Valerij Dobler <2649244+Illutax@users.noreply.github.com> Date: Mon, 19 Jun 2023 21:10:05 +0200 Subject: [PATCH 62/79] Fixes Checktype violations - removes wildcard imports (#982) --- .../locationtech/jts/io/oracle/OraWriter.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/modules/io/ora/src/main/java/org/locationtech/jts/io/oracle/OraWriter.java b/modules/io/ora/src/main/java/org/locationtech/jts/io/oracle/OraWriter.java index 0876a2d5a1..542761e24a 100644 --- a/modules/io/ora/src/main/java/org/locationtech/jts/io/oracle/OraWriter.java +++ b/modules/io/ora/src/main/java/org/locationtech/jts/io/oracle/OraWriter.java @@ -19,16 +19,32 @@ */ package org.locationtech.jts.io.oracle; +import java.util.List; +import java.util.ArrayList; import java.sql.SQLException; -import java.util.*; import org.locationtech.jts.algorithm.CGAlgorithms; -import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.util.Assert; +import oracle.jdbc.OracleConnection; + +import oracle.sql.ARRAY; +import oracle.sql.Datum; +import oracle.sql.NUMBER; +import oracle.sql.STRUCT; import oracle.jdbc.OracleConnection; -import oracle.sql.*; /** * Translates a JTS Geometry into an Oracle STRUCT representing an MDSYS.SDO_GEOMETRY object. From 94f791e6d755683e80a427ac39598f0424d2e010 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 20 Jun 2023 22:18:00 -0700 Subject: [PATCH 63/79] Improve Convex Hull performance by avoiding duplicate uniquing (#985) Signed-off-by: Martin Davis --- .../jts/algorithm/ConvexHull.java | 135 +++++++++++++----- .../perf/algorithm/ConvexHullPerfTest.java | 48 +++++++ 2 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 modules/core/src/test/java/test/jts/perf/algorithm/ConvexHullPerfTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java index 665d0b4a08..438d69f718 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java @@ -14,10 +14,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; -import java.util.TreeSet; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateArrays; @@ -30,7 +30,6 @@ import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.util.Assert; -import org.locationtech.jts.util.UniqueCoordinateArrayFilter; /** * Computes the convex hull of a {@link Geometry}. @@ -38,11 +37,16 @@ * points in the input Geometry. *

        * Uses the Graham Scan algorithm. + *

        + * Incorporates heuristics to optimize checking for degenerate results, + * and to reduce the number of points processed for large inputs. * *@version 1.7 */ public class ConvexHull { + private static final int TUNING_REDUCE_SIZE = 50; + private GeometryFactory geomFactory; private Coordinate[] inputPts; @@ -51,25 +55,20 @@ public class ConvexHull */ public ConvexHull(Geometry geometry) { - this(extractCoordinates(geometry), geometry.getFactory()); + this(geometry.getCoordinates(), geometry.getFactory()); } /** * Create a new convex hull construction for the input {@link Coordinate} array. */ public ConvexHull(Coordinate[] pts, GeometryFactory geomFactory) - { - inputPts = UniqueCoordinateArrayFilter.filterCoordinates(pts); - //inputPts = pts; + { + //-- suboptimal early uniquing - for performance testing only + //inputPts = UniqueCoordinateArrayFilter.filterCoordinates(pts); + + inputPts = pts; this.geomFactory = geomFactory; } - private static Coordinate[] extractCoordinates(Geometry geom) - { - UniqueCoordinateArrayFilter filter = new UniqueCoordinateArrayFilter(); - geom.apply(filter); - return filter.getCoordinates(); - } - /** * Returns a {@link Geometry} that represents the convex hull of the input * geometry. @@ -84,21 +83,19 @@ private static Coordinate[] extractCoordinates(Geometry geom) */ public Geometry getConvexHull() { - if (inputPts.length == 0) { - return geomFactory.createGeometryCollection(); - } - if (inputPts.length == 1) { - return geomFactory.createPoint(inputPts[0]); - } - if (inputPts.length == 2) { - return geomFactory.createLineString(inputPts); - } - + Geometry fewPointsGeom = createFewPointsResult(); + if (fewPointsGeom != null) + return fewPointsGeom; + Coordinate[] reducedPts = inputPts; - // use heuristic to reduce points, if large - if (inputPts.length > 50) { + //-- use heuristic to reduce points, if large + if (inputPts.length > TUNING_REDUCE_SIZE) { reducedPts = reduce(inputPts); } + else { + //-- the points must be made unique + reducedPts = extractUnique(inputPts); + } // sort points for Graham scan. Coordinate[] sortedPts = preSort(reducedPts); @@ -109,9 +106,62 @@ public Geometry getConvexHull() { Coordinate[] cH = toCoordinateArray(cHS); // Convert array to appropriate output geometry. + // (an empty or point result will be detected earlier) return lineOrPolygon(cH); } + /** + * Checks if there are <= 2 unique points, + * which produce an obviously degenerate result. + * If there are more points, returns null to indicate this. + * + * This is a fast check for an obviously degenerate result. + * If the result is not obviously degenerate (at least 3 unique points found) + * the full uniquing of the entire point set is + * done only once during the reduce phase. + * + * @return a degenerate hull geometry, or null if the number of input points is large + */ + private Geometry createFewPointsResult() { + Coordinate[] uniquePts = extractUnique(inputPts, 2); + if (uniquePts == null) { + return null; + } + else if (uniquePts.length == 0) { + return geomFactory.createGeometryCollection(); + } + else if (uniquePts.length == 1) { + return geomFactory.createPoint(uniquePts[0]); + } + else { + return geomFactory.createLineString(uniquePts); + } + } + + private static Coordinate[] extractUnique(Coordinate[] pts) { + return extractUnique(pts, -1); + } + + /** + * Extracts unique coordinates from an array of coordinates, + * up to a maximum count of values. + * If more than the given maximum of unique values are found, + * this is reported by returning null. + * (the expectation is that the original array can then be used). + * + * @param pts an array of Coordinates + * @param maxPts the maximum number of unique points to scan + * @return an array of unique values, or null + */ + private static Coordinate[] extractUnique(Coordinate[] pts, int maxPts) { + Set uniquePts = new HashSet(); + for (Coordinate pt : pts) { + uniquePts.add(pt); + if (maxPts >= 0 && uniquePts.size() > maxPts) return null; + } + return CoordinateArrays.toCoordinateArray(uniquePts); + } + /** * An alternative to Stack.toArray, which is not present in earlier versions * of Java. @@ -140,6 +190,9 @@ protected Coordinate[] toCoordinateArray(Stack stack) { *

        * To satisfy the requirements of the Graham Scan algorithm, * the returned array has at least 3 entries. + *

        + * This has the side effect of making the reduced points unique, + * as required by the convex hull algorithm used. * * @param pts the points to reduce * @return the reduced list of points (at least 3) @@ -147,29 +200,28 @@ protected Coordinate[] toCoordinateArray(Stack stack) { private Coordinate[] reduce(Coordinate[] inputPts) { //Coordinate[] polyPts = computeQuad(inputPts); - Coordinate[] polyPts = computeOctRing(inputPts); - //Coordinate[] polyPts = null; - + Coordinate[] innerPolyPts = computeInnerOctolateralRing(inputPts); + // unable to compute interior polygon for some reason - if (polyPts == null) + if (innerPolyPts == null) return inputPts; // LinearRing ring = geomFactory.createLinearRing(polyPts); // System.out.println(ring); // add points defining polygon - Set reducedSet = new TreeSet(); - for (int i = 0; i < polyPts.length; i++) { - reducedSet.add(polyPts[i]); + Set reducedSet = new HashSet(); + for (int i = 0; i < innerPolyPts.length; i++) { + reducedSet.add(innerPolyPts[i]); } /** * Add all unique points not in the interior poly. - * CGAlgorithms.isPointInRing is not defined for points actually on the ring, + * CGAlgorithms.isPointInRing is not defined for points exactly on the ring, * but this doesn't matter since the points of the interior polygon * are forced to be in the reduced set. */ for (int i = 0; i < inputPts.length; i++) { - if (! PointLocation.isInRing(inputPts[i], polyPts)) { + if (! PointLocation.isInRing(inputPts[i], innerPolyPts)) { reducedSet.add(inputPts[i]); } } @@ -277,8 +329,8 @@ private boolean isBetween(Coordinate c1, Coordinate c2, Coordinate c3) { return false; } - private Coordinate[] computeOctRing(Coordinate[] inputPts) { - Coordinate[] octPts = computeOctPts(inputPts); + private Coordinate[] computeInnerOctolateralRing(Coordinate[] inputPts) { + Coordinate[] octPts = computeInnerOctolateralPts(inputPts); CoordinateList coordList = new CoordinateList(); coordList.add(octPts, false); @@ -290,7 +342,14 @@ private Coordinate[] computeOctRing(Coordinate[] inputPts) { return coordList.toCoordinateArray(); } - private Coordinate[] computeOctPts(Coordinate[] inputPts) + /** + * Computes the extremal points of an inner octolateral. + * Some points may be duplicates - these are collapsed later. + * + * @param inputPts the points to compute the octolateral for + * @return the extremal points of the octolateral + */ + private Coordinate[] computeInnerOctolateralPts(Coordinate[] inputPts) { Coordinate[] pts = new Coordinate[8]; for (int j = 0; j < pts.length; j++) { @@ -338,8 +397,6 @@ private Geometry lineOrPolygon(Coordinate[] coordinates) { coordinates = cleanRing(coordinates); if (coordinates.length == 3) { return geomFactory.createLineString(new Coordinate[]{coordinates[0], coordinates[1]}); -// return new LineString(new Coordinate[]{coordinates[0], coordinates[1]}, -// geometry.getPrecisionModel(), geometry.getSRID()); } LinearRing linearRing = geomFactory.createLinearRing(coordinates); return geomFactory.createPolygon(linearRing); diff --git a/modules/core/src/test/java/test/jts/perf/algorithm/ConvexHullPerfTest.java b/modules/core/src/test/java/test/jts/perf/algorithm/ConvexHullPerfTest.java new file mode 100644 index 0000000000..fc551f8f02 --- /dev/null +++ b/modules/core/src/test/java/test/jts/perf/algorithm/ConvexHullPerfTest.java @@ -0,0 +1,48 @@ +package test.jts.perf.algorithm; + +import java.util.ArrayList; +import java.util.Random; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.util.GeometricShapeFactory; + +import test.jts.perf.PerformanceTestCase; +import test.jts.perf.PerformanceTestRunner; + +public class ConvexHullPerfTest extends PerformanceTestCase { + public static void main(String args[]) { + PerformanceTestRunner.run(ConvexHullPerfTest.class); + } + + private MultiPoint geom; + + public ConvexHullPerfTest(String name) + { + super(name); + setRunSize(new int[] { 1000, 10_000, 100_000, 1_000_000 }); + setRunIterations(100); + } + + public void startRun(int num) + { + System.out.println("Running with size " + num); + geom = createRandomMultiPoint(num); + } + + private MultiPoint createRandomMultiPoint(int num) { + Coordinate[] pts = new Coordinate[num]; + Random rand = new Random(1324); + for (int i = 0; i < num; i++) { + pts[i] = new Coordinate(rand.nextDouble()*100, rand.nextDouble()*100); + } + GeometryFactory fact = new GeometryFactory(); + return fact.createMultiPointFromCoords(pts); + } + + public void runConvexHull() { + Geometry convextHull = geom.convexHull(); + } +} From 618fde91e6a4f21b33f046386f8259ca59e7951d Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 20 Jun 2023 22:18:40 -0700 Subject: [PATCH 64/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index 20564a634f..f531b000ca 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -58,6 +58,7 @@ Distributions for older JTS versions can be obtained at the ### Performance Improvements * Improve `Polygonizer` performance in some cases with many islands (#906) +* Improve Convex Hull performance by avoiding duplicate uniquing (#985) # Version 1.19 From 2292bf625d0996b1b5c9542181bcc5f89667ff6e Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 21 Jun 2023 08:03:06 -0700 Subject: [PATCH 65/79] Javadoc --- .../locationtech/jts/algorithm/ConvexHull.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java index 438d69f718..973ed20d0c 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/ConvexHull.java @@ -144,19 +144,21 @@ private static Coordinate[] extractUnique(Coordinate[] pts) { /** * Extracts unique coordinates from an array of coordinates, - * up to a maximum count of values. + * up to an (optional) maximum count of values. * If more than the given maximum of unique values are found, * this is reported by returning null. - * (the expectation is that the original array can then be used). + * This avoids scanning all input points if not needed. + * If the maximum points is not specified, all unique points are extracted. * * @param pts an array of Coordinates - * @param maxPts the maximum number of unique points to scan + * @param maxPts the maximum number of unique points to scan, or -1 * @return an array of unique values, or null */ private static Coordinate[] extractUnique(Coordinate[] pts, int maxPts) { Set uniquePts = new HashSet(); for (Coordinate pt : pts) { uniquePts.add(pt); + //-- if maxPts is provided, exit if more unique pts found if (maxPts >= 0 && uniquePts.size() > maxPts) return null; } return CoordinateArrays.toCoordinateArray(uniquePts); @@ -403,10 +405,11 @@ private Geometry lineOrPolygon(Coordinate[] coordinates) { } /** - *@param vertices the vertices of a linear ring, which may or may not be + * Cleans a list of points by removing interior collinear vertices. + * + * @param vertices the vertices of a linear ring, which may or may not be * flattened (i.e. vertices collinear) - *@return the coordinates with unnecessary (collinear) vertices - * removed + * @return the coordinates with unnecessary (collinear) vertices removed */ private Coordinate[] cleanRing(Coordinate[] original) { Assert.equals(original[0], original[original.length - 1]); From 38d2b5ecacdf6fc2a3edb3b16eb8f57cdd5a8a22 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 28 Jun 2023 11:49:23 -0700 Subject: [PATCH 66/79] Fix Geometry.getCoordinate empty element handling (#987) --- .../org/locationtech/jts/geom/Geometry.java | 9 +-- .../jts/geom/GeometryCollection.java | 8 ++- .../jts/geom/GeometryCoordinateTest.java | 68 +++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateTest.java diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java index b0dd119b96..ba0d353fb2 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java @@ -346,13 +346,14 @@ public PrecisionModel getPrecisionModel() { } /** - * Returns a vertex of this Geometry - * (usually, but not necessarily, the first one). + * Returns a vertex of this geometry + * (usually, but not necessarily, the first one), + * or null if the geometry is empty. * The returned coordinate should not be assumed - * to be an actual Coordinate object used in + * to be an actual Coordinate object used in * the internal representation. * - *@return a {@link Coordinate} which is a vertex of this Geometry. + *@return a coordinate which is a vertex of this Geometry. *@return null if this Geometry is empty */ public abstract Coordinate getCoordinate(); diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java index 42e0f8d1d5..06b0a4a6f9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java @@ -57,8 +57,12 @@ public GeometryCollection(Geometry[] geometries, GeometryFactory factory) { } public Coordinate getCoordinate() { - if (isEmpty()) return null; - return geometries[0].getCoordinate(); + for (int i = 0; i < geometries.length; i++) { + if (! geometries[i].isEmpty()) { + return geometries[i].getCoordinate(); + } + } + return null; } /** diff --git a/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateTest.java b/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateTest.java new file mode 100644 index 0000000000..bfaadeda23 --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.geom; + +import test.jts.GeometryTestCase; + +public class GeometryCoordinateTest extends GeometryTestCase { + + public static void main(String[] args) throws Exception { + junit.textui.TestRunner.run(GeometryCoordinateTest.class); + } + + public GeometryCoordinateTest(String name) { + super(name); + } + + public void testPoint() { + checkCoordinate( "POINT (1 1)", 1, 1); + } + + public void testLineString() { + checkCoordinate( "LINESTRING (1 1, 2 2)", 1, 1); + } + + public void testPolygon() { + checkCoordinate( "POLYGON ((1 1, 1 2, 2 1, 1 1))", 1, 1); + } + + public void testEmptyElementsAll() { + checkCoordinate( "GEOMETRYCOLLECTION ( LINESTRING EMPTY, POINT EMPTY )"); + } + + public void testEmptyFirstElementPolygonal() { + checkCoordinate( "MULTIPOLYGON ( EMPTY, ((1 1, 1 2, 2 1, 1 1)) )", 1, 1); + } + + public void testEmptyFirstElement() { + checkCoordinate( "GEOMETRYCOLLECTION ( LINESTRING EMPTY, POINT(1 1) )", 1, 1); + } + + public void testEmptySecondElement() { + checkCoordinate( "GEOMETRYCOLLECTION ( POINT(1 1), LINESTRING EMPTY )", 1, 1); + } + + private void checkCoordinate(String wkt, int x, int y) { + checkCoordinate(read(wkt), new Coordinate(x, y)); + } + + private void checkCoordinate(final Geometry g, Coordinate expected) { + Coordinate actual = g.getCoordinate(); + checkEqualXY( expected, actual ); + } + + private void checkCoordinate(String wkt) { + Geometry g = read(wkt); + Coordinate actual = g.getCoordinate(); + assertNull(actual); + } +} From b4d5b4972f387508247b3997076ad0ed68d135fa Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 28 Jun 2023 11:50:38 -0700 Subject: [PATCH 67/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index f531b000ca..a2de6d8511 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -54,6 +54,7 @@ Distributions for older JTS versions can be obtained at the * Fix `MaximumInscribedCircle` and `LargestEmptyCircle` to avoid long looping for thin inputs (#978) * Fix `OffsetCurve` to use a minimum QuadrantSegs value (#981) * Fix `HilbertEncoder` Y extent handling +* Fix `Geometry.getCoordinate` to return non-null coordinate for collections with empty first element (#987) ### Performance Improvements From d92f783163d9440fcc10c729143787bf7b9fe8f9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 28 Jun 2023 12:21:09 -0700 Subject: [PATCH 68/79] Fix LargestEmptyCircle to handle polygonal obstacles (#988) --- .../construct/IndexedDistanceToPoint.java | 81 +++++++++++++++++++ .../IndexedPointInPolygonsLocater.java | 67 +++++++++++++++ .../construct/LargestEmptyCircle.java | 24 +++--- .../jts/geom/util/PolygonalExtracter.java | 59 ++++++++++++++ .../construct/LargestEmptyCircleTest.java | 7 +- 5 files changed, 222 insertions(+), 16 deletions(-) create mode 100644 modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java create mode 100644 modules/core/src/main/java/org/locationtech/jts/geom/util/PolygonalExtracter.java diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java new file mode 100644 index 0000000000..b67375613b --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm.construct; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.operation.distance.IndexedFacetDistance; + +/** + * Computes the distance between a point and a geometry + * (which may be a collection containing any type of geometry). + * Also computes the pair of points containing the input + * point and the nearest point on the geometry. + * + * @author mdavis + * + */ +class IndexedDistanceToPoint { + + private Geometry targetGeometry; + private IndexedFacetDistance facetDistance; + private IndexedPointInPolygonsLocater ptLocater; + + public IndexedDistanceToPoint(Geometry geom) { + this.targetGeometry = geom; + } + + private void init() { + if (facetDistance != null) + return; + facetDistance = new IndexedFacetDistance(targetGeometry); + ptLocater = new IndexedPointInPolygonsLocater(targetGeometry); + } + + /** + * Computes the distance from a point to the geometry. + * + * @param pt the input point + * @return the distance to the geometry + */ + public double distance(Point pt) { + init(); + //-- distance is 0 if point is inside a target polygon + if (isInArea(pt)) { + return 0; + } + return facetDistance.distance(pt); + } + + private boolean isInArea(Point pt) { + return Location.EXTERIOR != ptLocater.locate(pt.getCoordinate()); + } + + /** + * Gets the nearest locations between the geometry and a point. + * The first location lies on the geometry, + * and the second location is the provided point. + * + * @param pt the point to compute the nearest location for + * @return a pair of locations + */ + public Coordinate[] nearestPoints(Point pt) { + init(); + if (isInArea(pt)) { + Coordinate p = pt.getCoordinate(); + return new Coordinate[] { p.copy(), p.copy() }; + } + return facetDistance.nearestPoints(pt); + } +} diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java new file mode 100644 index 0000000000..34d42ba1b0 --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.algorithm.construct; + +import java.util.List; + +import org.locationtech.jts.algorithm.locate.IndexedPointInAreaLocator; +import org.locationtech.jts.algorithm.locate.PointOnGeometryLocator; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Location; +import org.locationtech.jts.geom.util.PolygonalExtracter; +import org.locationtech.jts.index.strtree.STRtree; + +/** + * Determines the location of a point in the polygonal elements of a geometry. + * Uses spatial indexing to provide efficient performance. + * + * @author mdavis + * + */ +class IndexedPointInPolygonsLocater implements PointOnGeometryLocator { + + private Geometry geom; + private List polys; + private STRtree index; + + public IndexedPointInPolygonsLocater(Geometry geom) { + this.geom = geom; + } + + private void init() { + if (polys != null) + return; + polys = PolygonalExtracter.getPolygonals(geom); + index = new STRtree(); + for (int i = 0; i < polys.size(); i++) { + Geometry poly = polys.get(i); + index.insert(poly.getEnvelopeInternal(), new IndexedPointInAreaLocator(poly)); + } + } + + @Override + public int locate(Coordinate p) { + init(); + + List results = index.query(new Envelope(p)); + for (int i = 0; i < results.size(); i++) { + IndexedPointInAreaLocator ptLocater = (IndexedPointInAreaLocator) results.get(i); + int loc = ptLocater.locate(p); + if (loc != Location.EXTERIOR) + return loc; + } + return Location.EXTERIOR; + } + +} diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java index 3d23c7a630..53308484ca 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java @@ -29,8 +29,7 @@ /** * Constructs the Largest Empty Circle for a set * of obstacle geometries, up to a given accuracy distance tolerance. - * The obstacles are point and line geometries. - * (Polygonal obstacles may be supplied, but only their boundaries are used.) + * The obstacles may be any combination of point, linear and polygonal geometries. *

        * The Largest Empty Circle (LEC) is the largest circle * whose interior does not intersect with any obstacle @@ -45,18 +44,13 @@ * If it is not specified the convex hull of the obstacles is used as the boundary. *

        * To compute an LEC which lies wholly within - * a polygonal boundary, include the boundary polygon as an obstacle as well. + * a polygonal boundary, include the boundary of the polygon(s) as an obstacle. *

        * The implementation uses a successive-approximation technique * over a grid of square cells covering the obstacles and boundary. * The grid is refined using a branch-and-bound algorithm. * Point containment and distance are computed in a performant * way by using spatial indexes. - *

        - *

        Future Enhancements

        - *
          - *
        • Support polygons as obstacles - *
        * * @author Martin Davis * @@ -131,8 +125,8 @@ public static LineString getRadiusLine(Geometry obstacles, Geometry boundary, do private double tolerance; private GeometryFactory factory; - private IndexedPointInAreaLocator ptLocater; - private IndexedFacetDistance obstacleDistance; + private IndexedDistanceToPoint obstacleDistance; + private IndexedPointInAreaLocator boundaryPtLocater; private IndexedFacetDistance boundaryDistance; private Envelope gridEnv; private Cell farthestCell; @@ -168,7 +162,7 @@ public LargestEmptyCircle(Geometry obstacles, Geometry boundary, double toleranc this.boundary = boundary; this.factory = obstacles.getFactory(); this.tolerance = tolerance; - obstacleDistance = new IndexedFacetDistance( obstacles ); + obstacleDistance = new IndexedDistanceToPoint( obstacles ); } /** @@ -220,7 +214,7 @@ public LineString getRadiusLine() { * @return the signed distance to the constraints (negative indicates outside the boundary) */ private double distanceToConstraints(Point p) { - boolean isOutide = Location.EXTERIOR == ptLocater.locate(p.getCoordinate()); + boolean isOutide = Location.EXTERIOR == boundaryPtLocater.locate(p.getCoordinate()); if (isOutide) { double boundaryDist = boundaryDistance.distance(p); return -boundaryDist; @@ -244,7 +238,7 @@ private void initBoundary() { gridEnv = bounds.getEnvelopeInternal(); // if bounds does not enclose an area cannot create a ptLocater if (bounds.getDimension() >= 2) { - ptLocater = new IndexedPointInAreaLocator( bounds ); + boundaryPtLocater = new IndexedPointInAreaLocator( bounds ); boundaryDistance = new IndexedFacetDistance( bounds ); } } @@ -255,8 +249,8 @@ private void compute() { // check if already computed if (centerCell != null) return; - // if ptLocater is not present then result is degenerate (represented as zero-radius circle) - if (ptLocater == null) { + // if boundaryPtLocater is not present then result is degenerate (represented as zero-radius circle) + if (boundaryPtLocater == null) { Coordinate pt = obstacles.getCoordinate(); centerPt = pt.copy(); centerPoint = factory.createPoint(pt); diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/util/PolygonalExtracter.java b/modules/core/src/main/java/org/locationtech/jts/geom/util/PolygonalExtracter.java new file mode 100644 index 0000000000..42b892f4bb --- /dev/null +++ b/modules/core/src/main/java/org/locationtech/jts/geom/util/PolygonalExtracter.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Martin Davis. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ +package org.locationtech.jts.geom.util; + +import java.util.ArrayList; +import java.util.List; + +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +/** + * Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry}. + */ +public class PolygonalExtracter +{ + /** + * Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry} + * and adds them to the provided list. + * + * @param geom the geometry from which to extract + * @param list the list to add the extracted elements to + */ + public static List getPolygonals(Geometry geom, List list) + { + if (geom instanceof Polygon || geom instanceof MultiPolygon) { + list.add(geom); + } + else if (geom instanceof GeometryCollection) { + for (int i = 0; i < geom.getNumGeometries(); i++) { + getPolygonals(geom.getGeometryN(i), list); + } + } + // skip non-Polygonal elemental geometries + return list; + } + + /** + * Extracts the {@link Polygon} and {@link MultiPolygon} elements from a {@link Geometry} + * and returns them in a list. + * + * @param geom the geometry from which to extract + */ + public static List getPolygonals(Geometry geom) + { + return getPolygonals(geom, new ArrayList()); + } + +} diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java index 7382bf66a9..6d54874ce1 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java @@ -93,11 +93,16 @@ public void testBoundaryMultiSquares() { } public void testBoundaryAsObstacle() { - checkCircle("GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9)), POINT (4 3), POINT (7 6))", + checkCircle("GEOMETRYCOLLECTION (LINESTRING (1 9, 9 9, 9 1, 1 1, 1 9), POINT (4 3), POINT (7 6))", "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", 0.01, 4, 6, 3 ); } + public void testObstacleEmptyElement() { + checkCircle("GEOMETRYCOLLECTION (LINESTRING EMPTY, POINT (4 3), POINT (7 6), POINT (4 6))", + 0.01, 5.5, 4.5, 2.12 ); + } + //======================================================== /** From 83845f0bee1269d7e1c1f8c922a9b1e0535344a5 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Wed, 28 Jun 2023 12:21:52 -0700 Subject: [PATCH 69/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index a2de6d8511..c98790ce64 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -55,6 +55,7 @@ Distributions for older JTS versions can be obtained at the * Fix `OffsetCurve` to use a minimum QuadrantSegs value (#981) * Fix `HilbertEncoder` Y extent handling * Fix `Geometry.getCoordinate` to return non-null coordinate for collections with empty first element (#987) +* Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) ### Performance Improvements From d6a280c4fc73df694441baf325122e466a08d82c Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 30 Jun 2023 15:05:12 -0700 Subject: [PATCH 70/79] Code cleanup --- .../jts/algorithm/RobustLineIntersector.java | 33 ++----------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java index e6cd2ac3ce..21a5891773 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/RobustLineIntersector.java @@ -223,46 +223,19 @@ private static Coordinate copy(Coordinate p) { /** * This method computes the actual value of the intersection point. - * To obtain the maximum precision from the intersection calculation, - * the coordinates are normalized by subtracting the minimum - * ordinate values (in absolute value). This has the effect of - * removing common significant digits from the calculation to - * maintain more bits of precision. + * It is rounded to the precision model if being used. */ private Coordinate intersection( Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { Coordinate intPt = intersectionSafe(p1, p2, q1, q2); - - /* - // TESTING ONLY - Coordinate intPtDD = CGAlgorithmsDD.intersection(p1, p2, q1, q2); - double dist = intPt.distance(intPtDD); - System.out.println(intPt + " - " + intPtDD + " dist = " + dist); - //intPt = intPtDD; - */ - - /** - * Due to rounding it can happen that the computed intersection is - * outside the envelopes of the input segments. Clearly this - * is inconsistent. - * This code checks this condition and forces a more reasonable answer - * - * MD - May 4 2005 - This is still a problem. Here is a failure case: - * - * LINESTRING (2089426.5233462777 1180182.3877339689, 2085646.6891757075 1195618.7333999649) - * LINESTRING (1889281.8148903656 1997547.0560044837, 2259977.3672235999 483675.17050843034) - * int point = (2097408.2633752143,1144595.8008114607) - * - * MD - Dec 14 2006 - This does not seem to be a failure case any longer - */ + if (! isInSegmentEnvelopes(intPt)) { // System.out.println("Intersection outside segment envelopes: " + intPt); // compute a safer result // copy the coordinate, since it may be rounded later intPt = copy(nearestEndpoint(p1, p2, q1, q2)); -// intPt = CentralEndpointIntersector.getIntersection(p1, p2, q1, q2); // System.out.println("Segments: " + this); // System.out.println("Snapped to " + intPt); @@ -288,7 +261,7 @@ private void checkDD(Coordinate p1, Coordinate p2, Coordinate q1, */ /** - * Computes a segment intersection using homogeneous coordinates. + * Computes a segment intersection. * Round-off error can cause the raw computation to fail, * (usually due to the segments being approximately parallel). * If this happens, a reasonable approximation is computed instead. From 668c4bce68c0043f5f2bf6da6d8d481264e391f9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jul 2023 08:47:10 -0700 Subject: [PATCH 71/79] Make intersection computation more robust (#989) --- .../jts/algorithm/Intersection.java | 17 +++++++ .../algorithm/RobustLineIntersectionTest.java | 8 +-- .../jts/operation/relate/ContainsTest.java | 41 +++++++++++++--- .../jts/operation/relate/RelateTest.java | 32 +++++++++--- .../testxml/general/TestRelateLL.xml | 49 ++++++++++++++++++- .../general/TestUnaryUnionFloating.xml | 3 +- 6 files changed, 128 insertions(+), 22 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java index cc86506199..34ac57675b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java @@ -44,6 +44,23 @@ public class Intersection { * @see CGAlgorithmsDD#intersection(Coordinate, Coordinate, Coordinate, Coordinate) */ public static Coordinate intersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { + return CGAlgorithmsDD.intersection(p1, p2, q1, q2); + } + + /** + * Compute intersection of two lines, using a floating-point algorithm. + * This is less accurate than {@link CGAlgorithmsDD#intersection(Coordinate, Coordinate, Coordinate, Coordinate)}. + * It has caused spatial predicate failures in some cases. + * This is kept for testing purposes. + * + * @param p1 an endpoint of line 1 + * @param p2 an endpoint of line 1 + * @param q1 an endpoint of line 2 + * @param q2 an endpoint of line 2 + * @return the intersection point between the lines, if there is one, + * or null if the lines are parallel or collinear + */ + private static Coordinate intersectionFP(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { // compute midpoint of "kernel envelope" double minX0 = p1.x < p2.x ? p1.x : p2.x; double minY0 = p1.y < p2.y ? p1.y : p2.y; diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/RobustLineIntersectionTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/RobustLineIntersectionTest.java index 08f7485878..15419ca95c 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/RobustLineIntersectionTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/RobustLineIntersectionTest.java @@ -74,7 +74,7 @@ public void testCentralEndpointHeuristicFailure2() "LINESTRING (-58.00593335955 -1.43739086465, -513.86101637525 -457.29247388035)", "LINESTRING (-215.22279674875 -158.65425425385, -218.1208801283 -160.68343590235)", 1, - "POINT ( -215.22279674875 -158.65425425385 )", + "POINT ( -215.22279674875003 -158.65425425385004 )", 0); } @@ -192,7 +192,7 @@ public void testDaveSkeaCase() "LINESTRING ( 1889281.8148903656 1997547.0560044837, 2259977.3672235999 483675.17050843034 )", 1, new Coordinate[] { - new Coordinate(2087536.6062609926, 1187900.560566967), + new Coordinate(2087600.4716727887, 1187639.7426241424), }, 0); } @@ -209,7 +209,7 @@ public void testCmp5CaseWKT() "LINESTRING (4348433.26211463 5552595.47838573, 4348440.8493874 5552599.27202212 )", 1, new Coordinate[] { - new Coordinate(4348440.8493874, 5552599.27202212), + new Coordinate(4348440.849387399, 5552599.27202212), }, 0); } @@ -230,7 +230,7 @@ public void testCmp5CaseRaw() new Coordinate(4348440.8493874, 5552599.27202212) }, 1, new Coordinate[] { - new Coordinate(4348440.8493874, 5552599.27202212), + new Coordinate(4348440.849387399, 5552599.27202212), }, 0); } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relate/ContainsTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relate/ContainsTest.java index 2ca6069608..0cc2a459f1 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relate/ContainsTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relate/ContainsTest.java @@ -16,8 +16,8 @@ import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.io.WKTReader; -import junit.framework.TestCase; import junit.textui.TestRunner; +import test.jts.GeometryTestCase; /** @@ -27,9 +27,9 @@ * @version 1.7 */ public class ContainsTest - extends TestCase + extends GeometryTestCase { - public static void main(String args[]) { + public static void main(String[] args) { TestRunner.run(ContainsTest.class); } @@ -45,6 +45,8 @@ public ContainsTest(String name) * From GEOS #572. * A case where B is contained in A, but * the JTS relate algorithm fails to compute this correctly. + * when using an FP intersection algorithm. + * This case works when using CGAlgorithmsDD#intersection(Coordinate, Coordinate, Coordinate, Coordinate). * * The cause is that the long segment in A nodes the single-segment line in B. * The node location cannot be computed precisely. @@ -63,8 +65,35 @@ public void testContainsIncorrect() { String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; String b = "LINESTRING (0 0, 2 2)"; - - // for now assert this as false, although it should be true - assertTrue(! a.contains(b)); + checkContains(a, b); + } + + /** + * From GEOS #933. + * A case where B is contained in A, but + * the JTS relate algorithm fails to compute this correctly. + * when using an FP intersection algorithm. + * This case works when using CGAlgorithmsDD#intersection(Coordinate, Coordinate, Coordinate, Coordinate). + */ + public void testContainsGEOS933() + throws Exception + { + String a = "MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1))"; + String b = "LINESTRING (0 0, 1 1)"; + checkContains(a, b); + } + + private void checkContains(String wktA, String wktB) { + Geometry geomA = read(wktA); + Geometry geomB = read(wktB); + boolean actual = geomA.contains(geomB); + assertTrue(actual); + } + + private void checkContainsError(String wktA, String wktB) { + Geometry geomA = read(wktA); + Geometry geomB = read(wktB); + boolean actual = geomA.contains(geomB); + assertFalse(actual); } } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java index d3f8fee011..8b7ee3e872 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/relate/RelateTest.java @@ -42,21 +42,37 @@ public RelateTest(String name) } /** - * From GEOS #572 + * From https://github.com/locationtech/jts/issues/396 * - * The cause is that the longer line nodes the single-segment line. - * The node then tests as not lying precisely on the original longer line. + * The original failure is caused by the intersection computed + * during noding not lying exactly on each original line segment. + * This is due to numerical error in the FP intersection algorithm. + * This is fixed by using DD intersection calculation. */ - public void testContainsIncorrectIMMatrix() + public void testContainsNoding() { String a = "LINESTRING (1 0, 0 2, 0 0, 2 2)"; String b = "LINESTRING (0 0, 2 2)"; - // actual matrix is 001F001F2 - // true matrix should be 101F00FF2 - runRelateTest(a, b, "001F001F2" ); + runRelateTest(a, b, "101F00FF2" ); } + /** + * From GEOS https://github.com/libgeos/geos/issues/933 + * + * The original failure is caused by the intersection computed + * during noding not lying exactly on each original line segment. + * This is due to numerical error in the FP intersection algorithm. + * This is fixed by using DD intersection calculation. + */ + public void testContainsNoding2() + { + String a = "MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1))"; + String b = "LINESTRING (0 0, 1 1)"; + + runRelateTest(a, b, "1F1000FF2" ); + } + /** * Tests case where segments intersect properly, but computed intersection point * snaps to a boundary endpoint due to roundoff. @@ -93,6 +109,6 @@ void runRelateTest(String wkt1, String wkt2, String expectedIM) IntersectionMatrix im = RelateOp.relate(g1, g2); String imStr = im.toString(); //System.out.println(imStr); - assertTrue(im.matches(expectedIM)); + assertEquals(expectedIM, imStr); } } diff --git a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml index 4c05b66642..4a4fb4289d 100644 --- a/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml +++ b/modules/tests/src/test/resources/testxml/general/TestRelateLL.xml @@ -1,5 +1,4 @@ - LL - disjoint, non-overlapping envelopes @@ -308,7 +307,7 @@ -LA - closed multiline / empty line +LL - closed multiline / empty line MULTILINESTRING ((0 0, 0 1), (0 1, 1 1, 1 0, 0 0)) @@ -320,4 +319,50 @@ + +LL - test intersection node computation (see https://github.com/locationtech/jts/issues/396) + + LINESTRING (1 0, 0 2, 0 0, 2 2) + + + LINESTRING (0 0, 2 2) + + + true + + true + false + true + false + false + false + true + false + false + false + + + +LL - test intersection node computation (see https://github.com/libgeos/geos/issues/933) + + MULTILINESTRING ((0 0, 1 1), (0.5 0.5, 1 0.1, -1 0.1)) + + + LINESTRING (0 0, 1 1) + + + true + + true + false + true + false + false + false + true + false + false + false + + diff --git a/modules/tests/src/test/resources/testxml/general/TestUnaryUnionFloating.xml b/modules/tests/src/test/resources/testxml/general/TestUnaryUnionFloating.xml index a8bc222ad3..fd6af522e7 100644 --- a/modules/tests/src/test/resources/testxml/general/TestUnaryUnionFloating.xml +++ b/modules/tests/src/test/resources/testxml/general/TestUnaryUnionFloating.xml @@ -12,8 +12,7 @@ - POLYGON ((0.2942036115049298 2.226702215615205, -3 -2, -6 900, 700 900, 699.6114719806972 899.5000276853219, 700 899.5, 700 860, 670.2204017222961 861.6785046602191, 0.3 -0.4, 0.2942036115049298 2.226702215615205)) - + POLYGON ((-3 -2, -6 900, 700 900, 699.6114719806972 899.5000276853219, 700 899.5, 700 860, 670.2204017222961 861.6785046602191, 0.3 -0.4, 0.29420361150493 2.226702215615145, -3 -2)) From dcb4edf173529431c87d32df24e7cd09db56b2cf Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jul 2023 08:47:56 -0700 Subject: [PATCH 72/79] Update JTS_Version_History.md --- doc/JTS_Version_History.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/JTS_Version_History.md b/doc/JTS_Version_History.md index c98790ce64..794cf66ad6 100644 --- a/doc/JTS_Version_History.md +++ b/doc/JTS_Version_History.md @@ -56,6 +56,7 @@ Distributions for older JTS versions can be obtained at the * Fix `HilbertEncoder` Y extent handling * Fix `Geometry.getCoordinate` to return non-null coordinate for collections with empty first element (#987) * Fix `LargestEmptyCircle` to handle polygonal obstacles (#988) +* Make intersection computation more robust (#989) ### Performance Improvements From 69ba32b7e53f2bfe85db4bb722e30502252ed014 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 4 Jul 2023 09:04:39 -0700 Subject: [PATCH 73/79] Fix TestBuilder Coverage.validate result to be GC --- .../jtstest/function/CoverageFunctions.java | 10 +++---- .../jtstest/function/FunctionsUtil.java | 27 ++++++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java index 7dcc957ef3..74f52f6a1b 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/CoverageFunctions.java @@ -34,16 +34,16 @@ public static Geometry validatePolygonWithGaps(Geometry geom, Geometry adjacentP return CoveragePolygonValidator.validate(geom, toGeometryArray(adjacentPolys), gapWidth); } - public static Geometry validateCoverage(Geometry geom) { + public static Geometry validate(Geometry geom) { Geometry[] invalid = CoverageValidator.validate(toGeometryArray(geom)); - return FunctionsUtil.buildGeometry(invalid); + return FunctionsUtil.buildGeometryCollection(invalid, geom.getFactory().createLineString()); } - public static Geometry validateCoverageWithGaps(Geometry geom, + public static Geometry validateWithGaps(Geometry geom, @Metadata(title="Gap width") double gapWidth) { Geometry[] invalid = CoverageValidator.validate(toGeometryArray(geom), gapWidth); - return FunctionsUtil.buildGeometry(invalid); + return FunctionsUtil.buildGeometryCollection(invalid, geom.getFactory().createLineString()); } public static Geometry findGaps(Geometry geom, @@ -53,7 +53,7 @@ public static Geometry findGaps(Geometry geom, } @Metadata(description="Fast Union of a coverage") - public static Geometry unionCoverage(Geometry coverage) { + public static Geometry union(Geometry coverage) { Geometry[] cov = toGeometryArray(coverage); return CoverageUnion.union(cov); } diff --git a/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java b/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java index f4ee726b45..3c331308e7 100644 --- a/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java +++ b/modules/app/src/main/java/org/locationtech/jtstest/function/FunctionsUtil.java @@ -79,10 +79,7 @@ public static Geometry buildGeometry(List geoms, Geometry parentGeom) public static Geometry buildGeometry(Geometry[] geoms) { - GeometryFactory gf = JTSTestBuilder.getGeometryFactory(); - if (geoms.length > 0) { - gf = getFactoryOrDefault(geoms[0]); - } + GeometryFactory gf = getFactory(geoms); List geomList = new ArrayList(); for (Geometry geom : geoms) { @@ -95,6 +92,28 @@ public static Geometry buildGeometry(Geometry[] geoms) return gf.buildGeometry(geomList); } + public static Geometry buildGeometryCollection(Geometry[] geoms, Geometry nullGeom) + { + GeometryFactory gf = getFactory(geoms); + + Geometry[] geomArray = new Geometry[geoms.length]; + for (int i = 0; i < geoms.length; i++) { + Geometry srcGeom = geoms[i] == null ? nullGeom : geoms[i]; + if (srcGeom != null) { + geomArray[i] = srcGeom.copy(); + } + } + return gf.createGeometryCollection(geomArray); + } + + private static GeometryFactory getFactory(Geometry[] geoms) { + GeometryFactory gf = JTSTestBuilder.getGeometryFactory(); + if (geoms.length > 0) { + gf = getFactoryOrDefault(geoms[0]); + } + return gf; + } + public static Geometry buildGeometry(Geometry a, Geometry b) { Geometry[] geoms = toGeometryArray(a, b); return getFactoryOrDefault(a, b).createGeometryCollection(geoms); } From c36a5c27f62d1c3864fd754b68e80386fc93adf4 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 10 Jul 2023 13:47:58 -0700 Subject: [PATCH 74/79] Add testing code --- .../main/java/org/locationtech/jts/algorithm/Intersection.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java index 34ac57675b..18812c964b 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/Intersection.java @@ -45,6 +45,9 @@ public class Intersection { */ public static Coordinate intersection(Coordinate p1, Coordinate p2, Coordinate q1, Coordinate q2) { return CGAlgorithmsDD.intersection(p1, p2, q1, q2); + //-- this is less robust + //return intersectionFP(p1, p2, q1, q2); + } /** From a1477a833cb8121aa148496dca20b7d134dcd4f3 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Mon, 10 Jul 2023 13:48:14 -0700 Subject: [PATCH 75/79] Add Node.toString method --- .../src/main/java/org/locationtech/jts/geomgraph/Node.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/core/src/main/java/org/locationtech/jts/geomgraph/Node.java b/modules/core/src/main/java/org/locationtech/jts/geomgraph/Node.java index a44f267c53..3702baba72 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geomgraph/Node.java +++ b/modules/core/src/main/java/org/locationtech/jts/geomgraph/Node.java @@ -152,4 +152,9 @@ public void print(PrintStream out) { out.println("node " + coord + " lbl: " + label); } + + public String toString() { + return "Node(" + coord.x + ", " + coord.y + ")"; + } + } From 9fc2e9fce442c8c38fdf903ae9525009dd4219bb Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 14 Jul 2023 11:26:12 -0700 Subject: [PATCH 76/79] Add LEC unit tests for polygon obstacles --- .../construct/LargestEmptyCircleTest.java | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java index 6d54874ce1..73a602d872 100644 --- a/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircleTest.java @@ -15,6 +15,8 @@ public static void main(String args[]) { public LargestEmptyCircleTest(String name) { super(name); } + //------------ Point Obstacles ----------------- + public void testPointsSquare() { checkCircle("MULTIPOINT ((100 100), (100 200), (200 200), (200 100))", 0.01, 150, 150, 70.71 ); @@ -30,6 +32,13 @@ public void testPointsTriangleInterior() { 0.01, 200.00, 141.66, 108.33 ); } + public void testPoint() { + checkCircleZeroRadius("POINT (100 100)", + 0.01 ); + } + + //------------ Line Obstacles ----------------- + public void testLinesOpenDiamond() { checkCircle("MULTILINESTRING ((50 100, 150 50), (250 50, 350 100), (350 150, 250 200), (50 150, 150 200))", 0.01, 200, 125, 90.13 ); @@ -45,16 +54,11 @@ public void testLinesZigzag() { 0.01, 77.52, 249.99, 54.81 ); } - public void testPointsLinesTriangle() { + public void testLinePointTriangle() { checkCircle("GEOMETRYCOLLECTION (LINESTRING (100 100, 300 100), POINT (250 200))", 0.01, 196.49, 164.31, 64.31 ); } - public void testPoint() { - checkCircleZeroRadius("POINT (100 100)", - 0.01 ); - } - public void testLineFlat() { checkCircleZeroRadius("LINESTRING (0 0, 50 50)", 0.01 ); @@ -65,6 +69,23 @@ public void testThinExtent() { 0.01 ); } + //------------ Polygon Obstacles ----------------- + + public void testPolygonConcave() { + checkCircle("POLYGON ((1 9, 9 6, 6 5, 5 3, 8 3, 9 4, 9 1, 1 1, 1 9))", + 0.01, 7.495, 4.216, 1.21); + } + + public void testPolygonsBoxes() { + checkCircle("MULTIPOLYGON (((1 6, 6 6, 6 1, 1 1, 1 6)), ((6 7, 4 7, 4 9, 6 9, 6 7)))", + 0.01, 2.50, 7.50, 1.50); + } + + public void testPolygonLines() { + checkCircle("GEOMETRYCOLLECTION (POLYGON ((1 6, 6 6, 6 1, 1 1, 1 6)), LINESTRING (6 7, 3 9), LINESTRING (1 7, 3 8))", + 0.01, 3.74, 7.14, 1.14); + } + //--------------------------------------------------------- // Obstacles and Boundary @@ -106,7 +127,7 @@ public void testObstacleEmptyElement() { //======================================================== /** - * A coarse distance check, mainly testing + * A simple distance check, mainly testing * that there is not a huge number of iterations. * (This will be revealed by CI taking a very long time!) * From ef493a3c7b8b1ca49d6d35e7a284dade141a09aa Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 14 Jul 2023 15:55:20 -0700 Subject: [PATCH 77/79] Standardize name of IndexedPointInPolygonsLocator --- .../jts/algorithm/construct/IndexedDistanceToPoint.java | 4 ++-- ...olygonsLocater.java => IndexedPointInPolygonsLocator.java} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename modules/core/src/main/java/org/locationtech/jts/algorithm/construct/{IndexedPointInPolygonsLocater.java => IndexedPointInPolygonsLocator.java} (94%) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java index b67375613b..cac5810db9 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedDistanceToPoint.java @@ -30,7 +30,7 @@ class IndexedDistanceToPoint { private Geometry targetGeometry; private IndexedFacetDistance facetDistance; - private IndexedPointInPolygonsLocater ptLocater; + private IndexedPointInPolygonsLocator ptLocater; public IndexedDistanceToPoint(Geometry geom) { this.targetGeometry = geom; @@ -40,7 +40,7 @@ private void init() { if (facetDistance != null) return; facetDistance = new IndexedFacetDistance(targetGeometry); - ptLocater = new IndexedPointInPolygonsLocater(targetGeometry); + ptLocater = new IndexedPointInPolygonsLocator(targetGeometry); } /** diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java similarity index 94% rename from modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java rename to modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java index 34d42ba1b0..c5ef4e1392 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocater.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java @@ -29,13 +29,13 @@ * @author mdavis * */ -class IndexedPointInPolygonsLocater implements PointOnGeometryLocator { +class IndexedPointInPolygonsLocator implements PointOnGeometryLocator { private Geometry geom; private List polys; private STRtree index; - public IndexedPointInPolygonsLocater(Geometry geom) { + public IndexedPointInPolygonsLocator(Geometry geom) { this.geom = geom; } From 7ea675cb9cc87806303144de4405aae4f76316f9 Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Fri, 14 Jul 2023 16:10:47 -0700 Subject: [PATCH 78/79] Improve IndexedPointInPolygonsLocator --- .../construct/IndexedPointInPolygonsLocator.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java index c5ef4e1392..ce99843041 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/IndexedPointInPolygonsLocator.java @@ -32,7 +32,6 @@ class IndexedPointInPolygonsLocator implements PointOnGeometryLocator { private Geometry geom; - private List polys; private STRtree index; public IndexedPointInPolygonsLocator(Geometry geom) { @@ -40,9 +39,9 @@ public IndexedPointInPolygonsLocator(Geometry geom) { } private void init() { - if (polys != null) + if (index != null) return; - polys = PolygonalExtracter.getPolygonals(geom); + List polys = PolygonalExtracter.getPolygonals(geom); index = new STRtree(); for (int i = 0; i < polys.size(); i++) { Geometry poly = polys.get(i); @@ -54,9 +53,8 @@ private void init() { public int locate(Coordinate p) { init(); - List results = index.query(new Envelope(p)); - for (int i = 0; i < results.size(); i++) { - IndexedPointInAreaLocator ptLocater = (IndexedPointInAreaLocator) results.get(i); + List results = index.query(new Envelope(p)); + for (IndexedPointInAreaLocator ptLocater : results) { int loc = ptLocater.locate(p); if (loc != Location.EXTERIOR) return loc; From 46d2cfd39c62a4d7a21d5c2536f095af57a8d5ef Mon Sep 17 00:00:00 2001 From: Martin Davis Date: Tue, 18 Jul 2023 15:13:01 -0700 Subject: [PATCH 79/79] Javadoc --- .../construct/LargestEmptyCircle.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java index 53308484ca..ede804f0f7 100644 --- a/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java +++ b/modules/core/src/main/java/org/locationtech/jts/algorithm/construct/LargestEmptyCircle.java @@ -44,7 +44,7 @@ * If it is not specified the convex hull of the obstacles is used as the boundary. *

        * To compute an LEC which lies wholly within - * a polygonal boundary, include the boundary of the polygon(s) as an obstacle. + * a polygonal boundary, include the boundary of the polygon(s) as a linear obstacle. *

        * The implementation uses a successive-approximation technique * over a grid of square cells covering the obstacles and boundary. @@ -64,9 +64,10 @@ public class LargestEmptyCircle { * Computes the center point of the Largest Empty Circle * interior-disjoint to a set of obstacles, * with accuracy to a given tolerance distance. + * The obstacles may be any collection of points, lines and polygons. * The center of the LEC lies within the convex hull of the obstacles. * - * @param obstacles a geometry representing the obstacles (points and lines) + * @param obstacles a geometry representing the obstacles * @param tolerance the distance tolerance for computing the center point * @return the center point of the Largest Empty Circle */ @@ -78,9 +79,10 @@ public static Point getCenter(Geometry obstacles, double tolerance) { * Computes the center point of the Largest Empty Circle * interior-disjoint to a set of obstacles and within a polygonal boundary, * with accuracy to a given tolerance distance. - * The center of the LEC lies within the boundary. + * The obstacles may be any collection of points, lines and polygons. + * The center of the LEC lies within the given boundary. * - * @param obstacles a geometry representing the obstacles (points and lines) + * @param obstacles a geometry representing the obstacles * @param boundary a polygonal geometry to contain the LEC center * @param tolerance the distance tolerance for computing the center point * @return the center point of the Largest Empty Circle @@ -94,9 +96,10 @@ public static Point getCenter(Geometry obstacles, Geometry boundary, double tole * Computes a radius line of the Largest Empty Circle * interior-disjoint to a set of obstacles, * with accuracy to a given tolerance distance. + * The obstacles may be any collection of points, lines and polygons. * The center of the LEC lies within the convex hull of the obstacles. * - * @param obstacles a geometry representing the obstacles (points and lines) + * @param obstacles a geometry representing the obstacles * @param tolerance the distance tolerance for computing the center point * @return a line from the center of the circle to a point on the edge */ @@ -108,9 +111,10 @@ public static LineString getRadiusLine(Geometry obstacles, double tolerance) { * Computes a radius line of the Largest Empty Circle * interior-disjoint to a set of obstacles and within a polygonal boundary, * with accuracy to a given tolerance distance. - * The center of the LEC lies within the boundary. + * The obstacles may be any collection of points, lines and polygons. + * The center of the LEC lies within the given boundary. * - * @param obstacles a geometry representing the obstacles (points and lines) + * @param obstacles a geometry representing the obstacles * @param boundary a polygonal geometry to contain the LEC center * @param tolerance the distance tolerance for computing the center point * @return a line from the center of the circle to a point on the edge @@ -140,11 +144,13 @@ public static LineString getRadiusLine(Geometry obstacles, Geometry boundary, do /** * Creates a new instance of a Largest Empty Circle construction, - * interior-disjoint to a set of obstacle geometries and within a polygonal boundary. + * interior-disjoint to a set of obstacle geometries + * and having its center within a polygonal boundary. + * The obstacles may be any collection of points, lines and polygons. * If the boundary is null or empty the convex hull * of the obstacles is used as the boundary. * - * @param obstacles a non-empty geometry representing the obstacles (points and lines) + * @param obstacles a non-empty geometry representing the obstacles * @param boundary a polygonal geometry (may be null or empty) * @param tolerance a distance tolerance for computing the circle center point (a positive value) */