commit 8611f7303e89299b7901e97a4b2e88e4f17acceb Author: Edwin Jakobs Date: Thu Oct 4 22:58:28 2018 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..294641de --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle/ +out/ +target/ +build/ +*.iml/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..e6cfcd8c --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# ORX (OPENRNDR EXTRA) + +A growing library of assorted data structures, algorithms and utilities. + +- orx-kdtree - kd-tree implementation for fast nearest point searches + + +## Usage + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..c9d6c149 --- /dev/null +++ b/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.2.71' + id 'maven' +} + +group 'org.openrndr.extra' +version '0.0.1' + +repositories { + mavenCentral() +} + +ext { + openrndrVersion = "0.4.0-SNAPSHOT" +} + + +subprojects { + + apply plugin: 'kotlin' + repositories { + mavenLocal() + mavenCentral() + maven { + url="https://dl.bintray.com/openrndr/openrndr" + } + } + + dependencies { + compile "org.openrndr:openrndr-core:$openrndrVersion" + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '0.27.0' + } + +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1948b907 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..221e4abf --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Oct 04 22:36:06 CEST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/orx-kdtree/src/main/kotlin/KDTree.kt b/orx-kdtree/src/main/kotlin/KDTree.kt new file mode 100644 index 00000000..3a805e0e --- /dev/null +++ b/orx-kdtree/src/main/kotlin/KDTree.kt @@ -0,0 +1,353 @@ +package org.openrndr.extra.kdtree + +import kotlinx.coroutines.experimental.CoroutineScope +import kotlinx.coroutines.experimental.GlobalScope +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.runBlocking +import org.openrndr.math.IntVector2 +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.Vector4 +import java.util.* +import java.util.concurrent.atomic.AtomicInteger +import kotlin.IllegalStateException + +/** built-in mapper for [Vector2] */ +fun vector2Mapper(v: Vector2, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + else -> v.y + } +} + +fun intVector2Mapper(v: IntVector2, dimension: Int): Double { + return when (dimension) { + 0 -> v.x.toDouble() + else -> v.y.toDouble() + } +} + + +/** built-in mapper for [Vector3] */ +fun vector3Mapper(v: Vector3, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + 1 -> v.y + else -> v.y + } +} + +/** built-in mapper for [Vector4] */ +fun vector4Mapper(v: Vector4, dimension: Int): Double { + return when (dimension) { + 0 -> v.x + 1 -> v.y + 2 -> v.z + else -> v.w + } +} + +class KDTreeNode { + var parent: KDTreeNode? = null + var median: Double = 0.0 + var dimension: Int = 0 + var children: Array?> = arrayOfNulls(2) + var item: T? = null + + internal val isLeaf: Boolean + get() = children[0] == null && children[1] == null + + override fun toString(): String { + return "KDTreeNode{" + + "median=" + median + + ", item=" + item + + ", dimension=" + dimension + + ", children=" + Arrays.toString(children) + + + "} " + super.toString() + } +} + +fun insertItem(root: KDTreeNode, item: T, mapper: (T, Int) -> Double): KDTreeNode { + return if (root.isLeaf) { + root.item = item + root + } else { + if (mapper(item, root.dimension) < root.median) { + insertItem(root.children[0] ?: throw IllegalStateException("left is null"), item, mapper) + } else { + insertItem(root.children[1] ?: throw IllegalStateException("right is null"), item, mapper) + } + } +} + +fun buildKDTree(items: MutableList, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { + val root = KDTreeNode() + + val start = System.currentTimeMillis() + fun buildTreeTask(scope: CoroutineScope, node: KDTreeNode, items: MutableList, dimensions: Int, levels: Int, mapper: (T, Int) -> Double): KDTreeNode { + + if (items.size > 0) { + val dimension = levels % dimensions + val values = ArrayList() + for (item in items) { + values.add(item) + } + + node.dimension = dimension + val median = selectNth(items, items.size / 2) { mapper(it, dimension) } + + val leftItems = mutableListOf() + val rightItems = mutableListOf() + + node.median = mapper(median, dimension) + node.item = median + for (item in items) { + if (item === median) { + continue + } + if (mapper(item, dimension) < node.median) { + leftItems.add(item) + } else { + rightItems.add(item) + } + } + + // validate split + if (leftItems.size + rightItems.size + 1 != items.size) { + throw IllegalStateException("left: ${leftItems.size}, right: ${rightItems.size}, items: ${items.size}") + } + + if (leftItems.size > 0) { + node.children[0] = KDTreeNode() + node.children[0]?.let { + it.parent = node + + scope.launch { + buildTreeTask(scope, it, leftItems, dimensions, levels + 1, mapper) + } + } + } + if (rightItems.size > 0) { + node.children[1] = KDTreeNode() + node.children[1]?.let { + it.parent = node + scope.launch { + buildTreeTask(scope, it, rightItems, dimensions, levels + 1, mapper) + } + } + } + } + return node + } + + val job = GlobalScope.launch { + buildTreeTask(this, root, items, dimensions, 0, mapper) + } + runBlocking { + job.join() + } + println("building took ${System.currentTimeMillis()-start}ms") + return root +} + + +private fun sqrDistance(left: T, right: T, dimensions: Int, mapper: (T, Int) -> Double): Double { + var distance = 0.0 + + for (i in 0 until dimensions) { + val d = mapper(left, i) - mapper(right, i) + distance += d * d + } + return distance +} + +fun findAllNodes(root: KDTreeNode): List> { + val stack = Stack>() + val all = ArrayList>() + stack.empty() + stack.push(root) + while (!stack.isEmpty()) { + val node = stack.pop() +// if (node.item != null /*&& !visited.contains(node.children[1])*/) { + all.add(node) +// } + + if (node.children[0] != null /*&&!visited.contains(node.children[0])*/) { + stack.push(node.children[0]) + } + if (node.children[1] != null) { + stack.push(node.children[1]) + } + } + return all +} + + +fun findNearest(root: KDTreeNode, item: T, dimensions: Int, mapper: (T, Int) -> Double): T? { + var nearest = java.lang.Double.POSITIVE_INFINITY + var nearestArg: KDTreeNode? = null + + fun nearest(node: KDTreeNode?, item: T) { + if (node != null) { + + if (node.item == null) { + println(node) + } + + val route: Int = if (mapper(item, node.dimension) < node.median) { + nearest(node.children[0], item) + 0 + } else { + nearest(node.children[1], item) + 1 + } + + val distance = sqrDistance(item, node.item + ?: throw IllegalStateException("item is null"), dimensions, mapper) + if (distance < nearest) { + nearest = distance + nearestArg = node + } + + + val d = Math.abs(node.median - mapper(item, node.dimension)) + if (d * d < nearest) { + nearest(node.children[1 - route], item) + } + } + } + nearest(root, item) + return nearestArg?.item +} + +fun insert(root: KDTreeNode, item: T, dimensions: Int, mapper: (T, Int) -> Double): KDTreeNode { + val stack = Stack>() + stack.push(root) + + dive@ while (true) { + + val node = stack.peek() + + val value = mapper(item, node.dimension) + + if (value < node.median) { + if (node.children[0] != null) { + stack.push(node.children[0]) + } else { + // sit here + node.children[0] = KDTreeNode() + node.children[0]?.item = item + node.children[0]?.dimension = (node.dimension + 1) % dimensions + node.children[0]?.median = mapper(item, (node.dimension + 1) % dimensions) + node.children[0]?.parent = node + return node.children[0] ?: throw IllegalStateException("child is null") + } + } else { + if (node.children[1] != null) { + stack.push(node.children[1]) + } else { + // sit here + node.children[1] = KDTreeNode() + node.children[1]?.item = item + node.children[1]?.dimension = (node.dimension + 1) % dimensions + node.children[1]?.median = mapper(item, (node.dimension + 1) % dimensions) + node.children[1]?.parent = node + return node.children[1] ?: throw IllegalStateException("child is null") + + } + } + } +} + +fun remove(toRemove: KDTreeNode, mapper: (T, Int) -> Double): KDTreeNode? { + // trivial case + if (toRemove.isLeaf) { + val p = toRemove.parent + if (p != null) { + when { + p.children[0] === toRemove -> p.children[0] = null + p.children[1] === toRemove -> p.children[1] = null + else -> { + // broken! + } + } + } else { + toRemove.item = null + } + } else { + val stack = Stack>() + + var branch = 0 + + if (toRemove.children[0] != null) { + stack.push(toRemove.children[0]) + branch = 0 + } else { + stack.push(toRemove.children[1]) + branch = 1 + } + + var minValue: Double = java.lang.Double.POSITIVE_INFINITY + var maxValue: Double = java.lang.Double.NEGATIVE_INFINITY + var minArg: KDTreeNode? = null + var maxArg: KDTreeNode? = null + + while (!stack.isEmpty()) { + val node = stack.pop() ?: throw RuntimeException("null on stack") + + val value = mapper(node.item ?: throw IllegalStateException("item is null"), toRemove.dimension) + + if (value < minValue) { + minValue = value + minArg = node + } + + if (value > maxValue) { + maxValue = value + maxArg = node + } + + if (node.dimension != toRemove.dimension) { + if (node.children[0] != null) { + stack.push(node.children[0]) + } + if (node.children[1] != null) { + stack.push(node.children[1]) + } + } else { + if (branch == 1) { + if (node.children[0] != null) { + stack.push(node.children[0]) + } else { + if (node.children[1] != null) { + stack.push(node.children[1]) + } + } + } + if (branch == 0) { + if (node.children[1] != null) { + stack.push(node.children[1]) + } else { + if (node.children[0] != null) { + stack.push(node.children[0]) + } + } + } + } + } + + + if (branch == 1) { + toRemove.item = minArg?.item + toRemove.median = mapper(minArg?.item ?: throw IllegalStateException("minArg is null"), toRemove.dimension) + remove(minArg, mapper) + } + if (branch == 0) { + toRemove.item = maxArg?.item + toRemove.median = mapper(maxArg?.item ?: throw IllegalStateException("maxArg is null"), toRemove.dimension) + remove(maxArg, mapper) + } + } + return null +} diff --git a/orx-kdtree/src/main/kotlin/QuickSelect.kt b/orx-kdtree/src/main/kotlin/QuickSelect.kt new file mode 100644 index 00000000..da29d207 --- /dev/null +++ b/orx-kdtree/src/main/kotlin/QuickSelect.kt @@ -0,0 +1,37 @@ +package org.openrndr.extra.kdtree + +fun selectNth(items: MutableList, n: Int, mapper: (T)->Double): T { + var from = 0 + var to = items.size - 1 + + // if from == to we reached the kth element + while (from < to) { + var r = from + var w = to + val mid = mapper(items[(r + w) / 2]) + + // stop when the reader and writer meet + while (r < w) { + if (mapper(items[r]) >= mid) { // put the large values at the end + val tmp = items[w] + items[w] = items[r] + items[r] = tmp + w-- + } else { // the value is smaller than the pivot, skip + r++ + } + } + + // if we stepped up (r++) we need to step one down + if (mapper(items[r]) > mid) + r-- + + // the r pointer is on the end of the first k elements + if (n <= r) { + to = r + } else { + from = r + 1 + } + } + return items[n] +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..da03ac5f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'orx' + +include 'orx-kdtree' \ No newline at end of file