├── .github └── workflows │ ├── gradle-publish.yml │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.MD ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── menecats │ └── polybool │ ├── Epsilon.java │ ├── ExperimentalEpsilon.java │ ├── PolyBool.java │ ├── helpers │ └── PolyBoolHelper.java │ ├── internal │ ├── AbstractIntersecter.java │ ├── GeoJSON.java │ ├── LinkedList.java │ ├── NonSelfIntersecter.java │ ├── SegmentChainer.java │ ├── SegmentSelector.java │ └── SelfIntersecter.java │ └── models │ ├── Polygon.java │ ├── Segment.java │ └── geojson │ └── Geometry.java └── test └── java └── com └── menecats └── polybool └── PolyBoolExample.java /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Publish package 4 | 5 | on: 6 | release: 7 | types: [created] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-java@v1 16 | with: 17 | java-version: 1.8 18 | - name: Publish package 19 | run: gradle publish 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | workflow_dispatch: 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up JDK 1.8 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 1.8 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | - name: Build with Gradle 29 | run: ./gradlew build 30 | - uses: actions/upload-artifact@v2 31 | with: 32 | name: Package 33 | path: build/libs 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEA stuffs 2 | /.idea 3 | *.iml 4 | 5 | # Gradle stuffs 6 | .gradle 7 | **/build/ 8 | !src/**/build/ 9 | 10 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 11 | !gradle-wrapper.jar 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Davide Menegatti (@menecats) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # polybool-java 2 | 3 | Java port of [https://github.com/velipso/polybooljs](https://github.com/velipso/polybooljs). 4 | 5 | Boolean operations on polygons (union, intersection, difference, xor). 6 | 7 | # Features 8 | 9 | 1. Clips polygons for all boolean operations 10 | 2. Removes unnecessary vertices 11 | 3. Handles segments that are coincident (overlap perfectly, share vertices, one inside the other, 12 | etc) 13 | 4. Uses formulas that take floating point irregularities into account (via configurable epsilon) 14 | 5. Provides an API for constructing efficient sequences of operations 15 | 6. Support for GeoJSON `"Polygon"` and `"MultiPolygon"` types (experimental) 16 | 17 | # Installing 18 | 19 | To use polybool-java, you need to use the following Maven dependency: 20 | 21 | ```xml 22 | 23 | 24 | com.menecats 25 | polybool-java 26 | 1.0.1 27 | 28 | ``` 29 | 30 | ```groovy 31 | // Gradle (Groovy) 32 | implementation 'com.menecats:polybool-java:1.0.1' 33 | ``` 34 | 35 | or download jars from Maven repository (or via quick links on 36 | the [Release page](https://github.com/Menecats/polybool-java/releases)) 37 | 38 | # Example 39 | 40 | ```java 41 | import com.menecats.polybool.Epsilon; 42 | import com.menecats.polybool.PolyBool; 43 | import com.menecats.polybool.models.Polygon; 44 | 45 | import static com.menecats.polybool.helpers.PolyBoolHelper.*; 46 | 47 | public class PolyBoolExample { 48 | public static void main(String[] args) { 49 | Epsilon eps = epsilon(); 50 | 51 | Polygon intersection = PolyBool.intersect( 52 | eps, 53 | polygon( 54 | region( 55 | point(50, 50), 56 | point(150, 150), 57 | point(190, 50) 58 | ), 59 | region( 60 | point(130, 50), 61 | point(290, 150), 62 | point(290, 50) 63 | ) 64 | ), 65 | polygon( 66 | region( 67 | point(110, 20), 68 | point(110, 110), 69 | point(20, 20) 70 | ), 71 | region( 72 | point(130, 170), 73 | point(130, 20), 74 | point(260, 20), 75 | point(260, 170) 76 | ) 77 | ) 78 | ); 79 | 80 | System.out.println(intersection); 81 | // Polygon { inverted: false, regions: [ 82 | // [[50.0, 50.0], [110.0, 50.0], [110.0, 110.0]], 83 | // [[178.0, 80.0], [130.0, 50.0], [130.0, 130.0], [150.0, 150.0]], 84 | // [[178.0, 80.0], [190.0, 50.0], [260.0, 50.0], [260.0, 131.25]] 85 | // ]} 86 | } 87 | } 88 | ``` 89 | 90 | ![PolyBool Example](https://github.com/voidqk/polybooljs/raw/master/example.png) 91 | 92 | ## Basic Usage 93 | 94 | ```java 95 | Epsilon eps=new Epsilon(); 96 | 97 | Polygon poly=PolyBool.union(eps,poly1,poly2); 98 | Polygon poly=PolyBool.intersect(eps,poly1,poly2); 99 | Polygon poly=PolyBool.difference(eps,poly1,poly2); // poly1 - poly2 100 | Polygon poly=PolyBool.differenceRev(eps,poly1,poly2); // poly2 - poly1 101 | Polygon poly=PolyBool.xor(eps,poly1,poly2); 102 | ``` 103 | 104 | Where `poly1`, `poly2`, and the return value are `Polygon` objects. 105 | 106 | # GeoJSON (experimental) 107 | 108 | There are also functions for converting between the native polygon format and 109 | [GeoJSON](https://tools.ietf.org/html/rfc7946). 110 | 111 | Note: These functions are currently **experimental**, and I'm hoping users can provide feedback. 112 | Please comment in [this issue on GitHub](https://github.com/voidqk/polybooljs/issues/7) -- including 113 | letting me know if it's working as expected. I don't use GeoJSON, but I thought I would take a 114 | crack at conversion functions. 115 | 116 | Use the following functions: 117 | 118 | ```java 119 | Geometry geojson = PolyBool.polygonToGeoJSON(poly); 120 | Polygon poly = PolyBool.polygonFromGeoJSON(geojson); 121 | ``` 122 | 123 | Only `"Polygon"` and `"MultiPolygon"` types are supported. 124 | 125 | # Core API 126 | 127 | ```java 128 | Epsilon eps=new Epsilon(); 129 | 130 | Segments segments=PolyBool.segments(eps,polygon); 131 | Combined combined=PolyBool.combine(eps,segments1,segments2); 132 | 133 | Segments segments=PolyBool.selectUnion(combined); 134 | Segments segments=PolyBool.selectIntersect(combined); 135 | Segments segments=PolyBool.selectDifference(combined); 136 | Segments segments=PolyBool.selectDifferenceRev(combined); 137 | Segments segments=PolyBool.selectXor(combined); 138 | 139 | Polygon polygon=PolyBool.polygon(eps,segments); 140 | ``` 141 | 142 | Depending on your needs, it might be more efficient to construct your own sequence of operations 143 | using the lower-level API. Note that `PolyBool.union`, `PolyBool.intersect`, etc, are just thin 144 | wrappers for convenience. 145 | 146 | There are three types of objects you will encounter in the core API: 147 | 148 | 1. Polygons (discussed above, this is a list of regions and an `inverted` flag) 149 | 2. Segments 150 | 3. Combined Segments 151 | 152 | The basic flow chart of the API is: 153 | 154 | ![PolyBool API Flow Chart](https://github.com/voidqk/polybooljs/raw/master/flowchart.png) 155 | 156 | You start by converting Polygons to Segments using `PolyBool.segments(eps, poly)`. 157 | 158 | You convert Segments to Combined Segments using `PolyBool.combine(eps, seg1, seg2)`. 159 | 160 | You select the resulting Segments from the Combined Segments using one of the selection operators 161 | `PolyBool.selectUnion(combined)`, `PolyBool.selectIntersect(combined)`, etc. These selection 162 | functions return Segments. 163 | 164 | Once you're done, you convert the Segments back to Polygons using `PolyBool.polygon(eps, segments)`. 165 | 166 | Each transition is costly, so you want to navigate wisely. The selection transition is the least 167 | costly. 168 | 169 | ## Advanced Example 1 170 | 171 | Suppose you wanted to union a list of polygons together. The naive way to do it would be: 172 | 173 | ```java 174 | // works but not efficient 175 | 176 | Polygon result = polygons[0]; 177 | for (int i = 1; i < polygons.length; i++) 178 | result = PolyBool.union(eps, result, polygons[i]); 179 | 180 | return result; 181 | ``` 182 | 183 | Instead, it's more efficient to use the core API directly, like this: 184 | 185 | ```java 186 | // works AND efficient 187 | Segments segments=PolyBool.segments(eps,polygons[0]); 188 | for(int i=1;i ops = new HashMap<>(); 203 | 204 | ops.put("union", PolyBool.union (eps, poly1, poly2)); 205 | ops.put("intersect", PolyBool.intersect (eps, poly1, poly2)); 206 | ops.put("difference", PolyBool.difference (eps, poly1, poly2)); 207 | ops.put("differenceRev", PolyBool.differenceRev(eps, poly1, poly2)); 208 | ops.put("xor", PolyBool.xor (eps, poly1, poly2)); 209 | 210 | return operations; 211 | ``` 212 | 213 | Instead, it's more efficient to use the core API directly, like this: 214 | 215 | ```java 216 | // works AND efficient 217 | Segments seg1 = PolyBool.segments(eps, poly1); 218 | Segments seg2 = PolyBool.segments(eps, poly2); 219 | Combined comb = PolyBool.combine(eps, seg1, seg2); 220 | 221 | Map ops= new HashMap<>(); 222 | 223 | ops.put("union", PolyBool.polygon(eps, PolyBool.selectUnion (eps, poly1, poly2))); 224 | ops.put("intersect", PolyBool.polygon(eps, PolyBool.selectIntersect (eps, poly1, poly2))); 225 | ops.put("difference", PolyBool.polygon(eps, PolyBool.selectDifference (eps, poly1, poly2))); 226 | ops.put("differenceRev", PolyBool.polygon(eps, PolyBool.selectDifferenceRev(eps, poly1, poly2))); 227 | ops.put("xor", PolyBool.polygon(eps, PolyBool.selectXor (eps, poly1, poly2))); 228 | 229 | return ops; 230 | ``` 231 | 232 | ## Advanced Example 3 233 | 234 | As an added bonus, just going from Polygon to Segments and back performs simplification on the 235 | polygon. 236 | 237 | Suppose you have garbage polygon data and just want to clean it up. The naive way to do it would 238 | be: 239 | 240 | ```java 241 | // union the polygon with nothing in order to clean up the data 242 | // works but not efficient 243 | Polygon cleaned=PolyBool.union(eps,polygon,new Polygon()); 244 | ``` 245 | 246 | Instead, skip the combination and selection phase: 247 | 248 | ```java 249 | // works AND efficient 250 | Polygon cleaned=PolyBool.polygon(eps,PolyBool.segments(eps,polygon)); 251 | ``` 252 | 253 | # Epsilon 254 | 255 | Due to the beauty of floating point reality, floating point calculations are not exactly perfect. 256 | This is a problem when trying to detect whether lines are on top of each other, or if vertices are 257 | exactly the same. 258 | 259 | Normally you would expect this to work: 260 | 261 | ```java 262 | if(A==B){ 263 | /* A and B are equal */; 264 | }else{ 265 | /* A and B are not equal */; 266 | } 267 | ``` 268 | 269 | But for inexact floating point math, instead we use: 270 | 271 | ```java 272 | if(Math.abs(A-B) \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'polybool-java' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/Epsilon.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool; 2 | 3 | import java.util.List; 4 | 5 | import static com.menecats.polybool.helpers.PolyBoolHelper.point; 6 | 7 | public class Epsilon { 8 | public static class EpsilonIntersectionResult { 9 | public int alongA; 10 | public int alongB; 11 | public double[] pt; 12 | } 13 | 14 | protected double eps; 15 | 16 | public Epsilon() { 17 | this(1e-10); 18 | } 19 | 20 | public Epsilon(double eps) { 21 | this.eps = eps; 22 | } 23 | 24 | public double epsilon(double eps) { 25 | return (this.eps = Math.abs(eps)); 26 | } 27 | 28 | public boolean pointAboveOrOnLine(double[] pt, double[] left, double[] right) { 29 | double Ax = left[0]; 30 | double Ay = left[1]; 31 | double Bx = right[0]; 32 | double By = right[1]; 33 | double Cx = pt[0]; 34 | double Cy = pt[1]; 35 | 36 | return (Bx - Ax) * (Cy - Ay) - (By - Ay) * (Cx - Ax) >= -this.eps; 37 | } 38 | 39 | public boolean pointBetween(double[] p, double[] left, double[] right) { 40 | // p must be collinear with left->right 41 | // returns false if p == left, p == right, or left == right 42 | double d_py_ly = p[1] - left[1]; 43 | double d_rx_lx = right[0] - left[0]; 44 | double d_px_lx = p[0] - left[0]; 45 | double d_ry_ly = right[1] - left[1]; 46 | 47 | double dot = d_px_lx * d_rx_lx + d_py_ly * d_ry_ly; 48 | // if `dot` is 0, then `p` == `left` or `left` == `right` (reject) 49 | // if `dot` is less than 0, then `p` is to the left of `left` (reject) 50 | if (dot < this.eps) 51 | return false; 52 | 53 | double sqlen = d_rx_lx * d_rx_lx + d_ry_ly * d_ry_ly; 54 | // if `dot` > `sqlen`, then `p` is to the right of `right` (reject) 55 | // therefore, if `dot - sqlen` is greater than 0, then `p` is to the right of `right` (reject) 56 | return !(dot - sqlen > -this.eps); 57 | } 58 | 59 | public boolean pointsSameX(double[] p1, double[] p2) { 60 | return Math.abs(p1[0] - p2[0]) < this.eps; 61 | } 62 | 63 | public boolean pointsSameY(double[] p1, double[] p2) { 64 | return Math.abs(p1[1] - p2[1]) < this.eps; 65 | } 66 | 67 | public boolean pointsSame(double[] p1, double[] p2) { 68 | return this.pointsSameX(p1, p2) && this.pointsSameY(p1, p2); 69 | } 70 | 71 | public int pointsCompare(double[] p1, double[] p2) { 72 | // returns -1 if p1 is smaller, 1 if p2 is smaller, 0 if equal 73 | if (this.pointsSameX(p1, p2)) 74 | return this.pointsSameY(p1, p2) ? 0 : (p1[1] < p2[1] ? -1 : 1); 75 | return p1[0] < p2[0] ? -1 : 1; 76 | } 77 | 78 | public boolean pointsCollinear(double[] pt1, double[] pt2, double[] pt3) { 79 | // does pt1->pt2->pt3 make a straight line? 80 | // essentially this is just checking to see if the slope(pt1->pt2) === slope(pt2->pt3) 81 | // if slopes are equal, then they must be collinear, because they share pt2 82 | double dx1 = pt1[0] - pt2[0]; 83 | double dy1 = pt1[1] - pt2[1]; 84 | double dx2 = pt2[0] - pt3[0]; 85 | double dy2 = pt2[1] - pt3[1]; 86 | return Math.abs(dx1 * dy2 - dx2 * dy1) < this.eps; 87 | } 88 | 89 | public EpsilonIntersectionResult linesIntersect(double[] a0, double[] a1, double[] b0, double[] b1) { 90 | // returns false if the lines are coincident (e.g., parallel or on top of each other) 91 | // 92 | // returns an object if the lines intersect: 93 | // { 94 | // pt: [x, y], where the intersection point is at 95 | // alongA: where intersection point is along A, 96 | // alongB: where intersection point is along B 97 | // } 98 | // 99 | // alongA and alongB will each be one of: -2, -1, 0, 1, 2 100 | // 101 | // with the following meaning: 102 | // 103 | // -2 intersection point is before segment's first point 104 | // -1 intersection point is directly on segment's first point 105 | // 0 intersection point is between segment's first and second points (exclusive) 106 | // 1 intersection point is directly on segment's second point 107 | // 2 intersection point is after segment's second point 108 | double adx = a1[0] - a0[0]; 109 | double ady = a1[1] - a0[1]; 110 | double bdx = b1[0] - b0[0]; 111 | double bdy = b1[1] - b0[1]; 112 | 113 | double axb = adx * bdy - ady * bdx; 114 | if (Math.abs(axb) < this.eps) 115 | return null; // lines are coincident 116 | 117 | double dx = a0[0] - b0[0]; 118 | double dy = a0[1] - b0[1]; 119 | 120 | double A = (bdx * dy - bdy * dx) / axb; 121 | double B = (adx * dy - ady * dx) / axb; 122 | 123 | EpsilonIntersectionResult ret = new EpsilonIntersectionResult(); 124 | ret.pt = point( 125 | a0[0] + A * adx, 126 | a0[1] + A * ady 127 | ); 128 | 129 | // categorize where intersection point is along A and B 130 | 131 | if (A <= -this.eps) 132 | ret.alongA = -2; 133 | else if (A < this.eps) 134 | ret.alongA = -1; 135 | else if (A - 1 <= -this.eps) 136 | ret.alongA = 0; 137 | else if (A - 1 < this.eps) 138 | ret.alongA = 1; 139 | else 140 | ret.alongA = 2; 141 | 142 | if (B <= -this.eps) 143 | ret.alongB = -2; 144 | else if (B < this.eps) 145 | ret.alongB = -1; 146 | else if (B - 1 <= -this.eps) 147 | ret.alongB = 0; 148 | else if (B - 1 < this.eps) 149 | ret.alongB = 1; 150 | else 151 | ret.alongB = 2; 152 | 153 | return ret; 154 | } 155 | 156 | public boolean pointInsideRegion(double[] pt, List region) { 157 | double x = pt[0]; 158 | double y = pt[1]; 159 | double last_x = region.get(region.size() - 1)[0]; 160 | double last_y = region.get(region.size() - 1)[1]; 161 | boolean inside = false; 162 | for (double[] regionPt : region) { 163 | double curr_x = regionPt[0]; 164 | double curr_y = regionPt[1]; 165 | 166 | // if y is between curr_y and last_y, and 167 | // x is to the right of the boundary created by the line 168 | if ((curr_y - y > this.eps) != (last_y - y > this.eps) && (last_x - curr_x) * (y - curr_y) / (last_y - curr_y) + curr_x - x > this.eps) 169 | inside = !inside; 170 | 171 | last_x = curr_x; 172 | last_y = curr_y; 173 | } 174 | return inside; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/ExperimentalEpsilon.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool; 2 | 3 | import static com.menecats.polybool.helpers.PolyBoolHelper.point; 4 | 5 | public class ExperimentalEpsilon extends Epsilon { 6 | public ExperimentalEpsilon() { 7 | super(); 8 | } 9 | 10 | public ExperimentalEpsilon(double eps) { 11 | super(eps); 12 | } 13 | 14 | @Override 15 | public boolean pointAboveOrOnLine(double[] pt, double[] left, double[] right) { 16 | final double Ax = left[0]; 17 | final double Ay = left[1]; 18 | final double Bx = right[0]; 19 | final double By = right[1]; 20 | final double Cx = pt[0]; 21 | final double Cy = pt[1]; 22 | final double ABx = Bx - Ax; 23 | final double ABy = By - Ay; 24 | final double AB = Math.sqrt(ABx * ABx + ABy * ABy); 25 | // algebraic distance of 'pt' to ('left', 'right') line is: 26 | // [ABx * (Cy - Ay) - ABy * (Cx - Ax)] / AB 27 | return ABx * (Cy - Ay) - ABy * (Cx - Ax) >= -eps * AB; 28 | } 29 | 30 | @Override 31 | public boolean pointBetween(double[] p, double[] left, double[] right) { 32 | // p must be collinear with left->right 33 | // returns false if p == left, p == right, or left == right 34 | if (pointsSame(p, left) || pointsSame(p, right)) return false; 35 | final double d_py_ly = p[1] - left[1]; 36 | final double d_rx_lx = right[0] - left[0]; 37 | final double d_px_lx = p[0] - left[0]; 38 | final double d_ry_ly = right[1] - left[1]; 39 | 40 | double dot = d_px_lx * d_rx_lx + d_py_ly * d_ry_ly; 41 | // dot < 0 is p is to the left of 'left' 42 | if (dot < 0) return false; 43 | final double sqlen = d_rx_lx * d_rx_lx + d_ry_ly * d_ry_ly; 44 | // dot <= sqlen is p is to the left of 'right' 45 | return dot <= sqlen; 46 | } 47 | 48 | @Override 49 | public boolean pointsCollinear(double[] pt1, double[] pt2, double[] pt3) { 50 | // does pt1->pt2->pt3 make a straight line? 51 | // essentially this is just checking to see if the slope(pt1->pt2) === slope(pt2->pt3) 52 | // if slopes are equal, then they must be collinear, because they share pt2 53 | final double dx1 = pt1[0] - pt2[0]; 54 | final double dy1 = pt1[1] - pt2[1]; 55 | final double dx2 = pt2[0] - pt3[0]; 56 | final double dy2 = pt2[1] - pt3[1]; 57 | final double n1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); 58 | final double n2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); 59 | // Assuming det(u, v) = 0, we have: 60 | // |det(u + u_err, v + v_err)| = |det(u + u_err, v + v_err) - det(u,v)| 61 | // =|det(u, v_err) + det(u_err. v) + det(u_err, v_err)| 62 | // <= |det(u, v_err)| + |det(u_err, v)| + |det(u_err, v_err)| 63 | // <= N(u)N(v_err) + N(u_err)N(v) + N(u_err)N(v_err) 64 | // <= eps * (N(u) + N(v) + eps) 65 | // We have N(u) ~ N(u + u_err) and N(v) ~ N(v + v_err). 66 | // Assuming eps << N(u) and eps << N(v), we end with: 67 | // |det(u + u_err, v + v_err)| <= eps * (N(u + u_err) + N(v + v_err)) 68 | return Math.abs(dx1 * dy2 - dx2 * dy1) <= eps * (n1 + n2); 69 | } 70 | 71 | @Override 72 | public EpsilonIntersectionResult linesIntersect(double[] a0, double[] a1, double[] b0, double[] b1) { 73 | // returns false if the lines are coincident (e.g., parallel or on top of each other) 74 | // 75 | // returns an object if the lines intersect: 76 | // { 77 | // pt: [x, y], where the intersection point is at 78 | // alongA: where intersection point is along A, 79 | // alongB: where intersection point is along B 80 | // } 81 | // 82 | // alongA and alongB will each be one of: -2, -1, 0, 1, 2 83 | // 84 | // with the following meaning: 85 | // 86 | // -2 intersection point is before segment's first point 87 | // -1 intersection point is directly on segment's first point 88 | // 0 intersection point is between segment's first and second points (exclusive) 89 | // 1 intersection point is directly on segment's second point 90 | // 2 intersection point is after segment's second point 91 | final double adx = a1[0] - a0[0]; 92 | final double ady = a1[1] - a0[1]; 93 | final double bdx = b1[0] - b0[0]; 94 | final double bdy = b1[1] - b0[1]; 95 | 96 | final double axb = adx * bdy - ady * bdx; 97 | final double n1 = Math.sqrt(adx * adx + ady * ady); 98 | final double n2 = Math.sqrt(bdx * bdx + bdy * bdy); 99 | if (Math.abs(axb) <= eps * (n1 + n2)) 100 | return null; // lines are coincident 101 | 102 | final double dx = a0[0] - b0[0]; 103 | final double dy = a0[1] - b0[1]; 104 | 105 | final double A = (bdx * dy - bdy * dx) / axb; 106 | final double B = (adx * dy - ady * dx) / axb; 107 | final double[] pt = point( 108 | a0[0] + A * adx, 109 | a0[1] + A * ady 110 | ); 111 | 112 | final EpsilonIntersectionResult ret = new EpsilonIntersectionResult(); 113 | ret.pt = pt; 114 | 115 | // categorize where intersection point is along A and B 116 | if (pointsSame(pt, a0)) 117 | ret.alongA = -1; 118 | else if (pointsSame(pt, a1)) 119 | ret.alongA = 1; 120 | else if (A < 0) 121 | ret.alongA = -2; 122 | else if (A > 1) 123 | ret.alongA = 2; 124 | 125 | if (pointsSame(pt, b0)) 126 | ret.alongB = -1; 127 | else if (pointsSame(pt, b1)) 128 | ret.alongB = 1; 129 | else if (B < 0) 130 | ret.alongB = -2; 131 | else if (B > 1) 132 | ret.alongB = 2; 133 | 134 | return ret; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/PolyBool.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool; 2 | /* 3 | * @copyright 2016 Sean Connelly (@voidqk), http://syntheti.cc 4 | * @license MIT 5 | * @preserve Project Home: https://github.com/voidqk/polybooljs 6 | */ 7 | 8 | import com.menecats.polybool.internal.*; 9 | import com.menecats.polybool.models.Polygon; 10 | import com.menecats.polybool.models.Segment; 11 | import com.menecats.polybool.models.geojson.Geometry; 12 | 13 | import java.util.List; 14 | import java.util.function.Function; 15 | 16 | public final class PolyBool { 17 | public static final class Segments { 18 | private final List segments; 19 | private final boolean inverted; 20 | 21 | private Segments(List segments, boolean inverted) { 22 | this.segments = segments; 23 | this.inverted = inverted; 24 | } 25 | } 26 | 27 | public static final class Combined { 28 | private final List combined; 29 | private final boolean inverted1; 30 | private final boolean inverted2; 31 | 32 | private Combined(List combined, boolean inverted1, boolean inverted2) { 33 | this.combined = combined; 34 | this.inverted1 = inverted1; 35 | this.inverted2 = inverted2; 36 | } 37 | } 38 | 39 | // Core API 40 | public static Segments segments(Epsilon epsilon, Polygon polygon) { 41 | SelfIntersecter i = new SelfIntersecter(epsilon); 42 | 43 | for (List region : polygon.getRegions()) { 44 | i.addRegion(region); 45 | } 46 | 47 | return new Segments( 48 | i.calculate(polygon.isInverted()), 49 | polygon.isInverted() 50 | ); 51 | } 52 | 53 | public static Combined combine(Epsilon epsilon, Segments segments1, Segments segments2) { 54 | NonSelfIntersecter i3 = new NonSelfIntersecter(epsilon); 55 | 56 | return new Combined( 57 | i3.calculate( 58 | segments1.segments, segments1.inverted, 59 | segments2.segments, segments2.inverted 60 | ), 61 | segments1.inverted, 62 | segments2.inverted 63 | ); 64 | } 65 | 66 | public static Segments selectUnion(Combined combined) { 67 | return new Segments( 68 | SegmentSelector.union(combined.combined), 69 | combined.inverted1 || combined.inverted2 70 | ); 71 | } 72 | 73 | public static Segments selectIntersect(Combined combined) { 74 | return new Segments( 75 | SegmentSelector.intersect(combined.combined), 76 | combined.inverted1 && combined.inverted2 77 | ); 78 | } 79 | 80 | public static Segments selectDifference(Combined combined) { 81 | return new Segments( 82 | SegmentSelector.difference(combined.combined), 83 | combined.inverted1 && !combined.inverted2 84 | ); 85 | } 86 | 87 | public static Segments selectDifferenceRev(Combined combined) { 88 | return new Segments( 89 | SegmentSelector.differenceRev(combined.combined), 90 | !combined.inverted1 && combined.inverted2 91 | ); 92 | } 93 | 94 | public static Segments selectXor(Combined combined) { 95 | return new Segments( 96 | SegmentSelector.xor(combined.combined), 97 | combined.inverted1 != combined.inverted2 98 | ); 99 | } 100 | 101 | public static Polygon polygon(Epsilon epsilon, Segments segments) { 102 | return new Polygon( 103 | SegmentChainer.chain(segments.segments, epsilon), 104 | segments.inverted 105 | ); 106 | } 107 | 108 | // Public API 109 | private static Polygon operate(Epsilon epsilon, Polygon poly1, Polygon poly2, Function selector) { 110 | Segments seg1 = segments(epsilon, poly1); 111 | Segments seg2 = segments(epsilon, poly2); 112 | Combined comb = combine(epsilon, seg1, seg2); 113 | Segments seg3 = selector.apply(comb); 114 | return polygon(epsilon, seg3); 115 | } 116 | 117 | public static Polygon union(Epsilon epsilon, Polygon poly1, Polygon poly2) { 118 | return operate(epsilon, poly1, poly2, PolyBool::selectUnion); 119 | } 120 | 121 | public static Polygon intersect(Epsilon epsilon, Polygon poly1, Polygon poly2) { 122 | return operate(epsilon, poly1, poly2, PolyBool::selectIntersect); 123 | } 124 | 125 | public static Polygon difference(Epsilon epsilon, Polygon poly1, Polygon poly2) { 126 | return operate(epsilon, poly1, poly2, PolyBool::selectDifference); 127 | } 128 | 129 | public static Polygon differenceRev(Epsilon epsilon, Polygon poly1, Polygon poly2) { 130 | return operate(epsilon, poly1, poly2, PolyBool::selectDifferenceRev); 131 | } 132 | 133 | public static Polygon xor(Epsilon epsilon, Polygon poly1, Polygon poly2) { 134 | return operate(epsilon, poly1, poly2, PolyBool::selectXor); 135 | } 136 | 137 | // Import export 138 | // GeoJSON converters 139 | public static Polygon polygonFromGeoJSON(Epsilon epsilon, Geometry geojson) { 140 | return GeoJSON.toPolygon(epsilon, geojson); 141 | } 142 | 143 | public static Geometry polygonToGeoJSON(Epsilon epsilon, Polygon poly) { 144 | return GeoJSON.fromPolygon(epsilon, polygon(epsilon, segments(epsilon, poly))); 145 | } 146 | 147 | private PolyBool() { 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/helpers/PolyBoolHelper.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.helpers; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.ExperimentalEpsilon; 5 | import com.menecats.polybool.models.Polygon; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | public final class PolyBoolHelper { 11 | public static Epsilon epsilon() { 12 | return epsilon(false); 13 | } 14 | 15 | public static Epsilon epsilon(boolean experimental) { 16 | return experimental 17 | ? new ExperimentalEpsilon() 18 | : new Epsilon(); 19 | } 20 | 21 | public static Epsilon epsilon(double epsilon) { 22 | return epsilon(epsilon, false); 23 | } 24 | 25 | public static Epsilon epsilon(double epsilon, boolean experimental) { 26 | return experimental 27 | ? new ExperimentalEpsilon(epsilon) 28 | : new Epsilon(epsilon); 29 | } 30 | 31 | public static double[] point(double x, double y) { 32 | return new double[]{x, y}; 33 | } 34 | 35 | public static List region(double[]... points) { 36 | return Arrays.asList(points); 37 | } 38 | 39 | @SafeVarargs 40 | public static Polygon polygon(List... regions) { 41 | return polygon(false, regions); 42 | } 43 | 44 | @SafeVarargs 45 | public static Polygon polygon(boolean inverted, List... regions) { 46 | return new Polygon(Arrays.asList(regions), inverted); 47 | } 48 | 49 | private PolyBoolHelper() { 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/AbstractIntersecter.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.models.Segment; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | 12 | public abstract class AbstractIntersecter { 13 | protected static class IntersecterContent { 14 | boolean isStart; 15 | double[] pt; 16 | Segment seg; 17 | boolean primary; 18 | LinkedList other; 19 | LinkedList> status; 20 | } 21 | 22 | protected final Epsilon eps; 23 | 24 | private final boolean selfIntersection; 25 | private final LinkedList event_root = LinkedList.create(); 26 | 27 | AbstractIntersecter(boolean selfIntersection, Epsilon eps) { 28 | this.eps = eps; 29 | this.selfIntersection = selfIntersection; 30 | } 31 | 32 | protected Segment segmentNew(double[] start, double[] end) { 33 | return new Segment(start, end); 34 | } 35 | 36 | protected Segment segmentCopy(double[] start, double[] end, Segment seg) { 37 | return new Segment(start, end, new Segment.SegmentFill(seg.myFill.above, seg.myFill.below)); 38 | } 39 | 40 | 41 | private int eventCompare(boolean p1_isStart, double[] p1_1, double[] p1_2, 42 | boolean p2_isStart, double[] p2_1, double[] p2_2) { 43 | 44 | // compare the selected points first 45 | int comp = this.eps.pointsCompare(p1_1, p2_1); 46 | if (comp != 0) 47 | return comp; 48 | // the selected points are the same 49 | 50 | if (this.eps.pointsSame(p1_2, p2_2)) // if the non-selected points are the same too... 51 | return 0; // then the segments are equal 52 | 53 | if (p1_isStart != p2_isStart) // if one is a start and the other isn't... 54 | return p1_isStart ? 1 : -1; // favor the one that isn't the start 55 | 56 | // otherwise, we'll have to calculate which one is below the other manually 57 | return this.eps.pointAboveOrOnLine(p1_2, 58 | p2_isStart ? p2_1 : p2_2, // order matters 59 | p2_isStart ? p2_2 : p2_1 60 | ) ? 1 : -1; 61 | } 62 | 63 | private void eventAdd(LinkedList ev, double[] other_pt) { 64 | this.event_root.insertBefore(ev, (here) -> { 65 | // should ev be inserted before here? 66 | int comp = this.eventCompare( 67 | ev.getContent().isStart, ev.getContent().pt, other_pt, 68 | here.getContent().isStart, here.getContent().pt, here.getContent().other.getContent().pt 69 | ); 70 | return comp < 0; 71 | }); 72 | } 73 | 74 | private LinkedList eventAddSegmentStart(Segment seg, boolean primary) { 75 | IntersecterContent content = new IntersecterContent(); 76 | content.isStart = true; 77 | content.pt = seg.start; 78 | content.seg = seg; 79 | content.primary = primary; 80 | 81 | LinkedList ev_start = LinkedList.node(content); 82 | this.eventAdd(ev_start, seg.end); 83 | return ev_start; 84 | } 85 | 86 | private void eventAddSegmentEnd(LinkedList ev_start, Segment seg, boolean primary) { 87 | IntersecterContent content = new IntersecterContent(); 88 | content.isStart = false; 89 | content.pt = seg.end; 90 | content.seg = seg; 91 | content.primary = primary; 92 | content.other = ev_start; 93 | 94 | LinkedList ev_end = LinkedList.node(content); 95 | ev_start.getContent().other = ev_end; 96 | this.eventAdd(ev_end, ev_start.getContent().pt); 97 | } 98 | 99 | protected LinkedList eventAddSegment(Segment seg, boolean primary) { 100 | LinkedList ev_start = this.eventAddSegmentStart(seg, primary); 101 | this.eventAddSegmentEnd(ev_start, seg, primary); 102 | return ev_start; 103 | } 104 | 105 | private void eventUpdateEnd(LinkedList ev, double[] end) { 106 | // slides an end backwards 107 | // (start)------------(end) to: 108 | // (start)---(end) 109 | 110 | ev.getContent().other.remove(); 111 | ev.getContent().seg.end = end; 112 | ev.getContent().other.getContent().pt = end; 113 | this.eventAdd(ev.getContent().other, ev.getContent().pt); 114 | } 115 | 116 | private LinkedList eventDivide(LinkedList ev, double[] pt) { 117 | Segment ns = this.segmentCopy(pt, ev.getContent().seg.end, ev.getContent().seg); 118 | this.eventUpdateEnd(ev, pt); 119 | return this.eventAddSegment(ns, ev.getContent().primary); 120 | } 121 | 122 | protected List baseCalculate(boolean primaryPolyInverted, boolean secondaryPolyInverted) { 123 | // if selfIntersection is true then there is no secondary polygon, so that isn't used 124 | 125 | // 126 | // status logic 127 | // 128 | 129 | LinkedList> status_root = LinkedList.create(); 130 | 131 | BiFunction, LinkedList, Integer> statusCompare = (ev1, ev2) -> { 132 | double[] a1 = ev1.getContent().seg.start; 133 | double[] a2 = ev1.getContent().seg.end; 134 | double[] b1 = ev2.getContent().seg.start; 135 | double[] b2 = ev2.getContent().seg.end; 136 | 137 | if (this.eps.pointsCollinear(a1, b1, b2)) { 138 | if (this.eps.pointsCollinear(a2, b1, b2)) 139 | return 1;//eventCompare(true, a1, a2, true, b1, b2); 140 | return this.eps.pointAboveOrOnLine(a2, b1, b2) ? 1 : -1; 141 | } 142 | return this.eps.pointAboveOrOnLine(a1, b1, b2) ? 1 : -1; 143 | }; 144 | 145 | final Function, LinkedList.TransitionResult>> statusFindSurrounding = (ev) -> status_root 146 | .findTransition((here) -> { 147 | int comp = statusCompare.apply(ev, here.getContent()); 148 | return comp > 0; 149 | }); 150 | 151 | final BiFunction, LinkedList, LinkedList> checkIntersection = (ev1, ev2) -> { 152 | // returns the segment equal to ev1, or false if nothing equal 153 | 154 | final Segment seg1 = ev1.getContent().seg; 155 | final Segment seg2 = ev2.getContent().seg; 156 | final double[] a1 = seg1.start; 157 | final double[] a2 = seg1.end; 158 | final double[] b1 = seg2.start; 159 | final double[] b2 = seg2.end; 160 | 161 | final Epsilon.EpsilonIntersectionResult i = this.eps.linesIntersect(a1, a2, b1, b2); 162 | 163 | if (i == null) { 164 | // segments are parallel or coincident 165 | 166 | // if points aren't collinear, then the segments are parallel, so no intersections 167 | if (!this.eps.pointsCollinear(a1, a2, b1)) 168 | return null; 169 | // otherwise, segments are on top of each other somehow (aka coincident) 170 | 171 | if (this.eps.pointsSame(a1, b2) || this.eps.pointsSame(a2, b1)) 172 | return null; // segments touch at endpoints... no intersection 173 | 174 | final boolean a1_equ_b1 = this.eps.pointsSame(a1, b1); 175 | final boolean a2_equ_b2 = this.eps.pointsSame(a2, b2); 176 | 177 | if (a1_equ_b1 && a2_equ_b2) 178 | return ev2; // segments are exactly equal 179 | 180 | final boolean a1_between = !a1_equ_b1 && this.eps.pointBetween(a1, b1, b2); 181 | final boolean a2_between = !a2_equ_b2 && this.eps.pointBetween(a2, b1, b2); 182 | 183 | // handy for debugging: 184 | // buildLog.log({ 185 | // a1_equ_b1: a1_equ_b1, 186 | // a2_equ_b2: a2_equ_b2, 187 | // a1_between: a1_between, 188 | // a2_between: a2_between 189 | // }); 190 | 191 | if (a1_equ_b1) { 192 | if (a2_between) { 193 | // (a1)---(a2) 194 | // (b1)----------(b2) 195 | this.eventDivide(ev2, a2); 196 | } else { 197 | // (a1)----------(a2) 198 | // (b1)---(b2) 199 | this.eventDivide(ev1, b2); 200 | } 201 | return ev2; 202 | } else if (a1_between) { 203 | if (!a2_equ_b2) { 204 | // make a2 equal to b2 205 | if (a2_between) { 206 | // (a1)---(a2) 207 | // (b1)-----------------(b2) 208 | this.eventDivide(ev2, a2); 209 | } else { 210 | // (a1)----------(a2) 211 | // (b1)----------(b2) 212 | this.eventDivide(ev1, b2); 213 | } 214 | } 215 | 216 | // (a1)---(a2) 217 | // (b1)----------(b2) 218 | this.eventDivide(ev2, a1); 219 | } 220 | } else { 221 | // otherwise, lines intersect at i.pt, which may or may not be between the endpoints 222 | 223 | // is A divided between its endpoints? (exclusive) 224 | if (i.alongA == 0) { 225 | if (i.alongB == -1) // yes, at exactly b1 226 | this.eventDivide(ev1, b1); 227 | else if (i.alongB == 0) // yes, somewhere between B's endpoints 228 | this.eventDivide(ev1, i.pt); 229 | else if (i.alongB == 1) // yes, at exactly b2 230 | this.eventDivide(ev1, b2); 231 | } 232 | 233 | // is B divided between its endpoints? (exclusive) 234 | if (i.alongB == 0) { 235 | if (i.alongA == -1) // yes, at exactly a1 236 | this.eventDivide(ev2, a1); 237 | else if (i.alongA == 0) // yes, somewhere between A's endpoints (exclusive) 238 | this.eventDivide(ev2, i.pt); 239 | else if (i.alongA == 1) // yes, at exactly a2 240 | this.eventDivide(ev2, a2); 241 | } 242 | } 243 | return null; 244 | }; 245 | 246 | // 247 | // main event loop 248 | // 249 | List segments = new ArrayList<>(); 250 | while (!this.event_root.isEmpty()) { 251 | LinkedList ev = this.event_root.getHead(); 252 | 253 | if (ev.getContent().isStart) { 254 | LinkedList.TransitionResult> surrounding = statusFindSurrounding.apply(ev); 255 | LinkedList above = surrounding.before != null ? surrounding.before.getContent() : null; 256 | LinkedList below = surrounding.after != null ? surrounding.after.getContent() : null; 257 | 258 | Supplier> checkBothIntersections = () -> { 259 | if (above != null) { 260 | LinkedList eve = checkIntersection.apply(ev, above); 261 | if (eve != null) 262 | return eve; 263 | } 264 | if (below != null) 265 | return checkIntersection.apply(ev, below); 266 | return null; 267 | }; 268 | 269 | LinkedList eve = checkBothIntersections.get(); 270 | if (eve != null) { 271 | // ev and eve are equal 272 | // we'll keep eve and throw away ev 273 | 274 | // merge ev.seg's fill information into eve.seg 275 | 276 | if (this.selfIntersection) { 277 | boolean toggle; // are we a toggling edge? 278 | if (ev.getContent().seg.myFill.below == null) 279 | toggle = true; 280 | else 281 | toggle = ev.getContent().seg.myFill.above != ev.getContent().seg.myFill.below; 282 | 283 | // merge two segments that belong to the same polygon 284 | // think of this as sandwiching two segments together, where `eve.seg` is 285 | // the bottom -- this will cause the above fill flag to toggle 286 | if (toggle) 287 | eve.getContent().seg.myFill.above = !eve.getContent().seg.myFill.above; 288 | } else { 289 | // merge two segments that belong to different polygons 290 | // each segment has distinct knowledge, so no special logic is needed 291 | // note that this can only happen once per segment in this phase, because we 292 | // are guaranteed that all self-intersections are gone 293 | eve.getContent().seg.otherFill = ev.getContent().seg.myFill; 294 | } 295 | 296 | ev.getContent().other.remove(); 297 | ev.remove(); 298 | } 299 | 300 | if (this.event_root.getHead() != ev) { 301 | // something was inserted before us in the event queue, so loop back around and 302 | // process it before continuing 303 | continue; 304 | } 305 | 306 | // 307 | // calculate fill flags 308 | // 309 | if (this.selfIntersection) { 310 | boolean toggle; // are we a toggling edge? 311 | if (ev.getContent().seg.myFill.below == null) // if we are a new segment... 312 | toggle = true; // then we toggle 313 | else // we are a segment that has previous knowledge from a division 314 | toggle = ev.getContent().seg.myFill.above != ev.getContent().seg.myFill.below; // calculate toggle 315 | 316 | // next, calculate whether we are filled below us 317 | if (below == null) { // if nothing is below us... 318 | // we are filled below us if the polygon is inverted 319 | ev.getContent().seg.myFill.below = primaryPolyInverted; 320 | } else { 321 | // otherwise, we know the answer -- it's the same if whatever is below 322 | // us is filled above it 323 | ev.getContent().seg.myFill.below = below.getContent().seg.myFill.above; 324 | } 325 | 326 | // since now we know if we're filled below us, we can calculate whether 327 | // we're filled above us by applying toggle to whatever is below us 328 | if (toggle) 329 | ev.getContent().seg.myFill.above = !ev.getContent().seg.myFill.below; 330 | else 331 | ev.getContent().seg.myFill.above = ev.getContent().seg.myFill.below; 332 | } else { 333 | // now we fill in any missing transition information, since we are all-knowing 334 | // at this point 335 | 336 | if (ev.getContent().seg.otherFill == null) { 337 | // if we don't have other information, then we need to figure out if we're 338 | // inside the other polygon 339 | boolean inside; 340 | if (below == null) { 341 | // if nothing is below us, then we're inside if the other polygon is 342 | // inverted 343 | inside = ev.getContent().primary 344 | ? secondaryPolyInverted 345 | : primaryPolyInverted; 346 | } else { // otherwise, something is below us 347 | // so copy the below segment's other polygon's above 348 | if (ev.getContent().primary == below.getContent().primary) 349 | inside = below.getContent().seg.otherFill.above; 350 | else 351 | inside = below.getContent().seg.myFill.above; 352 | } 353 | ev.getContent().seg.otherFill = new Segment.SegmentFill(inside, inside); 354 | } 355 | } 356 | 357 | // insert the status and remember it for later removal 358 | ev.getContent().other.getContent().status = surrounding.insert.apply(LinkedList.node(ev)); 359 | } else { 360 | LinkedList> st = ev.getContent().status; 361 | 362 | if (st == null) { 363 | throw new RuntimeException("PolyBool: Zero-length segment detected; your epsilon is probably too small or too large"); 364 | } 365 | 366 | // removing the status will create two new adjacent edges, so we'll need to check 367 | // for those 368 | if (status_root.exists(st.getPrev()) && status_root.exists(st.getNext())) 369 | checkIntersection.apply(st.getPrev().getContent(), st.getNext().getContent()); 370 | 371 | // remove the status 372 | st.remove(); 373 | 374 | // if we've reached this point, we've calculated everything there is to know, so 375 | // save the segment for reporting 376 | if (!ev.getContent().primary) { 377 | // make sure `seg.myFill` actually points to the primary polygon though 378 | Segment.SegmentFill s = ev.getContent().seg.myFill; 379 | ev.getContent().seg.myFill = ev.getContent().seg.otherFill; 380 | ev.getContent().seg.otherFill = s; 381 | } 382 | segments.add(ev.getContent().seg); 383 | } 384 | 385 | // remove the event and continue 386 | this.event_root.getHead().remove(); 387 | } 388 | 389 | return segments; 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/GeoJSON.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.models.Polygon; 5 | import com.menecats.polybool.models.geojson.Geometry; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.function.Function; 11 | 12 | import static com.menecats.polybool.PolyBool.*; 13 | import static com.menecats.polybool.helpers.PolyBoolHelper.point; 14 | 15 | public final class GeoJSON { 16 | private static class Node { 17 | private final List region; 18 | private final List children; 19 | 20 | private Node(List region) { 21 | this.region = region; 22 | this.children = new ArrayList<>(); 23 | } 24 | } 25 | 26 | @SuppressWarnings("unchecked") 27 | public static Polygon toPolygon(final Epsilon epsilon, 28 | final Geometry geojson) { 29 | 30 | final Function>, Segments> geoPoly = (coords) -> { 31 | // check for empty coords 32 | if (coords.isEmpty()) { 33 | return segments(epsilon, new Polygon()); 34 | } 35 | 36 | final Function, Segments> lineString = ls -> { 37 | List region = new ArrayList<>(ls); 38 | 39 | region.remove(region.size() - 1); 40 | 41 | return segments(epsilon, new Polygon(Collections.singletonList(region))); 42 | }; 43 | 44 | Segments out = lineString.apply(coords.get(0)); 45 | 46 | for (int i = 1; i < coords.size(); ++i) { 47 | out = selectDifference(combine(epsilon, out, lineString.apply(coords.get(i)))); 48 | } 49 | 50 | return out; 51 | }; 52 | 53 | if ("Polygon".equals(geojson.getType())) { 54 | final List> coordinates = (List>) geojson.getCoordinates(); 55 | 56 | return polygon(epsilon, geoPoly.apply(coordinates)); 57 | } 58 | 59 | if ("MultiPolygon".equals(geojson.getType())) { 60 | final List>> coordinates = (List>>) geojson.getCoordinates(); 61 | 62 | Segments out = segments(epsilon, new Polygon()); 63 | for (List> coordinate : coordinates) { 64 | out = selectUnion(combine(epsilon, out, geoPoly.apply(coordinate))); 65 | } 66 | return polygon(epsilon, out); 67 | } 68 | 69 | throw new IllegalArgumentException("PolyBool: Cannot convert GeoJSON object to PolyBool polygon"); 70 | } 71 | 72 | public static Geometry fromPolygon(final Epsilon epsilon, 73 | Polygon poly) { 74 | 75 | // make sure out polygon is clean 76 | poly = polygon(epsilon, segments(epsilon, poly)); 77 | 78 | // calculate inside heirarchy 79 | // 80 | // _____________________ _______ roots -> A -> F 81 | // | A | | F | | | 82 | // | _______ _______ | | ___ | +-- B +-- G 83 | // | | B | | C | | | | | | | | 84 | // | | ___ | | ___ | | | | | | | +-- D 85 | // | | | D | | | | E | | | | | G | | | 86 | // | | |___| | | |___| | | | | | | +-- C 87 | // | |_______| |_______| | | |___| | | 88 | // |_____________________| |_______| +-- E 89 | 90 | 91 | final Node roots = new Node(null); 92 | 93 | // add all regions to the root 94 | for (int i = 0; i < poly.getRegions().size(); i++) { 95 | List region = poly.getRegions().get(i); 96 | if (region.size() < 3) // regions must have at least 3 points (sanity check) 97 | continue; 98 | fromPolygon_addChild(epsilon, roots, region); 99 | } 100 | 101 | final List>> geopolys = new ArrayList<>(); 102 | 103 | // root nodes are exterior 104 | for (int i = 0; i < roots.children.size(); i++) 105 | fromPolygon_addExterior(geopolys, roots.children.get(i)); 106 | 107 | // lastly, construct the approrpriate GeoJSON object 108 | 109 | if (geopolys.isEmpty()) // empty GeoJSON Polygon 110 | return new Geometry.PolygonGeometry(); 111 | if (geopolys.size() == 1) // use a GeoJSON Polygon 112 | return new Geometry.PolygonGeometry(geopolys.get(0)); 113 | 114 | // otherwise, use a GeoJSON MultiPolygon 115 | return new Geometry.MultiPolygonGeometry(geopolys); 116 | } 117 | 118 | // test if r1 is inside r2 119 | private static boolean fromPolygon_regionInsideRegion(final Epsilon epsilon, 120 | final List r1, 121 | final List r2) { 122 | 123 | // we're guaranteed no lines intersect (because the polygon is clean), but a vertex 124 | // could be on the edge -- so we just average pt[0] and pt[1] to produce a point on the 125 | // edge of the first line, which cannot be on an edge 126 | return epsilon.pointInsideRegion(point( 127 | (r1.get(0)[0] + r1.get(1)[0]) * 0.5, 128 | (r1.get(0)[1] + r1.get(1)[1]) * 0.5 129 | ), r2); 130 | } 131 | 132 | private static void fromPolygon_addChild(final Epsilon epsilon, 133 | final Node root, 134 | final List region) { 135 | 136 | // first check if we're inside any children 137 | for (int i = 0; i < root.children.size(); i++) { 138 | final Node child = root.children.get(i); 139 | if (fromPolygon_regionInsideRegion(epsilon, region, child.region)) { 140 | // we are, so insert inside them instead 141 | fromPolygon_addChild(epsilon, child, region); 142 | return; 143 | } 144 | } 145 | 146 | // not inside any children, so check to see if any children are inside us 147 | final Node node = new Node(region); 148 | for (int i = 0; i < root.children.size(); i++) { 149 | final Node child = root.children.get(i); 150 | if (fromPolygon_regionInsideRegion(epsilon, child.region, region)) { 151 | // oops... move the child beneath us, and remove them from root 152 | node.children.add(child); 153 | root.children.remove(i); 154 | i--; 155 | } 156 | } 157 | 158 | // now we can add ourselves 159 | root.children.add(node); 160 | } 161 | 162 | // with our heirarchy, we can distinguish between exterior borders, and interior holes 163 | // the root nodes are exterior, children are interior, children's children are exterior, 164 | // children's children's children are interior, etc 165 | 166 | // while we're at it, exteriors are counter-clockwise, and interiors are clockwise 167 | private static List fromPolygon_forceWinding(final List region, 168 | final boolean clockwise) { 169 | 170 | // first, see if we're clockwise or counter-clockwise 171 | // https://en.wikipedia.org/wiki/Shoelace_formula 172 | int winding = 0; 173 | double last_x = region.get(region.size() - 1)[0]; 174 | double last_y = region.get(region.size() - 1)[1]; 175 | final List copy = new ArrayList<>(); 176 | for (double[] point : region) { 177 | double curr_x = point[0]; 178 | double curr_y = point[1]; 179 | copy.add(point(curr_x, curr_y)); // create a copy while we're at it 180 | winding += curr_y * last_x - curr_x * last_y; 181 | last_x = curr_x; 182 | last_y = curr_y; 183 | } 184 | // this assumes Cartesian coordinates (Y is positive going up) 185 | boolean isclockwise = winding < 0; 186 | if (isclockwise != clockwise) 187 | Collections.reverse(copy); 188 | 189 | // while we're here, the last point must be the first point... 190 | copy.add(point(copy.get(0)[0], copy.get(0)[1])); 191 | return copy; 192 | } 193 | 194 | private static void fromPolygon_addExterior(final List>> geopolys, 195 | final Node node) { 196 | final List> p = new ArrayList<>(); 197 | p.add(fromPolygon_forceWinding(node.region, false)); 198 | 199 | geopolys.add(p); 200 | 201 | for (int i = 0; i < node.children.size(); ++i) { 202 | p.add(fromPolygon_getInterior(geopolys, node.children.get(i))); 203 | } 204 | } 205 | 206 | private static List fromPolygon_getInterior(final List>> geopolys, 207 | final Node node) { 208 | for (int i = 0; i < node.children.size(); ++i) { 209 | fromPolygon_addExterior(geopolys, node.children.get(i)); 210 | } 211 | 212 | return fromPolygon_forceWinding(node.region, true); 213 | } 214 | 215 | private GeoJSON() { 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/LinkedList.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import java.util.function.Function; 4 | import java.util.function.Predicate; 5 | 6 | public class LinkedList { 7 | public static class TransitionResult { 8 | public final LinkedList before; 9 | public final LinkedList after; 10 | public final Function, LinkedList> insert; 11 | 12 | public TransitionResult(final LinkedList before, 13 | final LinkedList after, 14 | final Function, LinkedList> insert) { 15 | 16 | this.before = before; 17 | this.after = after; 18 | this.insert = insert; 19 | } 20 | } 21 | 22 | public static LinkedList create() { 23 | return new LinkedList<>(true, null); 24 | } 25 | 26 | public static LinkedList node(T content) { 27 | return new LinkedList<>(false, content); 28 | } 29 | 30 | private LinkedList prev; 31 | private LinkedList next; 32 | 33 | private final T content; 34 | private final boolean root; 35 | 36 | private LinkedList(boolean root, T content) { 37 | this.root = root; 38 | this.content = content; 39 | } 40 | 41 | public boolean exists(LinkedList node) { 42 | return node != null && node != this; 43 | } 44 | 45 | public boolean isEmpty() { 46 | return this.next == null; 47 | } 48 | 49 | public LinkedList getHead() { 50 | return this.next; 51 | } 52 | 53 | public LinkedList getPrev() { 54 | return prev; 55 | } 56 | 57 | public LinkedList getNext() { 58 | return next; 59 | } 60 | 61 | public void insertBefore(LinkedList node, Predicate> check) { 62 | LinkedList last = this; 63 | LinkedList here = this.next; 64 | 65 | while (here != null && !here.root) { 66 | if (check.test(here)) { 67 | node.prev = here.prev; 68 | node.next = here; 69 | if (here.prev != null) 70 | here.prev.next = node; 71 | here.prev = node; 72 | return; 73 | } 74 | last = here; 75 | here = here.next; 76 | } 77 | 78 | last.next = node; 79 | node.prev = last; 80 | node.next = null; 81 | } 82 | 83 | public TransitionResult findTransition(Predicate> check) { 84 | LinkedList prev = this; 85 | LinkedList here = this.next; 86 | 87 | while (here != null) { 88 | if (check.test(here)) break; 89 | 90 | prev = here; 91 | here = here.next; 92 | } 93 | 94 | final LinkedList finalPrev = prev; 95 | final LinkedList finalHere = here; 96 | 97 | return new TransitionResult<>( 98 | prev == this 99 | ? null 100 | : prev, 101 | here, 102 | node -> { 103 | node.prev = finalPrev; 104 | node.next = finalHere; 105 | finalPrev.next = node; 106 | if (finalHere != null) 107 | finalHere.prev = node; 108 | 109 | return node; 110 | } 111 | ); 112 | } 113 | 114 | public void remove() { 115 | if (this.root) return; 116 | 117 | if (this.prev != null) 118 | this.prev.next = this.next; 119 | if (this.next != null) 120 | this.next.prev = this.prev; 121 | 122 | this.prev = null; 123 | this.next = null; 124 | } 125 | 126 | public T getContent() { 127 | return content; 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/NonSelfIntersecter.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.models.Segment; 5 | 6 | import java.util.List; 7 | 8 | public class NonSelfIntersecter extends AbstractIntersecter { 9 | public NonSelfIntersecter(Epsilon eps) { 10 | super(false, eps); 11 | } 12 | 13 | public List calculate(List segments1, boolean inverted1, List segments2, boolean inverted2) { 14 | // segmentsX come from the self-intersection API, or this API 15 | // invertedX is whether we treat that list of segments as an inverted polygon or not 16 | // returns segments that can be used for further operations 17 | for (Segment seg : segments1) { 18 | this.eventAddSegment(this.segmentCopy(seg.start, seg.end, seg), true); 19 | } 20 | for (Segment seg : segments2) { 21 | this.eventAddSegment(this.segmentCopy(seg.start, seg.end, seg), false); 22 | } 23 | return this.baseCalculate(inverted1, inverted2); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/SegmentChainer.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.models.Segment; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.Consumer; 11 | 12 | public final class SegmentChainer { 13 | private static class SegmentChainerMatch { 14 | int index; 15 | boolean matches_head; 16 | boolean matches_pt1; 17 | 18 | public SegmentChainerMatch() { 19 | this(0, false, false); 20 | } 21 | 22 | public SegmentChainerMatch(int index, boolean matches_head, boolean matches_pt1) { 23 | this.index = index; 24 | this.matches_head = matches_head; 25 | this.matches_pt1 = matches_pt1; 26 | } 27 | } 28 | 29 | private interface TriPredicate { 30 | boolean apply(T t, U u, V v); 31 | } 32 | 33 | public static List> chain(List segments, Epsilon eps) { 34 | List> chains = new ArrayList<>(); 35 | List> regions = new ArrayList<>(); 36 | 37 | for (Segment seg : segments) { 38 | double[] pt1 = seg.start; 39 | double[] pt2 = seg.end; 40 | if (eps.pointsSame(pt1, pt2)) { 41 | System.err.println("PolyBool: Warning: Zero-length segment detected; your epsilon is probably too small or too large"); 42 | return null; 43 | } 44 | 45 | // search for two chains that this segment matches 46 | final SegmentChainerMatch first_match = new SegmentChainerMatch(); 47 | final SegmentChainerMatch second_match = new SegmentChainerMatch(); 48 | 49 | final SegmentChainerMatch[] next_match = {first_match}; 50 | 51 | final TriPredicate setMatch = (index, matches_head, matches_pt1) -> { 52 | // return true if we've matched twice 53 | next_match[0].index = index; 54 | next_match[0].matches_head = matches_head; 55 | next_match[0].matches_pt1 = matches_pt1; 56 | 57 | if (next_match[0] == first_match) { 58 | next_match[0] = second_match; 59 | return false; 60 | } 61 | next_match[0] = null; 62 | return true; // we've matched twice, we're done here 63 | }; 64 | 65 | for (int i = 0; i < chains.size(); i++) { 66 | List chain = chains.get(i); 67 | double[] head = chain.get(0); 68 | double[] tail = chain.get(chain.size() - 1); 69 | 70 | if (eps.pointsSame(head, pt1)) { 71 | if (setMatch.apply(i, true, true)) 72 | break; 73 | } else if (eps.pointsSame(head, pt2)) { 74 | if (setMatch.apply(i, true, false)) 75 | break; 76 | } else if (eps.pointsSame(tail, pt1)) { 77 | if (setMatch.apply(i, false, true)) 78 | break; 79 | } else if (eps.pointsSame(tail, pt2)) { 80 | if (setMatch.apply(i, false, false)) 81 | break; 82 | } 83 | } 84 | 85 | if (next_match[0] == first_match) { 86 | List newChain = new ArrayList<>(); 87 | newChain.add(pt1); 88 | newChain.add(pt2); 89 | 90 | // we didn't match anything, so create a new chain 91 | chains.add(newChain); 92 | continue; 93 | } 94 | 95 | if (next_match[0] == second_match) { 96 | // we matched a single chain 97 | 98 | // add the other point to the apporpriate end, and check to see if we've closed the 99 | // chain into a loop 100 | 101 | int index = first_match.index; 102 | double[] pt = first_match.matches_pt1 ? pt2 : pt1; // if we matched pt1, then we add pt2, etc 103 | boolean addToHead = first_match.matches_head; // if we matched at head, then add to the head 104 | 105 | List chain = chains.get(index); 106 | double[] grow = addToHead ? chain.get(0) : chain.get(chain.size() - 1); 107 | double[] grow2 = addToHead ? chain.get(1) : chain.get(chain.size() - 2); 108 | double[] oppo = addToHead ? chain.get(chain.size() - 1) : chain.get(0); 109 | double[] oppo2 = addToHead ? chain.get(chain.size() - 2) : chain.get(1); 110 | 111 | if (eps.pointsCollinear(grow2, grow, pt)) { 112 | // grow isn't needed because it's directly between grow2 and pt: 113 | // grow2 ---grow---> pt 114 | if (addToHead) { 115 | chain.remove(0); 116 | } else { 117 | chain.remove(chain.size() - 1); 118 | } 119 | grow = grow2; // old grow is gone... new grow is what grow2 was 120 | } 121 | 122 | if (eps.pointsSame(oppo, pt)) { 123 | // we're closing the loop, so remove chain from chains 124 | chains.remove(index); 125 | 126 | if (eps.pointsCollinear(oppo2, oppo, grow)) { 127 | // oppo isn't needed because it's directly between oppo2 and grow: 128 | // oppo2 ---oppo--->grow 129 | if (addToHead) { 130 | chain.remove(chain.size() - 1); 131 | } else { 132 | chain.remove(0); 133 | } 134 | } 135 | 136 | // we have a closed chain! 137 | regions.add(chain); 138 | continue; 139 | } 140 | 141 | // not closing a loop, so just add it to the apporpriate side 142 | if (addToHead) { 143 | chain.add(0, pt); 144 | } else { 145 | chain.add(pt); 146 | } 147 | continue; 148 | } 149 | 150 | // otherwise, we matched two chains, so we need to combine those chains together 151 | 152 | Consumer reverseChain = index -> Collections.reverse(chains.get(index)); 153 | BiConsumer appendChain = (index1, index2) -> { 154 | // index1 gets index2 appended to it, and index2 is removed 155 | List chain1 = chains.get(index1); 156 | List chain2 = chains.get(index2); 157 | double[] tail = chain1.get(chain1.size() - 1); 158 | double[] tail2 = chain1.get(chain1.size() - 2); 159 | double[] head = chain2.get(0); 160 | double[] head2 = chain2.get(1); 161 | 162 | if (eps.pointsCollinear(tail2, tail, head)) { 163 | // tail isn't needed because it's directly between tail2 and head 164 | // tail2 ---tail---> head 165 | chain1.remove(chain1.size() - 1); 166 | tail = tail2; // old tail is gone... new tail is what tail2 was 167 | } 168 | 169 | if (eps.pointsCollinear(tail, head, head2)) { 170 | // head isn't needed because it's directly between tail and head2 171 | // tail ---head---> head2 172 | chain2.remove(0); 173 | } 174 | 175 | List concatenated = new ArrayList<>(); 176 | concatenated.addAll(chain1); 177 | concatenated.addAll(chain2); 178 | chains.set(index1, concatenated); 179 | chains.remove((int) index2); 180 | }; 181 | 182 | int F = first_match.index; 183 | int S = second_match.index; 184 | 185 | boolean reverseF = chains.get(F).size() < chains.get(S).size(); // reverse the shorter chain, if needed 186 | if (first_match.matches_head) { 187 | if (second_match.matches_head) { 188 | if (reverseF) { 189 | // <<<< F <<<< --- >>>> S >>>> 190 | reverseChain.accept(F); 191 | // >>>> F >>>> --- >>>> S >>>> 192 | appendChain.accept(F, S); 193 | } else { 194 | // <<<< F <<<< --- >>>> S >>>> 195 | reverseChain.accept(S); 196 | // <<<< F <<<< --- <<<< S <<<< logically same as: 197 | // >>>> S >>>> --- >>>> F >>>> 198 | appendChain.accept(S, F); 199 | } 200 | } else { 201 | // <<<< F <<<< --- <<<< S <<<< logically same as: 202 | // >>>> S >>>> --- >>>> F >>>> 203 | appendChain.accept(S, F); 204 | } 205 | } else { 206 | if (second_match.matches_head) { 207 | // >>>> F >>>> --- >>>> S >>>> 208 | appendChain.accept(F, S); 209 | } else { 210 | if (reverseF) { 211 | // >>>> F >>>> --- <<<< S <<<< 212 | reverseChain.accept(F); 213 | // <<<< F <<<< --- <<<< S <<<< logically same as: 214 | // >>>> S >>>> --- >>>> F >>>> 215 | appendChain.accept(S, F); 216 | } else { 217 | // >>>> F >>>> --- <<<< S <<<< 218 | reverseChain.accept(S); 219 | // >>>> F >>>> --- >>>> S >>>> 220 | appendChain.accept(F, S); 221 | } 222 | } 223 | } 224 | } 225 | 226 | return regions; 227 | } 228 | } -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/SegmentSelector.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.models.Segment; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class SegmentSelector { 9 | private static List select(List segments, int[] selection) { 10 | List result = new ArrayList<>(); 11 | 12 | for (Segment seg : segments) { 13 | int index = 14 | (seg.myFill.above ? 8 : 0) + 15 | (seg.myFill.below ? 4 : 0) + 16 | ((seg.otherFill != null && seg.otherFill.above) ? 2 : 0) + 17 | ((seg.otherFill != null && seg.otherFill.below) ? 1 : 0); 18 | 19 | if (selection[index] != 0) { 20 | // copy the segment to the results, while also calculating the fill status 21 | result.add(new Segment( 22 | seg.start, 23 | seg.end, 24 | new Segment.SegmentFill( 25 | selection[index] == 1, 26 | selection[index] == 2 27 | ) 28 | )); 29 | } 30 | } 31 | 32 | return result; 33 | } 34 | 35 | public static List union(List segments) { 36 | // above1 below1 above2 below2 Keep? Value 37 | // 0 0 0 0 => no 0 38 | // 0 0 0 1 => yes filled below 2 39 | // 0 0 1 0 => yes filled above 1 40 | // 0 0 1 1 => no 0 41 | // 0 1 0 0 => yes filled below 2 42 | // 0 1 0 1 => yes filled below 2 43 | // 0 1 1 0 => no 0 44 | // 0 1 1 1 => no 0 45 | // 1 0 0 0 => yes filled above 1 46 | // 1 0 0 1 => no 0 47 | // 1 0 1 0 => yes filled above 1 48 | // 1 0 1 1 => no 0 49 | // 1 1 0 0 => no 0 50 | // 1 1 0 1 => no 0 51 | // 1 1 1 0 => no 0 52 | // 1 1 1 1 => no 0 53 | return select(segments, new int[]{ 54 | 0, 2, 1, 0, 55 | 2, 2, 0, 0, 56 | 1, 0, 1, 0, 57 | 0, 0, 0, 0 58 | }); 59 | } 60 | 61 | public static List intersect(List segments) { 62 | // above1 below1 above2 below2 Keep? Value 63 | // 0 0 0 0 => no 0 64 | // 0 0 0 1 => no 0 65 | // 0 0 1 0 => no 0 66 | // 0 0 1 1 => no 0 67 | // 0 1 0 0 => no 0 68 | // 0 1 0 1 => yes filled below 2 69 | // 0 1 1 0 => no 0 70 | // 0 1 1 1 => yes filled below 2 71 | // 1 0 0 0 => no 0 72 | // 1 0 0 1 => no 0 73 | // 1 0 1 0 => yes filled above 1 74 | // 1 0 1 1 => yes filled above 1 75 | // 1 1 0 0 => no 0 76 | // 1 1 0 1 => yes filled below 2 77 | // 1 1 1 0 => yes filled above 1 78 | // 1 1 1 1 => no 0 79 | return select(segments, new int[]{ 80 | 0, 0, 0, 0, 81 | 0, 2, 0, 2, 82 | 0, 0, 1, 1, 83 | 0, 2, 1, 0 84 | }); 85 | } 86 | 87 | public static List difference(List segments) { // primary - secondary 88 | // above1 below1 above2 below2 Keep? Value 89 | // 0 0 0 0 => no 0 90 | // 0 0 0 1 => no 0 91 | // 0 0 1 0 => no 0 92 | // 0 0 1 1 => no 0 93 | // 0 1 0 0 => yes filled below 2 94 | // 0 1 0 1 => no 0 95 | // 0 1 1 0 => yes filled below 2 96 | // 0 1 1 1 => no 0 97 | // 1 0 0 0 => yes filled above 1 98 | // 1 0 0 1 => yes filled above 1 99 | // 1 0 1 0 => no 0 100 | // 1 0 1 1 => no 0 101 | // 1 1 0 0 => no 0 102 | // 1 1 0 1 => yes filled above 1 103 | // 1 1 1 0 => yes filled below 2 104 | // 1 1 1 1 => no 0 105 | return select(segments, new int[]{ 106 | 0, 0, 0, 0, 107 | 2, 0, 2, 0, 108 | 1, 1, 0, 0, 109 | 0, 1, 2, 0 110 | }); 111 | } 112 | 113 | 114 | public static List differenceRev(List segments) { // secondary - primary 115 | // above1 below1 above2 below2 Keep? Value 116 | // 0 0 0 0 => no 0 117 | // 0 0 0 1 => yes filled below 2 118 | // 0 0 1 0 => yes filled above 1 119 | // 0 0 1 1 => no 0 120 | // 0 1 0 0 => no 0 121 | // 0 1 0 1 => no 0 122 | // 0 1 1 0 => yes filled above 1 123 | // 0 1 1 1 => yes filled above 1 124 | // 1 0 0 0 => no 0 125 | // 1 0 0 1 => yes filled below 2 126 | // 1 0 1 0 => no 0 127 | // 1 0 1 1 => yes filled below 2 128 | // 1 1 0 0 => no 0 129 | // 1 1 0 1 => no 0 130 | // 1 1 1 0 => no 0 131 | // 1 1 1 1 => no 0 132 | return select(segments, new int[]{ 133 | 0, 2, 1, 0, 134 | 0, 0, 1, 1, 135 | 0, 2, 0, 2, 136 | 0, 0, 0, 0 137 | }); 138 | } 139 | 140 | public static List xor(List segments) { // primary ^ secondary 141 | // above1 below1 above2 below2 Keep? Value 142 | // 0 0 0 0 => no 0 143 | // 0 0 0 1 => yes filled below 2 144 | // 0 0 1 0 => yes filled above 1 145 | // 0 0 1 1 => no 0 146 | // 0 1 0 0 => yes filled below 2 147 | // 0 1 0 1 => no 0 148 | // 0 1 1 0 => no 0 149 | // 0 1 1 1 => yes filled above 1 150 | // 1 0 0 0 => yes filled above 1 151 | // 1 0 0 1 => no 0 152 | // 1 0 1 0 => no 0 153 | // 1 0 1 1 => yes filled below 2 154 | // 1 1 0 0 => no 0 155 | // 1 1 0 1 => yes filled above 1 156 | // 1 1 1 0 => yes filled below 2 157 | // 1 1 1 1 => no 0 158 | return select(segments, new int[]{ 159 | 0, 2, 1, 0, 160 | 2, 0, 0, 1, 161 | 1, 0, 0, 2, 162 | 0, 1, 2, 0 163 | }); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/internal/SelfIntersecter.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.internal; 2 | 3 | import com.menecats.polybool.Epsilon; 4 | import com.menecats.polybool.models.Segment; 5 | 6 | import java.util.List; 7 | 8 | public class SelfIntersecter extends AbstractIntersecter { 9 | public SelfIntersecter(Epsilon eps) { 10 | super(true, eps); 11 | } 12 | 13 | public void addRegion(List region) { 14 | // regions are a list of points: 15 | // [ [0, 0], [100, 0], [50, 100] ] 16 | // you can add multiple regions before running calculate 17 | double[] pt1; 18 | double[] pt2 = region.get(region.size() - 1); 19 | for (double[] pt : region) { 20 | pt1 = pt2; 21 | pt2 = pt; 22 | 23 | int forward = this.eps.pointsCompare(pt1, pt2); 24 | if (forward == 0) // points are equal, so we have a zero-length segment 25 | continue; // just skip it 26 | 27 | this.eventAddSegment( 28 | this.segmentNew( 29 | forward < 0 ? pt1 : pt2, 30 | forward < 0 ? pt2 : pt1 31 | ), 32 | true 33 | ); 34 | } 35 | } 36 | 37 | public List calculate(boolean inverted) { 38 | // is the polygon inverted? 39 | // returns segments 40 | return this.baseCalculate(inverted, false); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/models/Polygon.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.models; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | public final class Polygon { 8 | private List> regions; 9 | private boolean inverted; 10 | 11 | public Polygon() { 12 | this(new ArrayList<>()); 13 | } 14 | 15 | public Polygon(List> regions) { 16 | this(regions, false); 17 | } 18 | 19 | public Polygon(List> regions, boolean inverted) { 20 | this.regions = regions; 21 | this.inverted = inverted; 22 | } 23 | 24 | public List> getRegions() { 25 | return regions; 26 | } 27 | 28 | public void setRegions(List> regions) { 29 | this.regions = regions; 30 | } 31 | 32 | public boolean isInverted() { 33 | return inverted; 34 | } 35 | 36 | public void setInverted(boolean inverted) { 37 | this.inverted = inverted; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return String.format( 43 | "Polygon { inverted: %s, regions: [\n\t%s\n]}", 44 | inverted, 45 | regions 46 | .stream() 47 | .map(region -> "[" + region 48 | .stream() 49 | .map(point -> String.format( 50 | "[%s, %s]", 51 | point[0], 52 | point[1] 53 | )) 54 | .collect(Collectors.joining(", ")) + "]" 55 | ) 56 | .collect(Collectors.joining(",\n\t")) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/models/Segment.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.models; 2 | 3 | public final class Segment { 4 | public static final class SegmentFill { 5 | public Boolean above; 6 | public Boolean below; 7 | 8 | public SegmentFill() { 9 | } 10 | 11 | public SegmentFill(Boolean above, Boolean below) { 12 | this.above = above; 13 | this.below = below; 14 | } 15 | } 16 | 17 | public double[] start; 18 | public double[] end; 19 | public SegmentFill myFill; 20 | public SegmentFill otherFill; 21 | 22 | public Segment(double[] start, double[] end) { 23 | this(start, end, new SegmentFill()); 24 | } 25 | 26 | public Segment(double[] start, double[] end, SegmentFill myFill) { 27 | this.start = start; 28 | this.end = end; 29 | this.myFill = myFill; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/menecats/polybool/models/geojson/Geometry.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool.models.geojson; 2 | 3 | import java.util.List; 4 | 5 | public abstract class Geometry { 6 | public static final class PolygonGeometry extends Geometry>> { 7 | public PolygonGeometry() { 8 | this(null); 9 | } 10 | 11 | public PolygonGeometry(List> coordinates) { 12 | super(coordinates); 13 | } 14 | 15 | @Override 16 | public String getType() { 17 | return "Polygon"; 18 | } 19 | } 20 | public static final class MultiPolygonGeometry extends Geometry>>> { 21 | public MultiPolygonGeometry() { 22 | this(null); 23 | } 24 | 25 | public MultiPolygonGeometry(List>> coordinates) { 26 | super(coordinates); 27 | } 28 | 29 | @Override 30 | public String getType() { 31 | return "MultiPolygon"; 32 | } 33 | } 34 | 35 | private T coordinates; 36 | 37 | protected Geometry(T coordinates) { 38 | setCoordinates(coordinates); 39 | } 40 | 41 | public abstract String getType(); 42 | 43 | public T getCoordinates() { 44 | return coordinates; 45 | } 46 | 47 | public void setCoordinates(T coordinates) { 48 | this.coordinates = coordinates; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/menecats/polybool/PolyBoolExample.java: -------------------------------------------------------------------------------- 1 | package com.menecats.polybool; 2 | 3 | import com.menecats.polybool.models.Polygon; 4 | 5 | import static com.menecats.polybool.helpers.PolyBoolHelper.*; 6 | 7 | public class PolyBoolExample { 8 | public static void main(String[] args) { 9 | Epsilon eps = epsilon(); 10 | 11 | Polygon intersection = PolyBool.intersect( 12 | eps, 13 | polygon( 14 | region( 15 | point(50, 50), 16 | point(150, 150), 17 | point(190, 50) 18 | ), 19 | region( 20 | point(130, 50), 21 | point(290, 150), 22 | point(290, 50) 23 | ) 24 | ), 25 | polygon( 26 | region( 27 | point(110, 20), 28 | point(110, 110), 29 | point(20, 20) 30 | ), 31 | region( 32 | point(130, 170), 33 | point(130, 20), 34 | point(260, 20), 35 | point(260, 170) 36 | ) 37 | ) 38 | ); 39 | 40 | System.out.println(intersection); 41 | // Polygon { inverted: false, regions: [ 42 | // [[50.0, 50.0], [110.0, 50.0], [110.0, 110.0]], 43 | // [[178.0, 80.0], [130.0, 50.0], [130.0, 130.0], [150.0, 150.0]], 44 | // [[178.0, 80.0], [190.0, 50.0], [260.0, 50.0], [260.0, 131.25]] 45 | // ]} 46 | } 47 | } 48 | --------------------------------------------------------------------------------