├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------