From da584159892db59341cc3c2b48d5aa26769b1dff Mon Sep 17 00:00:00 2001 From: tabidachinokaze Date: Thu, 13 Nov 2025 18:31:10 +0800 Subject: [PATCH] initial commit --- .gitignore | 11 + app/.gitignore | 1 + app/build.gradle | 49 ++ app/proguard-rules.pro | 21 + .../geotools/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 30 + .../java/com/icegps/common/helper/BlhToEnu.kt | 274 +++++++ .../com/icegps/common/helper/GeoHelper.kt | 419 +++++++++++ .../java/com/icegps/geotools/GeoJsonUtils.kt | 83 +++ .../main/java/com/icegps/geotools/GridCell.kt | 529 ++++++++++++++ .../java/com/icegps/geotools/MainActivity.kt | 594 +++++++++++++++ .../java/com/icegps/geotools/RasterUtils.kt | 165 +++++ .../main/java/com/icegps/geotools/ktx/Any.kt | 7 + .../java/com/icegps/geotools/ktx/Vector3D.kt | 17 + .../java/com/icegps/geotools/model/DPoint.kt | 11 + .../res/drawable/ic_launcher_background.xml | 170 +++++ .../res/drawable/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable/ic_pile_marker.xml | 29 + app/src/main/res/drawable/test_radar.png | Bin 0 -> 29053 bytes app/src/main/res/drawable/voroni4.jpg | Bin 0 -> 9086 bytes app/src/main/res/layout/activity_main.xml | 10 + .../main/res/mipmap-anydpi/ic_launcher.xml | 6 + .../res/mipmap-anydpi/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 16 + app/src/main/res/values/colors.xml | 10 + .../main/res/values/mapbox_access_token.xml | 4 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 16 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../com/icegps/geotools/ExampleUnitTest.kt | 17 + build.gradle | 7 + delaunator/.gitignore | 1 + delaunator/build.gradle | 40 + delaunator/consumer-rules.pro | 0 delaunator/proguard-rules.pro | 21 + .../geotools/ExampleInstrumentedTest.kt | 24 + delaunator/src/main/AndroidManifest.xml | 4 + .../java/com/icegps/geotools/Delaunator.kt | 625 ++++++++++++++++ .../java/com/icegps/geotools/model/Edge.kt | 7 + .../java/com/icegps/geotools/model/IEdge.kt | 7 + .../java/com/icegps/geotools/model/IPoint.kt | 6 + .../com/icegps/geotools/model/ITriangle.kt | 6 + .../com/icegps/geotools/model/IVoronoiCell.kt | 6 + .../java/com/icegps/geotools/model/Point.kt | 19 + .../com/icegps/geotools/model/Triangle.kt | 6 + .../com/icegps/geotools/model/VoronoiCell.kt | 6 + .../com/icegps/geotools/ExampleUnitTest.kt | 17 + gradle.properties | 23 + gradle/libs.versions.toml | 29 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 +++++ gradlew.bat | 89 +++ math/.gitignore | 1 + math/build.gradle | 17 + .../main/java/com/icegps/io/util/NumberExt.kt | 75 ++ .../java/com/icegps/io/util/NumberParser.kt | 78 ++ .../main/java/com/icegps/math/Alignment.kt | 47 ++ .../java/com/icegps/math/BooleanConversion.kt | 9 + math/src/main/java/com/icegps/math/Clamp.kt | 38 + .../main/java/com/icegps/math/ConvertRange.kt | 24 + .../src/main/java/com/icegps/math/Division.kt | 19 + math/src/main/java/com/icegps/math/Fract.kt | 4 + math/src/main/java/com/icegps/math/ILog.kt | 9 + .../java/com/icegps/math/IsAlmostEquals.kt | 14 + .../main/java/com/icegps/math/IsAlmostZero.kt | 4 + math/src/main/java/com/icegps/math/IsEven.kt | 9 + .../java/com/icegps/math/IsNanOrInfinite.kt | 11 + math/src/main/java/com/icegps/math/Math.kt | 133 ++++ .../java/com/icegps/math/NormalizeZero.kt | 7 + .../main/java/com/icegps/math/PowerOfTwo.kt | 22 + .../com/icegps/math/RoundDecimalPlaces.kt | 16 + .../com/icegps/math/ToIntegerConverters.kt | 29 + math/src/main/java/com/icegps/math/Umod.kt | 35 + .../src/main/java/com/icegps/math/Unsigned.kt | 13 + .../math/annotations/_Math_annotations.kt | 41 ++ .../java/com/icegps/math/geometry/AABB3D.kt | 55 ++ .../java/com/icegps/math/geometry/Anchor.kt | 103 +++ .../java/com/icegps/math/geometry/Angle.kt | 250 +++++++ .../com/icegps/math/geometry/BoundsBuilder.kt | 60 ++ .../java/com/icegps/math/geometry/Circle.kt | 29 + .../java/com/icegps/math/geometry/Ellipse.kt | 83 +++ .../com/icegps/math/geometry/EulerRotation.kt | 330 +++++++++ .../com/icegps/math/geometry/IPointList.kt | 124 ++++ .../java/com/icegps/math/geometry/Line.kt | 175 +++++ .../java/com/icegps/math/geometry/Line3D.kt | 3 + .../java/com/icegps/math/geometry/Margin.kt | 75 ++ .../java/com/icegps/math/geometry/Matrix.kt | 415 +++++++++++ .../java/com/icegps/math/geometry/Matrix3.kt | 237 ++++++ .../java/com/icegps/math/geometry/Matrix4.kt | 684 ++++++++++++++++++ .../com/icegps/math/geometry/Matrix4Ext.kt | 9 + .../com/icegps/math/geometry/MatrixExt.kt | 64 ++ .../icegps/math/geometry/MatrixMajorOrder.kt | 3 + .../com/icegps/math/geometry/Orientation.kt | 55 ++ .../java/com/icegps/math/geometry/Polygon.kt | 3 + .../java/com/icegps/math/geometry/Polyline.kt | 3 + .../com/icegps/math/geometry/Quaternion.kt | 326 +++++++++ .../main/java/com/icegps/math/geometry/Ray.kt | 90 +++ .../com/icegps/math/geometry/RectCorners.kt | 28 + .../com/icegps/math/geometry/Rectangle.kt | 291 ++++++++ .../com/icegps/math/geometry/RectangleInt.kt | 89 +++ .../icegps/math/geometry/RoundRectangle.kt | 18 + .../java/com/icegps/math/geometry/Scale.kt | 52 ++ .../com/icegps/math/geometry/ScaleMode.kt | 40 + .../java/com/icegps/math/geometry/Size.kt | 150 ++++ .../java/com/icegps/math/geometry/Spacing.kt | 28 + .../java/com/icegps/math/geometry/Sphere3D.kt | 13 + .../com/icegps/math/geometry/VectorExt.kt | 47 ++ .../com/icegps/math/geometry/VectorsDouble.kt | 343 +++++++++ .../com/icegps/math/geometry/VectorsFloat.kt | 523 +++++++++++++ .../com/icegps/math/geometry/VectorsInt.kt | 41 ++ .../math/geometry/shape/SimpleShape2D.kt | 15 + .../math/geometry/shape/SimpleShape3D.kt | 8 + .../com/icegps/math/interpolation/Easing.kt | 44 ++ .../interpolation/Interpolation.vector.kt | 8 + .../com/icegps/math/interpolation/Ratio.kt | 109 +++ .../java/com/icegps/math/range/OpenRange.kt | 8 + .../main/java/com/icegps/math/range/Ranges.kt | 21 + math/src/main/java/com/icegps/memory/Bits.kt | 349 +++++++++ .../main/java/com/icegps/memory/DoubleBits.kt | 47 ++ math/src/main/java/com/icegps/memory/Int64.kt | 194 +++++ .../main/java/com/icegps/number/StringExt.kt | 71 ++ .../com/icegps/math/geometry/AngleTest.kt | 22 + .../icegps/math/geometry/EulerRotationTest.kt | 15 + .../java/com/icegps/number/NiceStrTest.kt | 15 + settings.gradle | 29 + 137 files changed, 10167 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/icegps/common/helper/BlhToEnu.kt create mode 100644 app/src/main/java/com/icegps/common/helper/GeoHelper.kt create mode 100644 app/src/main/java/com/icegps/geotools/GeoJsonUtils.kt create mode 100644 app/src/main/java/com/icegps/geotools/GridCell.kt create mode 100644 app/src/main/java/com/icegps/geotools/MainActivity.kt create mode 100644 app/src/main/java/com/icegps/geotools/RasterUtils.kt create mode 100644 app/src/main/java/com/icegps/geotools/ktx/Any.kt create mode 100644 app/src/main/java/com/icegps/geotools/ktx/Vector3D.kt create mode 100644 app/src/main/java/com/icegps/geotools/model/DPoint.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_pile_marker.xml create mode 100644 app/src/main/res/drawable/test_radar.png create mode 100644 app/src/main/res/drawable/voroni4.jpg create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/mapbox_access_token.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/icegps/geotools/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 delaunator/.gitignore create mode 100644 delaunator/build.gradle create mode 100644 delaunator/consumer-rules.pro create mode 100644 delaunator/proguard-rules.pro create mode 100644 delaunator/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt create mode 100644 delaunator/src/main/AndroidManifest.xml create mode 100644 delaunator/src/main/java/com/icegps/geotools/Delaunator.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/Edge.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/IEdge.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/IPoint.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/ITriangle.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/IVoronoiCell.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/Point.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/Triangle.kt create mode 100644 delaunator/src/main/java/com/icegps/geotools/model/VoronoiCell.kt create mode 100644 delaunator/src/test/java/com/icegps/geotools/ExampleUnitTest.kt create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 math/.gitignore create mode 100644 math/build.gradle create mode 100644 math/src/main/java/com/icegps/io/util/NumberExt.kt create mode 100644 math/src/main/java/com/icegps/io/util/NumberParser.kt create mode 100644 math/src/main/java/com/icegps/math/Alignment.kt create mode 100644 math/src/main/java/com/icegps/math/BooleanConversion.kt create mode 100644 math/src/main/java/com/icegps/math/Clamp.kt create mode 100644 math/src/main/java/com/icegps/math/ConvertRange.kt create mode 100644 math/src/main/java/com/icegps/math/Division.kt create mode 100644 math/src/main/java/com/icegps/math/Fract.kt create mode 100644 math/src/main/java/com/icegps/math/ILog.kt create mode 100644 math/src/main/java/com/icegps/math/IsAlmostEquals.kt create mode 100644 math/src/main/java/com/icegps/math/IsAlmostZero.kt create mode 100644 math/src/main/java/com/icegps/math/IsEven.kt create mode 100644 math/src/main/java/com/icegps/math/IsNanOrInfinite.kt create mode 100644 math/src/main/java/com/icegps/math/Math.kt create mode 100644 math/src/main/java/com/icegps/math/NormalizeZero.kt create mode 100644 math/src/main/java/com/icegps/math/PowerOfTwo.kt create mode 100644 math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt create mode 100644 math/src/main/java/com/icegps/math/ToIntegerConverters.kt create mode 100644 math/src/main/java/com/icegps/math/Umod.kt create mode 100644 math/src/main/java/com/icegps/math/Unsigned.kt create mode 100644 math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/AABB3D.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Anchor.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Angle.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Circle.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Ellipse.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/EulerRotation.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/IPointList.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Line.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Line3D.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Margin.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Matrix.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Matrix3.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Matrix4.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/MatrixExt.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Orientation.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Polygon.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Polyline.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Quaternion.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Ray.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/RectCorners.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Rectangle.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/RectangleInt.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Scale.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/ScaleMode.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Size.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Spacing.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/Sphere3D.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/VectorExt.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/VectorsInt.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt create mode 100644 math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt create mode 100644 math/src/main/java/com/icegps/math/interpolation/Easing.kt create mode 100644 math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt create mode 100644 math/src/main/java/com/icegps/math/interpolation/Ratio.kt create mode 100644 math/src/main/java/com/icegps/math/range/OpenRange.kt create mode 100644 math/src/main/java/com/icegps/math/range/Ranges.kt create mode 100644 math/src/main/java/com/icegps/memory/Bits.kt create mode 100644 math/src/main/java/com/icegps/memory/DoubleBits.kt create mode 100644 math/src/main/java/com/icegps/memory/Int64.kt create mode 100644 math/src/main/java/com/icegps/number/StringExt.kt create mode 100644 math/src/test/java/com/icegps/math/geometry/AngleTest.kt create mode 100644 math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt create mode 100644 math/src/test/java/com/icegps/number/NiceStrTest.kt create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe3b002 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/.kotlin \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..16ada42 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,49 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace 'com.icegps.geotools' + compileSdk 36 + + defaultConfig { + applicationId "com.icegps.geotools" + minSdk 28 + targetSdk 28 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + + implementation libs.androidx.core.ktx + implementation libs.androidx.appcompat + implementation libs.material + implementation 'com.mapbox.maps:android-ndk27:11.16.2' + implementation libs.androidx.activity + implementation libs.androidx.constraintlayout + implementation project(':delaunator') + implementation project(':math') + + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..b923350 --- /dev/null +++ b/app/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.icegps.geotools + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.icegps.geotools", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..25ed9a2 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/icegps/common/helper/BlhToEnu.kt b/app/src/main/java/com/icegps/common/helper/BlhToEnu.kt new file mode 100644 index 0000000..5e1b0c8 --- /dev/null +++ b/app/src/main/java/com/icegps/common/helper/BlhToEnu.kt @@ -0,0 +1,274 @@ +package com.icegps.common.helper + +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt + +/** + * BLH -> ENU + * + * @author lm + * @date 2025/3/12 + * @link https://gist.github.com/komasaru/6ce0634475923ddac597f868288c54e9 + */ +class BlhToEnu { + + companion object { + private const val PI_180 = Math.PI / 180.0 + + // WGS84 坐标参数 + private const val A = 6378137.0 // a(地球椭球体长半径(赤道面平均半径)) + private const val ONE_F = 298.257223563 // 1 / f(地球椭球体扁平率 = (a - b) / a) + private val B = A * (1.0 - 1.0 / ONE_F) // b(地球椭球体短半径) + private val E2 = (1.0 / ONE_F) * (2 - (1.0 / ONE_F)) + + // e^2 = 2 * f - f * f = (a^2 - b^2) / a^2 + private val ED2 = E2 * A * A / (B * B) // e'^2 = (a^2 - b^2) / b^2 + } + + private var originLat: Double = 0.0 + private var originLon: Double = 0.0 + private var originHeight: Double = 0.0 + private var isOriginSet: Boolean = false + + fun getOriginLat(): Double = originLat + + fun getOriginLon(): Double = originLon + + fun getOriginHeight(): Double = originHeight + + fun resetEnuBenchmarkPoint() { + isOriginSet = false + } + + fun wgs84ToEnu(lat: Double, lon: Double, height: Double = 0.0): DoubleArray { + if (!isOriginSet) { + originLat = lat + originLon = lon + originHeight = height + isOriginSet = true + return doubleArrayOf(0.0, 0.0, 0.0) + } + + val enu = blh2enu(originLat, originLon, originHeight, lat, lon, height) +// var az = atan2(enu[0], enu[1]) * 180.0 / Math.PI +// if (az < 0.0) { +// az += 360.0 +// } +// val el = atan2( +// enu[2], +// sqrt(enu[0] * enu[0] + enu[1] * enu[1]) +// ) * 180.0 / Math.PI +// val dst = sqrt(enu.sumOf { it * it }) + +// println("--->") +// println( +// """ +// ENU: E = ${enu[0].format(3)}m +// N = ${enu[1].format(3)}m +// U = ${enu[2].format(3)}m +// 方位角 = ${az.format(3)}° +// 仰角 = ${el.format(3)}° +// 距离 = ${dst.format(3)}m +// """.trimIndent() +// ) + + return enu + } + + fun enuToWgs84(e: Double, n: Double, u: Double): DoubleArray { + if (!isOriginSet) { + return doubleArrayOf(0.0, 0.0, 0.0) + } + + val blh = enu2blh(originLat, originLon, originHeight, e, n, u) + +// println("--->") +// println( +// """ +// BLH: Beta = ${blh[0].format(8)}° +// Lambda = ${blh[1].format(8)}° +// Height = ${blh[2].format(3)}m +// """.trimIndent() +// ) + + return blh + } + + private fun Double.format(digits: Int) = "%.${digits}f".format(this) + + /** + * BLH -> ENU 转换(East, North, Up) + * + * @param bO 原点 Beta(纬度) + * @param lO 原点 Lambda(经度) + * @param hO 原点 Height(高度) + * @param b 目标点 Beta(纬度) + * @param l 目标点 Lambda(经度) + * @param h 目标点 Height(高度) + * @return ENU 坐标 [e, n, u] + */ + private fun blh2enu(bO: Double, lO: Double, hO: Double, b: Double, l: Double, h: Double): DoubleArray { + val (xO, yO, zO) = blh2ecef(bO, lO, hO) + val (x, y, z) = blh2ecef(b, l, h) + val mat0 = matZ(90.0) + val mat1 = matY(90.0 - bO) + val mat2 = matZ(lO) + val mat = mulMat(mulMat(mat0, mat1), mat2) + return rotate(mat, doubleArrayOf(x - xO, y - yO, z - zO)) + } + + /** + * BLH -> ECEF 转换 + * + * @param lat 纬度 + * @param lon 经度 + * @param height 高度 + * @return ECEF 坐标 [x, y, z] + */ + private fun blh2ecef(lat: Double, lon: Double, height: Double): DoubleArray { + val n = { x: Double -> A / sqrt(1.0 - E2 * sin(x * PI_180).pow(2)) } + val x = (n(lat) + height) * cos(lat * PI_180) * cos(lon * PI_180) + val y = (n(lat) + height) * cos(lat * PI_180) * sin(lon * PI_180) + val z = (n(lat) * (1.0 - E2) + height) * sin(lat * PI_180) + return doubleArrayOf(x, y, z) + } + + /** + * ENU -> BLH 转换 + * + * @param e East 坐标 + * @param n North 坐标 + * @param u Up 坐标 + * @return WGS84 坐标 [纬度, 经度, 高度] + */ + private fun enu2blh(bO: Double, lO: Double, hO: Double, e: Double, n: Double, u: Double): DoubleArray { + val mat0 = matZ(-lO) + val mat1 = matY(-(90.0 - bO)) + val mat2 = matZ(-90.0) + val mat = mulMat(mulMat(mat0, mat1), mat2) + + val enu = doubleArrayOf(e, n, u) + val xyz = rotate(mat, enu) + + val (xO, yO, zO) = blh2ecef(bO, lO, hO) + val x = xyz[0] + xO + val y = xyz[1] + yO + val z = xyz[2] + zO + + return ecef2blh(x, y, z) + } + + /** + * ECEF -> BLH 转换 + * + * @param x ECEF X 坐标 + * @param y ECEF Y 坐标 + * @param z ECEF Z 坐标 + * @return WGS84 坐标 [纬度, 经度, 高度] + */ + private fun ecef2blh(x: Double, y: Double, z: Double): DoubleArray { + val p = sqrt(x * x + y * y) + val theta = atan2(z * A, p * B) + val sinTheta = sin(theta) + val cosTheta = cos(theta) + + val lat = atan2( + z + ED2 * B * sinTheta.pow(3), + p - E2 * A * cosTheta.pow(3) + ) + val lon = atan2(y, x) + + val sinLat = sin(lat) + val n = A / sqrt(1.0 - E2 * sinLat * sinLat) + val h = p / cos(lat) - n + + return doubleArrayOf( + lat * 180.0 / Math.PI, + lon * 180.0 / Math.PI, + h + ) + } + + /** + * 以 x 轴为轴的旋转矩阵 + * + * @param ang 旋转角度(°) + * @return 旋转矩阵(3x3) + */ + private fun matX(ang: Double): Array { + val a = ang * PI_180 + val c = cos(a) + val s = sin(a) + return arrayOf( + doubleArrayOf(1.0, 0.0, 0.0), + doubleArrayOf(0.0, c, s), + doubleArrayOf(0.0, -s, c) + ) + } + + /** + * 以 y 轴为轴的旋转矩阵 + * + * @param ang 旋转角度(°) + * @return 旋转矩阵(3x3) + */ + private fun matY(ang: Double): Array { + val a = ang * PI_180 + val c = cos(a) + val s = sin(a) + return arrayOf( + doubleArrayOf(c, 0.0, -s), + doubleArrayOf(0.0, 1.0, 0.0), + doubleArrayOf(s, 0.0, c) + ) + } + + /** + * 以 z 轴为轴的旋转矩阵 + * + * @param ang 旋转角度(°) + * @return 旋转矩阵(3x3) + */ + private fun matZ(ang: Double): Array { + val a = ang * PI_180 + val c = cos(a) + val s = sin(a) + return arrayOf( + doubleArrayOf(c, s, 0.0), + doubleArrayOf(-s, c, 0.0), + doubleArrayOf(0.0, 0.0, 1.0) + ) + } + + /** + * 两个矩阵(3x3)的乘积 + * + * @param matA 3x3 矩阵 + * @param matB 3x3 矩阵 + * @return 3x3 矩阵 + */ + private fun mulMat(matA: Array, matB: Array): Array { + return Array(3) { k -> + DoubleArray(3) { j -> + (0..2).sumOf { i -> matA[k][i] * matB[i][j] } + } + } + } + + /** + * 点的旋转 + * + * @param mat 3x3 旋转矩阵 + * @param pt 旋转前坐标 [x, y, z] + * @return 旋转后坐标 [x, y, z] + */ + private fun rotate(mat: Array, pt: DoubleArray): DoubleArray { + return DoubleArray(3) { j -> + (0..2).sumOf { i -> mat[j][i] * pt[i] } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/icegps/common/helper/GeoHelper.kt b/app/src/main/java/com/icegps/common/helper/GeoHelper.kt new file mode 100644 index 0000000..7fdc7f0 --- /dev/null +++ b/app/src/main/java/com/icegps/common/helper/GeoHelper.kt @@ -0,0 +1,419 @@ +package com.icegps.common.helper + +import android.os.Parcel +import android.os.Parcelable +import kotlin.math.atan +import kotlin.math.atan2 +import kotlin.math.cbrt +import kotlin.math.cos +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.sin +import kotlin.math.sqrt +import kotlin.math.tan + +/** + * WGS84、EPSG3857、ENU 的坐标转换工具类 + * + * @author lm + * @date 2024/8/2 + */ +class GeoHelper private constructor() { + + companion object { + private var sharedInstance: GeoHelper? = null + fun getSharedInstance(): GeoHelper = sharedInstance ?: GeoHelper().also { sharedInstance = it } + fun createInstance(): GeoHelper = GeoHelper() + } + + // WGS-84 ellipsoid parameters + private val RADIUS = 6378137.0 // Major radius + private val RADIUS_B = 6356752.314245 // Minor radius + private val E = (RADIUS * RADIUS - RADIUS_B * RADIUS_B) / (RADIUS * RADIUS) // Eccentricity + private val HALF_SIZE = Math.PI * RADIUS // Half circumference of Earth + private val DEG2RAD = Math.PI / 180 // Degrees to radians conversion factor + private val RAD2DEG = 180 / Math.PI // Radians to degrees conversion factor + private val RE_WGS84 = 6378137.0 // Earth's equatorial radius in WGS84 + private val FE_WGS84 = 1.0 / 298.257223563 // Flattening of the WGS84 ellipsoid + + private var isFirstPoint = true + private var firstPoint = DoubleArray(3) + private val bPos = DoubleArray(3) + private var bECEF = DoubleArray(3) + private val rPos = DoubleArray(3) + private var rECEF = DoubleArray(3) + private val vECEF = DoubleArray(3) + + private var useBlhToEnu = true + private var blhToEnu = BlhToEnu() + + /** + * 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标 + * 如果是第一个点,它将被设置为 ENU 坐标系的基准点 + * + * @param lon 经度(度) + * @param lat 纬度(度) + * @param hgt 高度(米) + * @return 包含 ENU 坐标的 Enu 对象 + */ + fun wgs84ToENU(lon: Double, lat: Double, hgt: Double): ENU { + if (useBlhToEnu) { + val enu = blhToEnu.wgs84ToEnu(lon = lon, lat = lat, height = hgt) + return ENU(enu[0], enu[1], enu[2]) + } + + if (isFirstPoint) setEnuBenchmark(lon, lat, hgt) + rPos[0] = lat * DEG2RAD + rPos[1] = lon * DEG2RAD + rPos[2] = hgt + + rECEF = pos2ecef(rPos) + vECEF[0] = rECEF[0] - bECEF[0] + vECEF[1] = rECEF[1] - bECEF[1] + vECEF[2] = rECEF[2] - bECEF[2] + val enuDoubleArray = ecef2enu(bPos, vECEF) + return ENU(enuDoubleArray[0], enuDoubleArray[1], enuDoubleArray[2]) + } + + /** + * 将 WGS84 坐标转换为 ENU (East-North-Up) 坐标 + * 如果是第一个点,它将被设置为 ENU 坐标系的基准点 + * + * @param wgs84 WGS84 坐标对象 + * @return 包含 ENU 坐标的 Enu 对象 + */ + fun wgs84ObjectToENU(wgs84: WGS84): ENU = wgs84ToENU(wgs84.lon, wgs84.lat, wgs84.hgt) + + /** + * 是否已设置 ENU 坐标系的基准点 + */ + fun isEnuBenchmarkSet(): Boolean = !isFirstPoint + + /** + * 设置 ENU 坐标系的基准点 + * + * @param lon 基准点经度(度) + * @param lat 基准点纬度(度) + * @param hgt 基准点高度(米) + */ + private fun setEnuBenchmark(lon: Double, lat: Double, hgt: Double) { + firstPoint = doubleArrayOf(lon, lat, hgt) + bPos[0] = lat * DEG2RAD + bPos[1] = lon * DEG2RAD + bPos[2] = hgt + bECEF = pos2ecef(bPos) + isFirstPoint = false + } + + /** + * 获取 ENU 坐标系的基准点 + * + * @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray + */ + fun getEnuBenchmarkPoint(): DoubleArray { + if (useBlhToEnu) { + return doubleArrayOf(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight()) + } + return firstPoint + } + + /** + * 获取 ENU 坐标系的基准点 + * + * @return 包含 WGS84 坐标的 WGS84 对象 + */ + fun getEnuBenchmarkPointAsWGS84(): WGS84 { + if (useBlhToEnu) { + return WGS84(blhToEnu.getOriginLon(), blhToEnu.getOriginLat(), blhToEnu.getOriginHeight()) + } + return WGS84(firstPoint[0], firstPoint[1], firstPoint[2]) + } + + /** + * 重置 ENU 基准点 + * 调用此方法后,下一次 wgs84ToENU 调用将设置新的基准点 + */ + fun resetEnuBenchmarkPoint() { + if (useBlhToEnu) { + blhToEnu.resetEnuBenchmarkPoint() + return + } + isFirstPoint = true + } + + /** + * 将 ENU (East-North-Up) 坐标转换为 WGS84 坐标 + * + * @param enu 包含 ENU 坐标的 Enu 对象 + * @return 包含 WGS84 坐标 {经度, 纬度, 高度} 的 DoubleArray + */ + fun enuToWGS84(enu: ENU): DoubleArray { + if (useBlhToEnu) { + val wgs84 = blhToEnu.enuToWgs84(e = enu.x, n = enu.y, u = enu.z) + return doubleArrayOf(wgs84[1], wgs84[0], wgs84[2]) + } + + val enuArray = doubleArrayOf(enu.x, enu.y, enu.z) + val enuToEcefMatrix = xyz2enu(bPos) + val ecefArray = matmul(charArrayOf('T', 'N'), 3, 1, 3, 1.0, enuToEcefMatrix, enuArray, 0.0) + vECEF[0] = bECEF[0] + ecefArray[0] + vECEF[1] = bECEF[1] + ecefArray[1] + vECEF[2] = bECEF[2] + ecefArray[2] + return ecef2pos(vECEF) + } + + /** + * 将 ENU (East-North-Up) 坐标转换为 WGS84 坐标 + * + * @param enu 包含 ENU 坐标的 Enu 对象 + * @return 包含 WGS84 坐标的 WGS84 对象 + */ + fun enuToWGS84Object(enu: ENU): WGS84 { + val wgs84Array = enuToWGS84(enu) + return WGS84(wgs84Array[0], wgs84Array[1], wgs84Array[2]) + } + + /** + * 将 WGS84 坐标转换为 EPSG3857 坐标 + * + * @param lon 经度(度) + * @param lat 纬度(度) + * @return 包含 EPSG3857 坐标的 EPSG3857 对象 + */ + fun wgs84ToEPSG3857(lon: Double, lat: Double): EPSG3857 { + val x = lon * HALF_SIZE / 180 + var y = RADIUS * ln(tan(Math.PI * (lat + 90) / 360)) + y = y.coerceIn(-HALF_SIZE, HALF_SIZE) + return EPSG3857(x, y) + } + + /** + * 将 WGS84 坐标转换为 EPSG3857 坐标 + * + * @param wgs84 WGS84 坐标对象 + * @return 包含 EPSG3857 坐标的 EPSG3857 对象 + */ + fun wgs84ObjectToEPSG3857(wgs84: WGS84): EPSG3857 = wgs84ToEPSG3857(wgs84.lon, wgs84.lat) + + /** + * 将 EPSG3857 坐标转换为 WGS84 坐标 + * + * @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象 + * @return 包含 WGS84 坐标 {经度, 纬度} 的 DoubleArray + */ + fun epsg3857ToWGS84(epsg3857: EPSG3857): DoubleArray { + val lon = (epsg3857.x / HALF_SIZE) * 180.0 + val lat = (2 * atan(exp(epsg3857.y / RADIUS)) - Math.PI / 2) * RAD2DEG + return doubleArrayOf(lon, lat) + } + + /** + * 将 EPSG3857 坐标转换为 WGS84 坐标 + * + * @param epsg3857 包含 EPSG3857 坐标的 EPSG3857 对象 + * @return 包含 WGS84 坐标的 WGS84 对象 + */ + fun epsg3857ToWGS84Object(epsg3857: EPSG3857): WGS84 { + val wgs84Array = epsg3857ToWGS84(epsg3857) + return WGS84(wgs84Array[0], wgs84Array[1], 0.0) + } + + fun pos2ecef(pos: DoubleArray): DoubleArray { + val (lat, lon, hgt) = pos + val sinp = sin(lat) + val cosp = cos(lat) + val sin_l = sin(lon) + val cos_l = cos(lon) + val e2 = FE_WGS84 * (2.0 - FE_WGS84) + val v = RE_WGS84 / sqrt(1.0 - e2 * sinp * sinp) + + return doubleArrayOf( + (v + hgt) * cosp * cos_l, + (v + hgt) * cosp * sin_l, + (v * (1.0 - e2) + hgt) * sinp + ) + } + + fun ecef2enu(pos: DoubleArray, r: DoubleArray): DoubleArray { + val E = xyz2enu(pos) + return matmul(charArrayOf('N', 'N'), 3, 1, 3, 1.0, E, r, 0.0) + } + + fun matmul( + tr: CharArray, + n: Int, + k: Int, + m: Int, + alpha: Double, + A: DoubleArray, + B: DoubleArray, + beta: Double + ): DoubleArray { + val f = when { + tr[0] == 'N' && tr[1] == 'N' -> 1 + tr[0] == 'N' && tr[1] == 'T' -> 2 + tr[0] == 'T' && tr[1] == 'N' -> 3 + else -> 4 + } + val C = DoubleArray(n * k) + for (i in 0 until n) { + for (j in 0 until k) { + var d = 0.0 + when (f) { + 1 -> for (x in 0 until m) d += A[i + x * n] * B[x + j * m] + 2 -> for (x in 0 until m) d += A[i + x * n] * B[j + x * k] + 3 -> for (x in 0 until m) d += A[x + i * m] * B[x + j * m] + 4 -> for (x in 0 until m) d += A[x + i * m] * B[j + x * k] + } + C[i + j * n] = alpha * d + beta * C[i + j * n] + } + } + return C + } + + fun xyz2enu(pos: DoubleArray): DoubleArray { + val (lat, lon) = pos + val sinp = sin(lat) + val cosp = cos(lat) + val sin_l = sin(lon) + val cos_l = cos(lon) + + return doubleArrayOf( + -sin_l, cos_l, 0.0, + -sinp * cos_l, -sinp * sin_l, cosp, + cosp * cos_l, cosp * sin_l, sinp + ) + } + + fun ecef2pos(ecef: DoubleArray): DoubleArray { + val (x, y, z) = ecef + val a = RE_WGS84 + val b = a * (1 - FE_WGS84) + val e2 = (a * a - b * b) / (a * a) + val e2p = (a * a - b * b) / (b * b) + val r2 = x * x + y * y + val r = sqrt(r2) + val E2 = a * a - b * b + val F = 54 * b * b * z * z + val G = r2 + (1 - e2) * z * z - e2 * E2 + val c = (e2 * e2 * F * r2) / (G * G * G) + val s = cbrt(1 + c + sqrt(c * c + 2 * c)) + val P = F / (3 * (s + 1 / s + 1) * (s + 1 / s + 1) * G * G) + val Q = sqrt(1 + 2 * e2 * e2 * P) + val r0 = -(P * e2 * r) / (1 + Q) + sqrt(0.5 * a * a * (1 + 1.0 / Q) - P * (1 - e2) * z * z / (Q * (1 + Q)) - 0.5 * P * r2) + val U = sqrt((r - e2 * r0) * (r - e2 * r0) + z * z) + val V = sqrt((r - e2 * r0) * (r - e2 * r0) + (1 - e2) * z * z) + val Z0 = b * b * z / (a * V) + + val lon = atan2(y, x) * RAD2DEG + val lat = atan((z + e2p * Z0) / r) * RAD2DEG + val hgt = U * (1 - b * b / (a * V)) + return doubleArrayOf(lon, lat, hgt) + } + + data class WGS84(var lon: Double = 0.0, var lat: Double = 0.0, var hgt: Double = 0.0) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readDouble(), + parcel.readDouble(), + parcel.readDouble() + ) + + constructor(wgs84: DoubleArray) : this( + lon = wgs84.getOrElse(0) { 0.0 }, + lat = wgs84.getOrElse(1) { 0.0 }, + hgt = wgs84.getOrElse(2) { 0.0 } + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(lon) + parcel.writeDouble(lat) + parcel.writeDouble(hgt) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): WGS84 { + return WGS84(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + override fun toString(): String { + return "WGS84(lon=$lon, lat=$lat, hgt=$hgt)" + } + } + + data class EPSG3857(var x: Double = 0.0, var y: Double = 0.0) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readDouble(), + parcel.readDouble() + ) + + constructor(epsG3857: DoubleArray) : this( + x = epsG3857.getOrElse(0) { 0.0 }, + y = epsG3857.getOrElse(1) { 0.0 } + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(x) + parcel.writeDouble(y) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): EPSG3857 { + return EPSG3857(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + override fun toString(): String { + return "EPSG3857(x=$x, y=$y)" + } + } + + data class ENU(var x: Double = 0.0, var y: Double = 0.0, var z: Double = 0.0) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readDouble(), + parcel.readDouble(), + parcel.readDouble() + ) + + constructor(enu: DoubleArray) : this( + x = enu.getOrElse(0) { 0.0 }, + y = enu.getOrElse(1) { 0.0 }, + z = enu.getOrElse(2) { 0.0 } + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(x) + parcel.writeDouble(y) + parcel.writeDouble(z) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ENU { + return ENU(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + override fun toString(): String { + return "ENU(x=$x, y=$y, z=$z)" + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/icegps/geotools/GeoJsonUtils.kt b/app/src/main/java/com/icegps/geotools/GeoJsonUtils.kt new file mode 100644 index 0000000..06025c3 --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/GeoJsonUtils.kt @@ -0,0 +1,83 @@ +package com.icegps.geotools + +import com.icegps.geotools.model.IPoint +import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.LineString +import com.mapbox.geojson.Point +import com.mapbox.geojson.Polygon + +/** + * @author tabidachinokaze + * @date 2025/11/5 + */ +object GeoJsonUtils { + // 生成三角形(Polygon)FeatureCollection + fun trianglesToPolygons(delaunator: Delaunator): FeatureCollection { + val features = mutableListOf() + val tris = delaunator.triangles + // triangles 是按 3 个索引为一组三角形存储 + var i = 0 + while (i <= tris.lastIndex) { + val a = tris[i] + val b = tris[i + 1] + val c = tris[i + 2] + + val pa = delaunator.points[a] + val pb = delaunator.points[b] + val pc = delaunator.points[c] + + // Polygon 要求外环首尾闭合 + val ring = listOf( + Point.fromLngLat(pa.x, pa.y), + Point.fromLngLat(pb.x, pb.y), + Point.fromLngLat(pc.x, pc.y), + Point.fromLngLat(pa.x, pa.y) + ) + val polygon = Polygon.fromLngLats(listOf(ring)) + val feature = Feature.fromGeometry(polygon) + // 可把三角形的顶点 id/索引用作属性,方便后续交互 + feature.addNumberProperty("i0", a) + feature.addNumberProperty("i1", b) + feature.addNumberProperty("i2", c) + + features.add(feature) + i += 3 + } + return FeatureCollection.fromFeatures(features) + } + + // 生成边(LineString)FeatureCollection(每条边只输出一次) + fun trianglesToUniqueEdges(delaunator: Delaunator): FeatureCollection { + val features = mutableListOf() + val seen = HashSet>() + val tris = delaunator.triangles + var i = 0 + while (i <= tris.lastIndex) { + val a = tris[i] + val b = tris[i + 1] + val c = tris[i + 2] + val edges = listOf(Pair(a, b), Pair(b, c), Pair(c, a)) + for ((p, q) in edges) { + // 归一化顺序,避免重复(p,q)和(q,p) + val key = if (p <= q) Pair(p, q) else Pair(q, p) + if (seen.add(key)) { + val pp = delaunator.points[p] + val qq = delaunator.points[q] + val line = LineString.fromLngLats( + listOf( + Point.fromLngLat(pp.x, pp.y), + Point.fromLngLat(qq.x, qq.y) + ) + ) + val feature = Feature.fromGeometry(line) + feature.addNumberProperty("p0", p) + feature.addNumberProperty("p1", q) + features.add(feature) + } + } + i += 3 + } + return FeatureCollection.fromFeatures(features) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/icegps/geotools/GridCell.kt b/app/src/main/java/com/icegps/geotools/GridCell.kt new file mode 100644 index 0000000..2e8e3d7 --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/GridCell.kt @@ -0,0 +1,529 @@ +package com.icegps.geotools + +/** + * @author tabidachinokaze + * @date 2025/11/13 + */ +// Imports(根据你项目调整) +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import androidx.core.graphics.toColorInt +import com.icegps.geotools.model.DPoint +import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.Point +import com.mapbox.geojson.Polygon +import com.mapbox.maps.MapView +import com.mapbox.maps.extension.style.expressions.generated.Expression +import com.mapbox.maps.extension.style.layers.addLayer +import com.mapbox.maps.extension.style.layers.generated.FillLayer +import com.mapbox.maps.extension.style.layers.generated.rasterLayer +import com.mapbox.maps.extension.style.layers.properties.generated.Visibility +import com.mapbox.maps.extension.style.sources.addSource +import com.mapbox.maps.extension.style.sources.generated.ImageSource +import com.mapbox.maps.extension.style.sources.generated.geoJsonSource +import com.mapbox.maps.extension.style.sources.generated.imageSource +import com.mapbox.maps.extension.style.sources.getSourceAs +import com.mapbox.maps.extension.style.sources.updateImage +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.atan +import kotlin.math.ceil +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.max +import kotlin.math.min +import kotlin.math.tan + +// ----------------------------- +// Helper: WebMercator projection (EPSG:3857) +// ----------------------------- +fun lonToMercX(lon: Double): Double = lon * 20037508.34 / 180.0 +fun latToMercY(lat: Double): Double { + val y = ln(tan((90.0 + lat) * PI / 360.0)) / (PI / 180.0) + return y * 20037508.34 / 180.0 +} + +fun mercXToLon(x: Double): Double = x * 180.0 / 20037508.34 +fun mercYToLat(y: Double): Double { + val v = y * 180.0 / 20037508.34 + return 180.0 / PI * (2.0 * atan(exp(v * PI / 180.0)) - PI / 2.0) +} + +// ----------------------------- +// Geometry helpers +// ----------------------------- +data class Vec2(val x: Double, val y: Double) + +/** 点是否在三角形内(在 mercator 坐标系中) — 使用重心 / 矩阵法 */ +fun pointInTriangle(pt: Vec2, a: Vec2, b: Vec2, c: Vec2): Boolean { + val v0x = c.x - a.x + val v0y = c.y - a.y + val v1x = b.x - a.x + val v1y = b.y - a.y + val v2x = pt.x - a.x + val v2y = pt.y - a.y + + val dot00 = v0x * v0x + v0y * v0y + val dot01 = v0x * v1x + v0y * v1y + val dot02 = v0x * v2x + v0y * v2y + val dot11 = v1x * v1x + v1y * v1y + val dot12 = v1x * v2x + v1y * v2y + + val denom = dot00 * dot11 - dot01 * dot01 + if (denom == 0.0) return false + val invDenom = 1.0 / denom + val u = (dot11 * dot02 - dot01 * dot12) * invDenom + val v = (dot00 * dot12 - dot01 * dot02) * invDenom + return u >= 0 && v >= 0 && u + v <= 1 +} + +/** 可选:用三角形顶点值做双线性/重心内插(这里示例:按顶点值插值) + * valueAtVerts: DoubleArray of length 3 for the triangle's vertex values + */ +fun barycentricInterpolate(pt: Vec2, a: Vec2, b: Vec2, c: Vec2, values: DoubleArray): Double { + // compute areas (using cross product) as barycentric weights + val area = { p1: Vec2, p2: Vec2, p3: Vec2 -> + ((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)).absoluteValue / 2.0 + } + val areaTotal = area(a, b, c) + if (areaTotal == 0.0) return values[0] + val wA = area(pt, b, c) / areaTotal + val wB = area(pt, c, a) / areaTotal + val wC = area(pt, a, b) / areaTotal + return values[0] * wA + values[1] * wB + values[2] * wC +} + +// ----------------------------- +// 主函数:把 Delaunay 转成规则栅格(格子中心采样) +// ----------------------------- +data class GridCell(val row: Int, val col: Int, val centerLon: Double, val centerLat: Double, var value: Double? = null) + +data class GridModel( + val minLon: Double, val minLat: Double, + val maxLon: Double, val maxLat: Double, + val rows: Int, + val cols: Int, + val cellSizeMeters: Double, + val cells: Array // length rows*cols, row-major: idx = r*cols + c +) + +fun triangulationToGrid( + delaunator: Delaunator, + cellSizeMeters: Double = 50.0, // 每个格子的边长(米) + maxSidePixels: Int = 5000 // 限制 max rows/cols 防止 OOM(可选) +): GridModel { + val pts = delaunator.points + require(pts.isNotEmpty()) { "points empty" } + + // 1) bbox in lon/lat + var minLon = Double.POSITIVE_INFINITY + var maxLon = Double.NEGATIVE_INFINITY + var minLat = Double.POSITIVE_INFINITY + var maxLat = Double.NEGATIVE_INFINITY + for (p in pts) { + if (p.x < minLon) minLon = p.x + if (p.x > maxLon) maxLon = p.x + if (p.y < minLat) minLat = p.y + if (p.y > maxLat) maxLat = p.y + } + if (minLon == maxLon) { + minLon -= 0.0001; maxLon += 0.0001 + } + if (minLat == maxLat) { + minLat -= 0.0001; maxLat += 0.0001 + } + + // 2) 转为 mercator(米) + val minX = lonToMercX(minLon) + val maxX = lonToMercX(maxLon) + val minY = latToMercY(minLat) + val maxY = latToMercY(maxLat) + + val widthMeters = maxX - minX + val heightMeters = maxY - minY + + // rows/cols + var cols = ceil(widthMeters / cellSizeMeters).toInt() + var rows = ceil(heightMeters / cellSizeMeters).toInt() + + // 防止过大 + if (cols > maxSidePixels) cols = maxSidePixels + if (rows > maxSidePixels) rows = maxSidePixels + + // prepare output array + val cells = Array(rows * cols) { null } + + // 准备点/三角形在 mercator 下的缓存坐标 + val mercPts = pts.map { p -> Vec2(lonToMercX(p.x), latToMercY(p.y)) } + + // triangles 数组(每 3 个为一组) + val triIdx = delaunator.triangles + val triCount = triIdx.size / 3 + + // For potential vertex values: if you have scalar per vertex, prepare here. + // Example: create placeholder values (e.g., 0.0). Replace with your actual values if available. + val vertexValues = DoubleArray(pts.size) { 0.0 } + + // 3) iterate triangles and rasterize onto grid by checking the grid cells that intersect triangle bbox + for (ti in 0 until triCount) { + val i0 = triIdx[3 * ti] + val i1 = triIdx[3 * ti + 1] + val i2 = triIdx[3 * ti + 2] + val a = mercPts[i0] + val b = mercPts[i1] + val c = mercPts[i2] + + // triangle bbox in mercator + val tminX = minOf(a.x, b.x, c.x) + val tmaxX = maxOf(a.x, b.x, c.x) + val tminY = minOf(a.y, b.y, c.y) + val tmaxY = maxOf(a.y, b.y, c.y) + + // convert bbox to grid indices (clamp) + val colMin = ((tminX - minX) / cellSizeMeters).toInt().coerceIn(0, cols - 1) + val colMax = ((tmaxX - minX) / cellSizeMeters).toInt().coerceIn(0, cols - 1) + val rowMin = ((maxY - tmaxY) / cellSizeMeters).toInt().coerceIn(0, rows - 1) // 注意 Y 方向 + val rowMax = ((maxY - tminY) / cellSizeMeters).toInt().coerceIn(0, rows - 1) + + // optional: get vertex values for interpolation + val triVertexVals = doubleArrayOf(vertexValues[i0], vertexValues[i1], vertexValues[i2]) + + for (r in rowMin..rowMax) { + for (cIdx in colMin..colMax) { + // center of this cell in mercator + val centerX = minX + (cIdx + 0.5) * cellSizeMeters + val centerY = maxY - (r + 0.5) * cellSizeMeters + val pt = Vec2(centerX, centerY) + if (pointInTriangle(pt, a, b, c)) { + // example: set cell value as triangle index, or do interpolation + // cells index: + val idx = r * cols + cIdx + // choose value: triangle index -> convert to Double + cells[idx] = ti.toDouble() + // OR for interpolation: + // val valInterp = barycentricInterpolate(pt, a, b, c, triVertexVals) + // cells[idx] = valInterp + } + } + } + } + + // 4) produce GridModel and also convert bbox back to lon/lat for metadata + val grid = GridModel( + minLon = minLon, + minLat = minLat, + maxLon = maxLon, + maxLat = maxLat, + rows = rows, + cols = cols, + cellSizeMeters = cellSizeMeters, + cells = cells + ) + return grid +} + +// ----------------------------- +// 显示:把 GridModel 渲染成 Bitmap 并用 ImageSource 显示在 Mapbox(推荐) +// ----------------------------- +fun MapView.displayGridAsImageSource( + grid: GridModel, + testSourceId: String, + testLayerId: String, + palette: (Double?) -> Int = { v -> // 默认配色:基于三角形索引取色 + if (v == null) Color.TRANSPARENT + else { + val idx = v.toInt() + val r = (50 + (idx * 37) % 200) + val g = (80 + (idx * 61) % 150) + val b = (100 + (idx * 47) % 120) + Color.argb(220, r, g, b) + } + } +) { + mapboxMap.getStyle { style -> + val cols = grid.cols + val rows = grid.rows + + // 限制渲染大小防 OOM(可按需缩放) + val maxDim = 2048 + val width = cols.coerceAtMost(maxDim) + val height = rows.coerceAtMost(maxDim) + + // 如果 rows/cols 超过 maxDim,我们在渲染时按比例抽样(nearest neighbor) + val sampleX = cols.toDouble() / width.toDouble() + val sampleY = rows.toDouble() / height.toDouble() + + val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmp) + canvas.drawColor(Color.TRANSPARENT) + val paint = Paint().apply { + this.style = Paint.Style.FILL + } + + for (y in 0 until height) { + val srcRow = min((y * sampleY).toInt(), rows - 1) + for (x in 0 until width) { + val srcCol = min((x * sampleX).toInt(), cols - 1) + val valCell = grid.cells[srcRow * cols + srcCol] + paint.color = palette(valCell) + // draw a single pixel rect scaled to bitmap coordinates + canvas.drawRect(RectF(x.toFloat(), y.toFloat(), (x + 1).toFloat(), (y + 1).toFloat()), paint) + } + } + + // image source coords: grid bbox four corners (lon/lat) + val topLeft = listOf(grid.minLon, grid.maxLat) + val topRight = listOf(grid.maxLon, grid.maxLat) + val bottomRight = listOf(grid.maxLon, grid.minLat) + val bottomLeft = listOf(grid.minLon, grid.minLat) + val coords = listOf(topLeft, topRight, bottomRight, bottomLeft) + + if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId) + val imgSource = imageSource(testSourceId) { + coordinates(coords) + } + style.addSource(imgSource) + + // remove old layer if present + try { + style.removeStyleLayer(testLayerId) + } catch (_: Exception) { + } + val rasterLayer = rasterLayer(testLayerId, testSourceId) { + visibility(Visibility.VISIBLE) + } + style.addLayer(rasterLayer) + + // set image (updateImage or setImage) + val src = style.getSourceAs(testSourceId) + src?.updateImage(bmp) // 若你的 SDK 使用 setImage(bitmap) -> 改为 setImage + } +} + +fun MapView.displayGridAsImageSourceHighRes( + grid: GridModel, + testSourceId: String, + testLayerId: String, + maxSidePx: Int = 4096, // 根据内存调整,越大越清晰但越耗内存 + palette: (Double?) -> Int = { v -> + if (v == null) Color.TRANSPARENT else { + val idx = v.toInt() + Color.argb(220, (50 + (idx * 37) % 205), (80 + (idx * 61) % 175), (100 + (idx * 97) % 155)) + } + } +) { + mapboxMap.getStyle { style -> + val cols = grid.cols + val rows = grid.rows + if (cols <= 0 || rows <= 0) return@getStyle + + // 设备像素比(用于在高 DPI 设备上生成更清晰的位图) + val density = context.resources.displayMetrics.density // e.g. 3.0 for xxhdpi + + // target bitmap size: 尽量把每个格子映射为至少 1 device-pixel + var targetW = (cols * density).toInt() + var targetH = (rows * density).toInt() + + // 限制最大边长,防止 OOM + val scaleDown = max(1.0, max(targetW.toDouble() / maxSidePx, targetH.toDouble() / maxSidePx)) + if (scaleDown > 1.0) { + targetW = (targetW / scaleDown).toInt().coerceAtLeast(1) + targetH = (targetH / scaleDown).toInt().coerceAtLeast(1) + } + + // 如果 target 更小于 cols/rows(说明被压缩),我们先绘制到原始 colsxrows 的 bitmap 再用最近邻缩放到 target。 + // 但为了避免一次性分配超大内存,这里用两步策略: + val srcW = cols.coerceAtMost(maxSidePx) // 防止超大 + val srcH = rows.coerceAtMost(maxSidePx) + + // 创建源位图(每格一个像素的近似表示) + val srcBmp = Bitmap.createBitmap(srcW, srcH, Bitmap.Config.ARGB_8888) + val canvas = Canvas(srcBmp) + canvas.drawColor(Color.TRANSPARENT) + val paint = Paint().apply { + this.style = Paint.Style.FILL + isAntiAlias = false // 关闭抗锯齿 + isFilterBitmap = false // 关键:绘制/缩放时不使用双线性滤波 + } + + // 采样比(如果原 grid 比 src 大,则以 nearest-neighbor 采样) + val sampleX = cols.toDouble() / srcW.toDouble() + val sampleY = rows.toDouble() / srcH.toDouble() + + for (y in 0 until srcH) { + val srcRow = min((y * sampleY).toInt(), grid.rows - 1) + for (x in 0 until srcW) { + val srcCol = min((x * sampleX).toInt(), grid.cols - 1) + val v = grid.cells[srcRow * grid.cols + srcCol] + paint.color = palette(v) + // draw 1x1 rect == set single pixel + canvas.drawRect(x.toFloat(), y.toFloat(), (x + 1).toFloat(), (y + 1).toFloat(), paint) + } + } + + // 再把 srcBmp 最近邻缩放到 targetW x targetH(filter=false => nearest neighbor) + val finalBmp = if (srcW == targetW && srcH == targetH) { + srcBmp + } else { + Bitmap.createScaledBitmap(srcBmp, targetW, targetH, /*filter=*/ false) + } + + // 清理 srcBmp(若不再需要) + if (finalBmp !== srcBmp) { + srcBmp.recycle() + } + + // 把 finalBmp 传给 mapbox(imageSource coords 与之前一致) + val topLeft = listOf(grid.minLon, grid.maxLat) + val topRight = listOf(grid.maxLon, grid.maxLat) + val bottomRight = listOf(grid.maxLon, grid.minLat) + val bottomLeft = listOf(grid.minLon, grid.minLat) + val coords = listOf(topLeft, topRight, bottomRight, bottomLeft) + + if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId) + val imgSource = imageSource(testSourceId) { coordinates(coords) } + style.addSource(imgSource) + + try { + style.removeStyleLayer(testLayerId) + } catch (_: Exception) { + } + val rasterLayer = rasterLayer(testLayerId, testSourceId) { visibility(Visibility.VISIBLE) } + style.addLayer(rasterLayer) + + val src = style.getSourceAs(testSourceId) + src?.updateImage(finalBmp) // 或 setImage(finalBmp) 视 SDK 而定 + } +} + + +// ----------------------------- +// 可选:把每个格子做成 GeoJSON Polygons(每格一个 Fill)并显示(交互式,但格子多时非常慢) +// ----------------------------- +fun MapView.displayGridAsGeoJsonPolygons( + grid: GridModel, + testSourceId: String, + testLayerId: String, + palette: (Double?) -> String = { v -> + if (v == null) "#00000000" else { + val idx = v.toInt() + // 生成 hex color 例如 #RRGGBB + String.format("#%02X%02X%02X", (50 + (idx * 37) % 200), (80 + (idx * 61) % 150), (100 + (idx * 47) % 120)) + } + } +) { + mapboxMap.getStyle { style -> + val features = mutableListOf() + val minX = lonToMercX(grid.minLon) + val maxY = latToMercY(grid.maxLat) + val cellMeters = grid.cellSizeMeters + for (r in 0 until grid.rows) { + for (c in 0 until grid.cols) { + val idx = r * grid.cols + c + val v = grid.cells[idx] ?: continue + // compute four corners in mercator + val x0 = minX + c * cellMeters + val y0 = maxY - r * cellMeters + val x1 = x0 + cellMeters + val y1 = y0 - cellMeters + // to lon/lat + val lon0 = mercXToLon(x0); + val lat0 = mercYToLat(y0) + val lon1 = mercXToLon(x1); + val lat1 = mercYToLat(y1) + val ring = listOf( + Point.fromLngLat(lon0, lat0), + Point.fromLngLat(lon1, lat0), + Point.fromLngLat(lon1, lat1), + Point.fromLngLat(lon0, lat1), + Point.fromLngLat(lon0, lat0) + ) + val poly = Polygon.fromLngLats(listOf(ring)) + val f = Feature.fromGeometry(poly) + f.addStringProperty("color", palette(v)) + f.addNumberProperty("value", v) + features.add(f) + } + } + val fc = FeatureCollection.fromFeatures(features) + if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId) + style.addSource(geoJsonSource(testSourceId) { featureCollection(fc) }) + try { + style.removeStyleLayer(testLayerId) + } catch (_: Exception) { + } + val fill = FillLayer(testLayerId, testSourceId).apply { + // 使用 property 作为颜色表达式会更灵活;这里示例直接 constant color + fillColor(Expression.toColor(Expression.get("color"))) + fillOpacity(0.8) + // Note: For property-driven color you'd use expressions; kept simple here. + } + style.addLayer(fill) + } +} + +fun MapView.displayGridAsGeoJsonWithHeight( + grid: GridModel, + testSourceId: String, + testLayerId: String, + heightToColor: (Double) -> Int +) { + mapboxMap.getStyle { style -> + + val features = mutableListOf() + + // 计算每格经纬度跨度 + val deltaLon = (grid.maxLon - grid.minLon) / grid.cols + val deltaLat = (grid.maxLat - grid.minLat) / grid.rows + + for (r in 0 until grid.rows) { + for (c in 0 until grid.cols) { + val z = grid.cells[r * grid.cols + c] ?: continue + + val lon0 = grid.minLon + c * deltaLon + val lon1 = grid.minLon + (c + 1) * deltaLon + val lat0 = grid.maxLat - r * deltaLat + val lat1 = grid.maxLat - (r + 1) * deltaLat + + val ring = listOf( + Point.fromLngLat(lon0, lat0), + Point.fromLngLat(lon1, lat0), + Point.fromLngLat(lon1, lat1), + Point.fromLngLat(lon0, lat1), + Point.fromLngLat(lon0, lat0) + ) + + val poly = Polygon.fromLngLats(listOf(ring)) + val f = Feature.fromGeometry(poly) + + // 添加高度属性 + f.addNumberProperty("value", z) + + // 根据回调生成颜色 + val colorInt = heightToColor(z) + val colorStr = String.format("#%08X", colorInt) + f.addStringProperty("color", colorStr) + + features.add(f) + } + } + + val fc = FeatureCollection.fromFeatures(features) + + // 添加或更新 GeoJSON Source + if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId) + style.addSource(geoJsonSource(testSourceId) { featureCollection(fc) }) + + // 创建 FillLayer 并使用 feature.color + try { style.removeStyleLayer(testLayerId) } catch (_: Exception) {} + val fillLayer = FillLayer(testLayerId, testSourceId).apply { + fillColor(Expression.toColor(Expression.get("color"))) + fillOpacity(0.9) + } + style.addLayer(fillLayer) + } +} diff --git a/app/src/main/java/com/icegps/geotools/MainActivity.kt b/app/src/main/java/com/icegps/geotools/MainActivity.kt new file mode 100644 index 0000000..7928a91 --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/MainActivity.kt @@ -0,0 +1,594 @@ +package com.icegps.geotools + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.Config +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.drawable.BitmapDrawable +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.icegps.common.helper.GeoHelper +import com.icegps.geotools.ktx.TAG +import com.icegps.geotools.ktx.niceStr +import com.icegps.geotools.model.DPoint +import com.icegps.math.geometry.Vector3D +import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.MultiPoint +import com.mapbox.geojson.Point +import com.mapbox.geojson.Polygon +import com.mapbox.maps.CameraOptions +import com.mapbox.maps.MapView +import com.mapbox.maps.Style +import com.mapbox.maps.extension.style.expressions.generated.Expression.Companion.get +import com.mapbox.maps.extension.style.layers.addLayer +import com.mapbox.maps.extension.style.layers.addLayerBelow +import com.mapbox.maps.extension.style.layers.generated.FillLayer +import com.mapbox.maps.extension.style.layers.generated.fillLayer +import com.mapbox.maps.extension.style.layers.generated.lineLayer +import com.mapbox.maps.extension.style.layers.generated.rasterLayer +import com.mapbox.maps.extension.style.layers.generated.symbolLayer +import com.mapbox.maps.extension.style.layers.properties.generated.IconAnchor +import com.mapbox.maps.extension.style.layers.properties.generated.LineJoin +import com.mapbox.maps.extension.style.layers.properties.generated.TextAnchor +import com.mapbox.maps.extension.style.layers.properties.generated.TextJustify +import com.mapbox.maps.extension.style.layers.properties.generated.Visibility +import com.mapbox.maps.extension.style.sources.addSource +import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource +import com.mapbox.maps.extension.style.sources.generated.ImageSource +import com.mapbox.maps.extension.style.sources.generated.geoJsonSource +import com.mapbox.maps.extension.style.sources.generated.imageSource +import com.mapbox.maps.extension.style.sources.getSourceAs +import com.mapbox.maps.extension.style.sources.updateImage +import com.mapbox.maps.extension.style.style +import kotlin.math.roundToInt +import kotlin.random.Random + + +class MainActivity : AppCompatActivity() { + private lateinit var mapView: MapView + private val geoHelper = GeoHelper.getSharedInstance() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + mapView = MapView(this) + + setContentView(mapView) + initGeoHelper() + val pointsVector = coordinateGenerate() + val pointsLatLng = pointsVector.map { + val wgS84 = geoHelper.enuToWGS84Object( + GeoHelper.ENU(x = it.x, y = it.y, z = it.z) + ) + Point.fromLngLat(wgS84.lon, wgS84.lat, wgS84.hgt) + } + Log.d( + TAG, + buildString { + appendLine("enu 坐标集合") + appendLine(pointsVector.niceStr()) + } + ) + + /*val symbolLayer = SymbolLayer("point-layer", "point-source").apply { + this.iconImage("marker-icon") + }*/ + // mapView.mapboxMap.addLayer(symbolLayer) + + val delaunator = Delaunator(points = pointsLatLng.map { + DPoint(x = it.longitude(), y = it.latitude(), z = it.altitude()) + }) + val polygons = GeoJsonUtils.trianglesToPolygons(delaunator) + // 显示三角网 + if (false) mapView.loadGeoJson(delaunator) + // val rasterLayer = RasterLayer("raster-layer", sourceId = "raster-source") + if (false) mapView.loadRaterFromTin(delaunator) + + // 加载栅格网,但是没有显示出来 + if (false) mapView.loadImageTest(RasterUtils.boundingBox(pointsVector.map { com.icegps.geotools.model.Point(it.x, it.y) })) + + // 测试显示栅格图 + // mapView.loadTestRater() + mapView.loadRasterFromResource() + val testCenter = getCenter(testCoordinates2) + + // 显示三角网的各点 + if (false) mapView.mapboxMap.getStyle { style -> + val geoJsonSource = GeoJsonSource.Builder(sourceId = "geojson-source") + .feature( + value = Feature.fromGeometry( + MultiPoint.fromLngLats(pointsLatLng) + ) + ) + // .featureCollection(polygons) + .build() + style.addSource(geoJsonSource) + // style.addLayer(FillLayer("geojson-layer", "geojson-source")) + style.addImage( + "marker-icon", + getBitmapFromDrawable(R.drawable.ic_pile_marker) + ) + val symbol = symbolLayer("symbol-layer", "geojson-source") { + // filter(eq(get("geometry.type"), literal("Point"))) + iconImage("marker-icon") + iconAllowOverlap(true) + iconAnchor(IconAnchor.BOTTOM) + textField(get("name")) // 显示 properties.name + textAnchor(TextAnchor.TOP) + textJustify(TextJustify.CENTER) + } + style.addLayer(symbol) + } + // 显示三角网的另一种方式 + if (false) mapView.showVoronoiAsPolygons( + delaunator = delaunator, + testSourceId = "polygons-source-id-0", + testLayerId = "polygons-layer-id-0" + ) + if (false) mapView.rasterizeDelaunayToMap( + delaunator = delaunator, + testSourceId = "polygons-source-id-1", + testLayerId = "polygons-layer-id-1", + ) + // 显示栅格模型 + val gridModel = triangulationToGrid( + delaunator = delaunator, + cellSizeMeters = 2.0, + maxSidePixels = 6553500 + ) + val palette: (Double?) -> Int = { v -> + if (v == null) Color.MAGENTA else { // null 显 magenta 便于确认 + val idx = v.toInt() + Color.rgb((idx * 37) and 0xFF, (idx * 61) and 0xFF, (idx * 97) and 0xFF) + } + } + if (false) mapView.displayGridAsImageSourceHighRes( + grid = gridModel, + testSourceId = "raster-source-id-1", + testLayerId = "raster-layer-id-1", + ) + if (true) mapView.displayGridAsGeoJsonPolygons( + grid = gridModel, + testSourceId = "raster-source-id-0", + testLayerId = "raster-layer-id-0", + ) + + mapView.mapboxMap.setCamera( + CameraOptions.Builder() + //.center(pointsLatLng.first()) + //.center(testCenter) + .center(delaunator.points.first().let { Point.fromLngLat(it.x, it.y) }) + .pitch(0.0) + .zoom(18.0) + .bearing(0.0) + .build() + ) + } +} + +fun initGeoHelper(base: DPoint = DPoint(114.476060, 22.771073, 30.897)) { + val geoHelper = GeoHelper.getSharedInstance() + geoHelper.wgs84ToENU( + lon = base.x, + lat = base.y, + hgt = base.z + ) +} + +fun coordinateGenerate(): List { + val dPoints = (0..60).map { + Vector3D(x, y, z) + } + return dPoints +} + +val minX = -20.0 +val maxX = 20.0 +val minY = -20.0 +val maxY = 20.0 +val minZ = -20.0 +val maxZ = 20.0 + +val x: Double get() = Random.nextDouble(minX, maxX) +val y: Double get() = Random.nextDouble(minY, maxY) +val z: Double get() = Random.nextDouble(minZ, maxZ) + +fun Context.getBitmapFromDrawable(drawableResId: Int): Bitmap { + val drawable = ContextCompat.getDrawable(this, drawableResId)!! + if (drawable is BitmapDrawable) { + // 如果本身就是位图,直接返回 + return drawable.bitmap + } + + // 如果是 VectorDrawable 或其他类型,需手动转 Bitmap + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth.takeIf { it > 0 } ?: 1, + drawable.intrinsicHeight.takeIf { it > 0 } ?: 1, + Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap +} + +// 在 Activity/Fragment 中 +fun MapView.loadGeoJson(delaunator: Delaunator) { + mapboxMap.getStyle { style -> + // 1) 构建 FeatureCollection(选择边或三角形) + + val edgesFc = GeoJsonUtils.trianglesToUniqueEdges(delaunator) // 或 trianglesToPolygons(delaunator) + + // 2) 添加 GeoJsonSource(id = "triangles-source") + val source = GeoJsonSource.Builder("triangles-source") + .featureCollection(edgesFc) + .build() + style.addSource(source) + + // 3) 添加 LineLayer 渲染三角网边 + + val line = lineLayer("triangles-line-layer", "triangles-source") { + lineWidth(1.5) + lineJoin(LineJoin.ROUND) + lineOpacity(1.0) + // 可以通过 expression 使用属性动态着色,这里演示静态颜色: + lineColor("#ff0000") + } + style.addLayer(line) + + // 可选:如果你用 polygons 并想填充三角形 + val fill = fillLayer("triangles-fill-layer", "triangles-source") { + fillOpacity(0.2) + fillColor("#00FF00") + } + style.addLayerBelow(fill, "triangles-line-layer") + } +} + +private const val ID_IMAGE_SOURCE = "image_source-id" +private const val ID_IMAGE_LAYER = "image_layer-id" +fun MapView.loadImageTest(bbox: BoundingBox) { + this.mapboxMap.loadStyle( + style(style = Style.DARK) { + +imageSource(ID_IMAGE_SOURCE) { + val bboxCoordinates: List> = listOf( + listOf(bbox.minX, bbox.maxY), // top-left (lon, lat) + listOf(bbox.maxX, bbox.maxY), // top-right + listOf(bbox.maxX, bbox.minY), // bottom-right + listOf(bbox.minX, bbox.minY) // bottom-left + ) + coordinates(bboxCoordinates) + } + +rasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE) {} + } + ) { + context.getBitmapFromDrawable(R.drawable.ic_launcher_background).let { bitmap -> + val imageSource: ImageSource? = it.getSourceAs(ID_IMAGE_SOURCE) + imageSource?.updateImage(bitmap) + } + } +} + +fun MapView.loadRaterFromTin(delaunator: Delaunator) { + // add ImageSource + delaunator.points + val bbox = RasterUtils.boundingBox(delaunator.points) + val (floats, bitmap) = RasterUtils.rasterizeDelaunay( + delaunator = delaunator, + minX = bbox.minX, + minY = bbox.minY, + maxX = bbox.maxX, + maxY = bbox.maxY, + cols = 10, + rows = 10, + valueGetter = { it.z } + ) + val bboxCoordinates: List> = listOf( + listOf(minX, maxY), // top-left (lon, lat) + listOf(maxX, maxY), // top-right + listOf(maxX, minY), // bottom-right + listOf(minX, minY) // bottom-left + ) + + mapboxMap.getStyle { style -> +// 如果已经存在同 id 的 source/layer,先移除(避免重复添加) + val sourceId = "raster-image-source" + val layerId = "raster-layer" + try { + if (style.styleSourceExists(sourceId)) { + style.removeStyleSource(sourceId) + } + if (style.styleLayerExists(layerId)) { + style.removeStyleLayer(layerId) + } + } catch (e: Exception) { + // ignore + } + + // add ImageSource(直接传入 bitmap) + val imageSrc = imageSource(sourceId) { + coordinates(bboxCoordinates) + } + style.addSource(imageSrc) + + // add RasterLayer 显示 image source + val raster = rasterLayer(layerId, sourceId) { + rasterOpacity(0.9) + } + style.addLayer(raster) + } +} + +val testSourceId = "test-raster-image-source" +val testLayerId = "test-rater-layer" +val testCoordinates = listOf( + listOf(-80.425, 46.437), + listOf(-71.516, 46.437), + listOf(-71.516, 37.936), + listOf(-80.425, 37.936) +) +val testCoordinates2 = listOf( + listOf(-122.5233, 37.7091), // 左上 + listOf(-122.3566, 37.7091), // 右上 + listOf(-122.3566, 37.8120), // 右下 + listOf(-122.5233, 37.8120) // 左下 +) + +fun getCenter(coordinates: List>): Point? { + val minX = coordinates.minOf { it[0] } + val maxX = coordinates.maxOf { it[0] } + val minY = coordinates.minOf { it[1] } + val maxY = coordinates.maxOf { it[1] } + val centerX = (minX + maxX) / 2 + val centerY = (minY + maxY) / 2 + return Point.fromLngLat(centerX, centerY) +} + +fun MapView.loadTestRater() { + mapboxMap.getStyle { style -> + // 示例 4 个角点(经度, 纬度),按左Top、右Top、右Bottom、左Bottom 顺序 + val coords = testCoordinates2 + + val imageSource = imageSource( + id = testSourceId, + block = { + url("https://docs.mapbox.com/mapbox-gl-js/assets/radar.gif") + coordinates(coords) + } + ) + + // 添加 source(如果已有同 id 的 source 会导致异常,必要时先移除) + if (style.styleSourceExists(testSourceId)) { + style.removeStyleSource(testSourceId) + } + style.addSource(imageSource) + + // 创建 raster layer,类型为 raster,sourceId 必须和上面一致 + val rasterLayer = rasterLayer( + layerId = testLayerId, + sourceId = testSourceId, + block = { + // 可选:设置初始透明度或可见性 + visibility(Visibility.VISIBLE) + rasterOpacity(1.0) + } + ) + + // 把 layer 插入在合适位置:例如放在 "water" 之上,或放到最顶层 + // 如果你不知道在哪放,先加到最顶层: + style.addLayer(rasterLayer) + // 或者 style.addLayerAbove(rasterLayer, "water") 之类的 + } +} + +fun MapView.loadRasterFromResource() { + mapboxMap.getStyle { style -> + val coords = testCoordinates2 + + // 第一步:创建空的 ImageSource(仅带坐标) + val imageSource = imageSource(testSourceId) { + coordinates(coords) + } + + // 如果之前存在旧的同名 source,移除 + if (style.styleSourceExists(testSourceId)) { + style.removeStyleSource(testSourceId) + } + style.addSource(imageSource) + + // 第二步:创建 raster 图层 + val rasterLayer = rasterLayer( + layerId = testLayerId, + sourceId = testSourceId + ) { + visibility(Visibility.VISIBLE) + } + style.addLayer(rasterLayer) + + // 第三步:加载本地 Bitmap 并更新到 source + val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.voroni4) + style.getSourceAs(testSourceId)?.updateImage(bitmap) + } +} + +@Deprecated("显示效果不对") +fun MapView.showVoronoiAsPolygons( + delaunator: Delaunator, + testSourceId: String, + testLayerId: String +) { + mapboxMap.getStyle { style -> + // 假定 getVoronoiCells() 返回 VoronoiCell(index, points) + val features = mutableListOf() + for (cell in delaunator.getVoronoiCells()) { + // cell.points 是 IPoint 的列表,按顺序构成多边形 + val coords = cell.points.map { Point.fromLngLat(it.x, it.y) } + // GeoJSON polygon 要求首尾点相同 + val ring = coords.toMutableList() + if (ring.first() != ring.last()) ring.add(ring.first()) + val polygon = Polygon.fromLngLats(listOf(ring)) + val f = Feature.fromGeometry(polygon) + f.addNumberProperty("index", cell.index) + features.add(f) + } + val fc = FeatureCollection.fromFeatures(features) + if (style.styleSourceExists(testSourceId)) style.removeStyleSource(testSourceId) + style.addSource(geoJsonSource(testSourceId) { + featureCollection(fc) + }) + + // 添加 fill layer + if (style.styleLayerExists(testLayerId)) style.removeStyleLayer(testLayerId) + val fillLayer = FillLayer(testLayerId, testSourceId).apply { + // 这里用表达式或常量颜色,也可按 property 着色 + fillOpacity(0.7) + fillColor("#ff6600") + } + style.addLayer(fillLayer) + } +} + +/** + * rasterizeDelaunayToMap: + * - delaunator: 提供三角网(points + triangles) + * - pixelsPerDegree: 控制生成的栅格分辨率(每经度多少像素),或直接给 width/height + */ +fun MapView.rasterizeDelaunayToMap( + delaunator: Delaunator, + testSourceId: String, + testLayerId: String, + pixelsPerDegree: Double = 400.0 // 可调:值越大图片越精细、越大 +) { + mapboxMap.getStyle { style -> + val pts = delaunator.points + if (pts.isEmpty()) return@getStyle + + // 1) 计算经纬包围箱 + var minLon = Double.MAX_VALUE + var maxLon = -Double.MAX_VALUE + var minLat = Double.MAX_VALUE + var maxLat = -Double.MAX_VALUE + for (p in pts) { + if (p.x < minLon) minLon = p.x + if (p.x > maxLon) maxLon = p.x + if (p.y < minLat) minLat = p.y + if (p.y > maxLat) maxLat = p.y + } + // 防止 deg=0 的情况 + if (minLon == maxLon) { + minLon -= 0.0001; maxLon += 0.0001 + } + if (minLat == maxLat) { + minLat -= 0.0001; maxLat += 0.0001 + } + + val lonSpan = maxLon - minLon + val latSpan = maxLat - minLat + + // 2) 决定 bitmap 尺寸(可以用 pixelsPerDegree 或直接固定大小) + val width = (pixelsPerDegree * lonSpan).roundToInt().coerceAtLeast(64) + val height = (pixelsPerDegree * latSpan).roundToInt().coerceAtLeast(64) + + // 3) 创建 bitmap 和 canvas + val bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888) + val canvas = Canvas(bitmap) + canvas.drawColor(Color.TRANSPARENT) + + val paintFill = Paint(Paint.ANTI_ALIAS_FLAG).apply { + this.style = Paint.Style.FILL + } + val paintStroke = Paint(Paint.ANTI_ALIAS_FLAG).apply { + this.style = Paint.Style.STROKE + strokeWidth = 1f + color = Color.BLACK + } + + // helper: 经纬 -> 像素 + fun lon2x(lon: Double): Float { + val rel = (lon - minLon) / lonSpan + return (rel * (width - 1)).toFloat() + } + + fun lat2y(lat: Double): Float { + // 像素 y 从上到下,纬度从上(大)到下(小);所以用 (maxLat - lat) + val rel = (maxLat - lat) / latSpan + return (rel * (height - 1)).toFloat() + } + + // 4) 遍历三角形: delaunator.triangles 是索引数组(三元组按顺序) + val tris = delaunator.triangles + if (tris.size % 3 != 0) { + // 非常规情况:不处理 + } + val triCount = tris.size / 3 + for (i in 0 until triCount) { + val i0 = tris[3 * i] + val i1 = tris[3 * i + 1] + val i2 = tris[3 * i + 2] + val p0 = pts[i0] + val p1 = pts[i1] + val p2 = pts[i2] + + val path = Path().apply { + moveTo(lon2x(p0.x), lat2y(p0.y)) + lineTo(lon2x(p1.x), lat2y(p1.y)) + lineTo(lon2x(p2.x), lat2y(p2.y)) + close() + } + + // 你可以自定义着色函数:例如基于点索引、面积、属性等 + // 下面示例:按 index 渐变色(仅示例) + val color = colorFromIndex(i) + paintFill.color = color + canvas.drawPath(path, paintFill) + canvas.drawPath(path, paintStroke) + } + + // 5) 在 Mapbox 上创建 imageSource(四角经纬) + val topLeft = listOf(minLon, maxLat) + val topRight = listOf(maxLon, maxLat) + val bottomRight = listOf(maxLon, minLat) + val bottomLeft = listOf(minLon, minLat) + val coords = listOf(topLeft, topRight, bottomRight, bottomLeft) + + // remove old source if exists + if (style.styleSourceExists(testSourceId)) { + style.removeStyleSource(testSourceId) + } + val imgSource = imageSource(testSourceId) { + coordinates(coords) + } + style.addSource(imgSource) + + // add layer + val rasterLayer = rasterLayer(testLayerId, testSourceId) { + visibility(Visibility.VISIBLE) + } + // remove old layer if present + try { + style.removeStyleLayer(testLayerId) + } catch (_: Exception) { + } + style.addLayer(rasterLayer) + + // 6) 把 bitmap 更新到 source(多数新版叫 updateImage) + val src = style.getSourceAs(testSourceId) + src?.updateImage(bitmap) // 或 src?.setImage(bitmap) 视 SDK 而定 + } +} + +/** 简单的着色函数:按 index 生成颜色(示例,不必准确) */ +fun colorFromIndex(i: Int): Int { + // 生成一些可视颜色 + val r = (50 + (i * 37) % 200) + val g = (80 + (i * 61) % 150) + val b = (100 + (i * 47) % 120) + return Color.argb(200, r, g, b) +} + diff --git a/app/src/main/java/com/icegps/geotools/RasterUtils.kt b/app/src/main/java/com/icegps/geotools/RasterUtils.kt new file mode 100644 index 0000000..5ff8e77 --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/RasterUtils.kt @@ -0,0 +1,165 @@ +package com.icegps.geotools + +import android.graphics.Bitmap +import com.icegps.geotools.model.IPoint + +data class BoundingBox( + val minX: Double, + val minY: Double, + val maxX: Double, + val maxY: Double, +) + +/** + * @author tabidachinokaze + * @date 2025/11/5 + */ +object RasterUtils { + + fun boundingBox(points: List): BoundingBox { + return BoundingBox( + minX = points.minOf { it.x }, + maxX = points.maxOf { it.x }, + minY = points.minOf { it.y }, + maxY = points.maxOf { it.y }, + ) + } + + /** + * Rasterize a Delaunator mesh into a regular grid. + * + * @param delaunator your Delaunator instance (points are in map coordinates, e.g., lon/lat) + * @param minX left bound (coordinate) + * @param minY bottom bound (coordinate) + * @param maxX right bound + * @param maxY top bound + * @param cols number of columns (width) of the raster + * @param rows number of rows (height) of the raster + * @param valueGetter a function that given a point index returns the value (Double) to rasterize (e.g. elevation) + * @param noDataValue value for cells not covered by any triangle (default = Float.NaN) + * @return Pair(FloatArray(values row-major), Bitmap visualization) + */ + fun rasterizeDelaunay( + delaunator: Delaunator, + minX: Double, + minY: Double, + maxX: Double, + maxY: Double, + cols: Int, + rows: Int, + valueGetter: (T) -> Double, + noDataValue: Float = Float.NaN + ): Pair { + + require(cols > 0 && rows > 0) + // grid: row-major, index = row * cols + col + val raster = FloatArray(cols * rows) { noDataValue } + + val cellWidth = (maxX - minX) / cols.toDouble() + val cellHeight = (maxY - minY) / rows.toDouble() + + // helper: convert grid coords to world coordinates (center of cell) + fun centerX(col: Int) = minX + (col + 0.5) * cellWidth + fun centerY(row: Int) = maxY - (row + 0.5) * cellHeight // top->down rows + + // For performance, iterate triangles, compute their bbox in grid coords, then fill pixels inside. + val tris = delaunator.triangles + var t = 0 + while (t <= tris.lastIndex) { + val ia = tris[t] + val ib = tris[t + 1] + val ic = tris[t + 2] + val pa = delaunator.points[ia] + val pb = delaunator.points[ib] + val pc = delaunator.points[ic] + + // triangle bbox in world coords + val triMinX = minOf(pa.x, pb.x, pc.x) + val triMaxX = maxOf(pa.x, pb.x, pc.x) + val triMinY = minOf(pa.y, pb.y, pc.y) + val triMaxY = maxOf(pa.y, pb.y, pc.y) + + // map bbox to grid index range (clamp to raster) + val minCol = (((triMinX - minX) / cellWidth).toInt()).coerceIn(0, cols - 1) + val maxCol = (((triMaxX - minX) / cellWidth).toInt()).coerceIn(0, cols - 1) + val minRow = (((maxY - triMaxY) / cellHeight).toInt()).coerceIn(0, rows - 1) + val maxRow = (((maxY - triMinY) / cellHeight).toInt()).coerceIn(0, rows - 1) + + // Precompute values at vertices + val va = valueGetter(pa) + val vb = valueGetter(pb) + val vc = valueGetter(pc) + + // Precompute for barycentric / edge function method + // We'll compute barycentric weights using area method + val x0 = pa.x; + val y0 = pa.y + val x1 = pb.x; + val y1 = pb.y + val x2 = pc.x; + val y2 = pc.y + + // area * 2 + val denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2) + // if denom == 0 -> degenerate triangle, skip + if (kotlin.math.abs(denom) < 1e-15) { + t += 3 + continue + } + + for (row in minRow..maxRow) { + val cy = centerY(row) + for (col in minCol..maxCol) { + val cx = centerX(col) + + // compute barycentric coordinates (l1,l2,l3) for point (cx,cy) + val l1 = ((y1 - y2) * (cx - x2) + (x2 - x1) * (cy - y2)) / denom + val l2 = ((y2 - y0) * (cx - x2) + (x0 - x2) * (cy - y2)) / denom + val l3 = 1.0 - l1 - l2 + + // check if inside triangle (allow tiny negative eps) + if (l1 >= -1e-8 && l2 >= -1e-8 && l3 >= -1e-8) { + val value = l1 * va + l2 * vb + l3 * vc + raster[row * cols + col] = value.toFloat() + } + } + } + + t += 3 + } + + // Create a simple grayscale Bitmap for visualization (normalize values ignoring NaN) + var minV = Double.POSITIVE_INFINITY + var maxV = Double.NEGATIVE_INFINITY + for (v in raster) { + if (!v.isNaN()) { + if (v < minV) minV = v.toDouble() + if (v > maxV) maxV = v.toDouble() + } + } + if (minV == Double.POSITIVE_INFINITY || maxV == Double.NEGATIVE_INFINITY) { + // all nodata + minV = 0.0 + maxV = 1.0 + } + val bmp = Bitmap.createBitmap(cols, rows, Bitmap.Config.ARGB_8888) + for (r in 0 until rows) { + for (c in 0 until cols) { + val v = raster[r * cols + c] + val color = if (v.isNaN()) { + // transparent for nodata + 0x00000000 + } else { + val norm = ((v - minV) / (maxV - minV)).coerceIn(0.0, 1.0) + val gray = (norm * 255.0).toInt() + // ARGB + (0xFF shl 24) or (gray shl 16) or (gray shl 8) or gray + } + bmp.setPixel(c, r, color) + } + } + + return Pair(raster, bmp) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/icegps/geotools/ktx/Any.kt b/app/src/main/java/com/icegps/geotools/ktx/Any.kt new file mode 100644 index 0000000..03cfb8d --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/ktx/Any.kt @@ -0,0 +1,7 @@ +package com.icegps.geotools.ktx + +/** + * @author tabidachinokaze + * @date 2025/11/5 + */ +val Any.TAG: String get() = this::class.java.simpleName diff --git a/app/src/main/java/com/icegps/geotools/ktx/Vector3D.kt b/app/src/main/java/com/icegps/geotools/ktx/Vector3D.kt new file mode 100644 index 0000000..4be01b7 --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/ktx/Vector3D.kt @@ -0,0 +1,17 @@ +package com.icegps.geotools.ktx + +import com.icegps.math.geometry.Vector3D + +/** + * @author tabidachinokaze + * @date 2025/11/5 + */ +fun Vector3D.niceStr(): String { + return "[$x, $y, $z]".format(this) +} + +fun List.niceStr(): String { + return joinToString(", ", "[", "]") { + it.niceStr() + } +} diff --git a/app/src/main/java/com/icegps/geotools/model/DPoint.kt b/app/src/main/java/com/icegps/geotools/model/DPoint.kt new file mode 100644 index 0000000..0be50ee --- /dev/null +++ b/app/src/main/java/com/icegps/geotools/model/DPoint.kt @@ -0,0 +1,11 @@ +package com.icegps.geotools.model + +/** + * @author tabidachinokaze + * @date 2025/11/5 + */ +data class DPoint( + override var x: Double, + override var y: Double, + var z: Double +) : IPoint diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pile_marker.xml b/app/src/main/res/drawable/ic_pile_marker.xml new file mode 100644 index 0000000..df50811 --- /dev/null +++ b/app/src/main/res/drawable/ic_pile_marker.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/test_radar.png b/app/src/main/res/drawable/test_radar.png new file mode 100644 index 0000000000000000000000000000000000000000..c6e5fe19e7f55087208b9e789e8ef7971bb96f4b GIT binary patch literal 29053 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�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 + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmWIYbaN|VWnc(*bqWXzu!!JdU|xt`5!v_jjvkEam13D%HG@k-IMc zNLB6Ts|;5bzSwJe>ULbl1cvf&ZkhaCoSq2>@AZ5-D>jj1lIT2H&dag$gn7N+e4L{u z^V=qU9`n4<|KGj+Jg1Vc;+*Ym8}@ms^PajGxhz`fUcGqB^ot%&0-(G6 ztJqa}H}n3HU3Yip`SGVeoN_0jHpk{$^4G>a6%8p%x7&Z)J5^*02Bzg(emT zh6~~ha~ZWf7(5v43`IJe7AY!6)06s|5LTRZ~a|9(#3-?A(9e&YZ4?>X2s?GW>o|33SDelz`B z`+wn|Q;!w?=CKR)$n9qK&JB=|i?B8=Za(_yby{8ahEC_FQc?|fX6jq-S#{;T*P>@x z_ZT?env#-TMm+56J zYxbSv3`wunSg8L0QM8}=O*rp?-VOhM|NEg<)7%*xHvh-siA*kcCjMS>GB?P(>_@}3 z)Ft&xoDRNt&3eFh)jsaGt6V1Y-utsGX3D{BPrGiMe#bFC=cA;8JMEoLi`oHXxI)DE^oHMbkaEEw% z-9tKtyZ;ro&wXg4x@O8`m1O~q z?MexGg12&nX4y|S@pnFV^V}u9giQ-GT$3$KnU6R18RkeC3%K&!v-YvBIofAmw>PFs zIdS)#VNole5AMDC;{Wfgucb?`mgwZOH*=)K)xFxh zvAbsbOYcV^yis0yb^(2L-iLG^EU>@ZcYlA->$iWR*2l$qNu~2Y6n&DlA@j?-eeQ>s z$tnK&Z05FhmCbjTD+dy9O%PYyrL7Vlbht#oNZ2QS;ASZ@?QSi@KIA`)Q{Plar-EBMn zOue{8S#RsoI9{I8-1%wjYL3ra-WXN>myuB|k`TLaRw0(}Q{v*)neU%|Uf=%kE$@XZ zhG9pKE&f~)a@RGze*OGS1~2BsylnCL*)X3i|99J|eC4QK#}-~?>BH&Aafd zWjZOh{NVc@|Mx8WeQ&M$PqIskKh9jN|JQv<&;0)q`yZ~E&-DGzd;15MW#|73p4FDw z7Oym+LiWGhsn`4Zdv7t_;1XiVJF+=y-{rn5k2YR9d}BwU-%Zx7^dl3uQO%=KtB6@_8OJ`?s99PuHc(Wv{Uvn57cQnzH^! z4YT&W4>v_49d0g>;5}1QsqLw*bguIK-eRRko_V#EY72aobT&L-pQO|$YAra)UWXy) z?H0HH>?%h$pNjLSyTHFTTZD1@o5kYirK&#Ospl5Bs^WBcdditu$Df4zc4P@8o+&!k z$5~=Hz27y&v^)8mahg-|JA1KhUV`UTT02EQ$+(1e7MN)IY?yFdc~#TV8Gl98qIT3X z<+w`qp4Rl)QQ*p5k{WN6_9(e-vDU^NGYy%0D%Eo$*=nm_Iu+cp7Fo9EyM*zH8S<-( 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 AkN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 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

( 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..9287f5083623b375139afb391af71cc533a7dd37 GIT binary patch literal 5914 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

0vl^(ubK57i3$j`RXVdItc2Zu zTjP%3T{qqDyHu$1wf>en`}f$lI?l)hfy+`aCTxu8`Mp4Wt8~lzsk7fIKUzdbU&D@u>@#|8JoKt2mbv8|~*&2E%ct_*EIJ>)7o&PKE5_$1H zYTdgu^~m!0A4{%YZ`c&qZ)$yr|AXm!sqXjcKR?_Pn|Ao^jNS9)1$Q|wns@3F%fsvg zwSO(SHf}Wd&gY@5dZETG=E3!B4m$1-}j)^|n6tq;mOd3k8c0DH_GHdj+bC#}?wC!#j7mQS#vf_fXiT$x+$0-Ud(~5MTubmb>WqqsT;k{xpsIQhc5wmzv^leMXZ>rUpkvod!E z%4X!{CT@DfUb}1l9;OF%&cZj$=~`?izExW$`!W#MmrU&bU9x zY4?Y|ihvD`aVe?!Jd*bn>a_Ljw%A+cxQM63#5tgD>7pvIfk zXZB`A(+B?|xK;mhgAxy6j8m5a|tYYQtz-&)`wciH>w!z4Jl6EbdLNgW&t4n&Z^`r` zg&vWF3I>a?I;mM9pQdx?h+g`4^#9q@#m5&cUv`wUW67(N(f8cqnde#^*{|z4`#8&% z$(*MTuC#KwaPVof`>D`&U$yg@H74@YWB+XNzOk9HzHI)?S*96(9jadGnl+RhV?HYU zTPymOU!2^Y9Y;SYsn1QEpStG70rM^O4*D71J+(bX{#n9KLDg&XW+cq#d%E33PUWv_ zkfOTshO5hBt#1C8*W^yL{vnXQVRqiLErQMrj6U1<_5@j|Em*A4E4-te^ZzpYDI0B` zXg;x>w9{g(u&mK%{^Fz8n{xBlOZuI;!E!86S+BwIO2Xl&Fh%#ZoBUQU_O0g4%ANk> z0Jmq=?VG;}VuQcWs7ieBJAdXY(fhi`7ADvReRQ>AUivA=PwM}&`iLJJJJZ{_&k3Y2 zUvhnu->Tx7bEg!!9%6moQp;o7lU20XJ=QDEK<4}Dx`6F-nzAlROmUdnwpX%JBgyHE&EJd_^1F8lZE|Tb`4Z9o=_5<>!KzQ} zGZoDj*B>l-_{cMi-TF`Qmi1X-=QLK8d3<{luV|^Id8CphQgh85osBxngzXY9@84_h z%f`4XHqy%Sk8jJOIRCXp`#HYZ9tjb8r7sFp_47S~Y8LMZ0|ACD*T9*?vNtsq>c#FZ;4AF83!N-}7Zh7R+l= zw!4tMaFcX!P3Els8SK9LLhRyJK z+&t%0aQM50`k&8S%~Uwp?rm21|LFNWlV+G(ACydR-+6qUWz*kv3iZ{C%If{&>euW) z@@W5$w|~FeitEW*v$L+4y5z_58%Bq(G3{FX-GAnci|2Se?q;?>GnLW&F(+=#|9Z#w z-#_o;-&VrrqOwwO%B-^WHLF&}EjZj>w|%nOHqFIdHlY~LM|@kt^cB?v?aRN!JSJaHjLeW zb$GNz$NM?U8z(oNo3`r8e%lS3CEre$zVUyZ&{wym*PqV*QMTjIf~G4kJUQ0C+9_cC zLSfq{(=7&@u0G04(^)DtZ};(To{cq(j|-+htN*jEpuxFk0?Ru8XP3^z-&SNkURt^2 zTSfZcN5x^>ML|ky1rH}oViJ7+@cetDleV!hBksSwEn~4q@Xz`GmCrjHOK)GEd0p-B z(Y6SS8poDMuE(=E|4rGmcvp9MsGHU9V8)Ek>HE*irk%F9rK7RU-2N}yiz$uUO57Qx zXRAv!tdNVQd!7^~xNg;>=ojmFGC8tun)m$oP3PKhc*onS@K5?rr^}yu zUmUrRUESJJ=>F0}m;TMP>i;SjwwR~)_nQ*mc_z*-WwSXs6&fs_bqPz9s|2|0xg0$2 t@u$Pda=rYAtrOikX7%&GRsG&;Z1Lh{&yL;Ao6ROYUN$pxVgmyL003>zdZqvX literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..9126ae37cbc3587421d6889eadd1d91fbf1994d4 GIT binary patch literal 7778 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..6c56317 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/mapbox_access_token.xml b/app/src/main/res/values/mapbox_access_token.xml new file mode 100644 index 0000000..1a4ff3a --- /dev/null +++ b/app/src/main/res/values/mapbox_access_token.xml @@ -0,0 +1,4 @@ + + + pk.eyJ1IjoienpxMSIsImEiOiJjbWYzbzV1MzQwMHJvMmpvbG1wbjJwdjUyIn0.LvKjIrCv9dAFcGxOM52f2Q + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..8a54742 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + geotools + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..115c362 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/icegps/geotools/ExampleUnitTest.kt b/app/src/test/java/com/icegps/geotools/ExampleUnitTest.kt new file mode 100644 index 0000000..f7addbd --- /dev/null +++ b/app/src/test/java/com/icegps/geotools/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.icegps.geotools + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..fc94b22 --- /dev/null +++ b/build.gradle @@ -0,0 +1,7 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.android.library) apply false +} \ No newline at end of file diff --git a/delaunator/.gitignore b/delaunator/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/delaunator/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/delaunator/build.gradle b/delaunator/build.gradle new file mode 100644 index 0000000..84b0201 --- /dev/null +++ b/delaunator/build.gradle @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + namespace 'com.icegps.geotools' + compileSdk 35 + + defaultConfig { + minSdk 28 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + + implementation libs.androidx.core.ktx + implementation libs.androidx.appcompat + implementation libs.material + testImplementation libs.junit + androidTestImplementation libs.androidx.junit + androidTestImplementation libs.androidx.espresso.core +} \ No newline at end of file diff --git a/delaunator/consumer-rules.pro b/delaunator/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/delaunator/proguard-rules.pro b/delaunator/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/delaunator/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/delaunator/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt b/delaunator/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..bff94b7 --- /dev/null +++ b/delaunator/src/androidTest/java/com/icegps/geotools/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.icegps.geotools + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.icegps.geotools.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/delaunator/src/main/AndroidManifest.xml b/delaunator/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/delaunator/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/Delaunator.kt b/delaunator/src/main/java/com/icegps/geotools/Delaunator.kt new file mode 100644 index 0000000..7c9294f --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/Delaunator.kt @@ -0,0 +1,625 @@ +package com.icegps.geotools + +import com.icegps.geotools.model.Edge +import com.icegps.geotools.model.IEdge +import com.icegps.geotools.model.IPoint +import com.icegps.geotools.model.Point +import com.icegps.geotools.model.VoronoiCell +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.pow +import kotlin.math.sqrt + +interface IDelaunator { + val points: List + var triangles: Array + var halfedges: Array + fun getHullEdges(): List + fun getVoronoiCells(): Sequence + fun getEdges(): Sequence +} + +class Delaunator(override val points: List) : IDelaunator { + + private val EPSILON = 2.0.pow(-52.0) + private val edgeStack = Array(512) { 0 } + + override var triangles: Array + override var halfedges: Array + + private val hashSize: Int + private val hullPrev: MutableList + private val hullNext: MutableList + private val hullTri: MutableList + private val hullHash: Array + + private var cx: Double + private var cy: Double + + private var trianglesLen: Int + private val coords: Array + private var hullStart: Int + private var hullSize: Int + private val hull: Array + + + init { + if (points.size < 3) { + throw IndexOutOfBoundsException("Need at least 3 points") + } + + coords = Array(points.size * 2) { .0 } + + points.forEachIndexed { index, point -> + coords[2 * index] = point.x + coords[2 * index + 1] = point.y + } + + val n = coords.size shr 1 + val maxTriangles = 2 * n - 5 + + triangles = Array(maxTriangles * 3) { 0 } + + halfedges = Array(maxTriangles * 3) { 0 } + hashSize = ceil(sqrt(n.toDouble())).toInt() + + hullPrev = MutableList(n) { 0 } + hullNext = MutableList(n) { 0 } + hullTri = MutableList(n) { 0 } + hullHash = Array(hashSize) { 0 } + + val ids = Array(n) { 0 } + + var minX = Double.POSITIVE_INFINITY + var minY = Double.POSITIVE_INFINITY + var maxX = Double.POSITIVE_INFINITY + var maxY = Double.POSITIVE_INFINITY + + for (i in 0 until n) { + val x = coords[2 * i] + val y = coords[2 * i + 1] + if (x < minX) minX = x + if (y < minY) minY = y + if (x > maxX) maxX = x + if (y > maxY) maxY = y + ids[i] = i + } + + val cx = (minX + maxX) / 2 + val cy = (minY + maxY) / 2 + + var minDist = Double.POSITIVE_INFINITY + var i0 = 0 + var i1 = 0 + var i2 = 0 + + // pick a seed point close to the center + for (i in 0 until n) { + val d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]) + if (d < minDist) { + i0 = i + minDist = d + } + } + val i0x = coords[2 * i0] + val i0y = coords[2 * i0 + 1] + + minDist = Double.POSITIVE_INFINITY + + // find the point closest to the seed + for (i in 0 until n) { + if (i == i0) continue + val d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]) + if (d < minDist && d > 0) { + i1 = i + minDist = d + } + } + + var i1x = coords[2 * i1] + var i1y = coords[2 * i1 + 1] + + var minRadius = Double.POSITIVE_INFINITY + + // find the third point which forms the smallest circumcircle with the first two + for (i in 0 until n) { + if (i == i0 || i == i1) continue + val r = circumRadius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]) + if (r < minRadius) { + i2 = i + minRadius = r + } + } + var i2x = coords[2 * i2] + var i2y = coords[2 * i2 + 1] + + if (minRadius == Double.POSITIVE_INFINITY) { + throw Exception("No Delaunay triangulation exists for this input.") + } + + if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) { + val i = i1 + val x = i1x + val y = i1y + i1 = i2 + i1x = i2x + i1y = i2y + i2 = i + i2x = x + i2y = y + } + + val center = circumCenter(i0x, i0y, i1x, i1y, i2x, i2y) + this.cx = center.x + this.cy = center.y + + val dists = Array(n) { i -> + dist(coords[2 * i], coords[2 * i + 1], center.x, center.y) + } + + // sort the points by distance from the seed triangle circumcenter + quicksort(ids, dists, 0, n - 1) + + // set up the seed triangle as the starting hull + hullStart = i0 + hullSize = 3 + + hullPrev[i2] = i1 + hullNext[i0] = i1 + hullPrev[i0] = i2 + hullNext[i1] = i2 + hullPrev[i1] = i0 + hullNext[i2] = i0 + + hullTri[i0] = 0 + hullTri[i1] = 1 + hullTri[i2] = 2 + + hullHash[hashKey(i0x, i0y)] = i0 + hullHash[hashKey(i1x, i1y)] = i1 + hullHash[hashKey(i2x, i2y)] = i2 + + trianglesLen = 0 + addTriangle(i0, i1, i2, -1, -1, -1) + + var xp = .0 + var yp = .0 + + for (k in ids.indices) { + val i = ids[k] + val x = coords[2 * i] + val y = coords[2 * i + 1] + + // skip near-duplicate points + if (k > 0 && abs(x - xp) <= EPSILON && abs(y - yp) <= EPSILON) continue + xp = x + yp = y + + // skip seed triangle points + if (i == i0 || i == i1 || i == i2) continue + + // find a visible edge on the convex hull using edge hash + var start = 0 + for (j in 0 until hashSize) { + val key = hashKey(x, y) + start = hullHash[(key + j) % hashSize] + if (start != -1 && start != hullNext[start]) break + } + + + start = hullPrev[start] + var e = start + var q = hullNext[e] + + while (!orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1])) { + e = q + if (e == start) { + e = Int.MAX_VALUE + break + } + + q = hullNext[e] + } + + if (e == Int.MAX_VALUE) continue // likely a near-duplicate point; skip it + + // add the first triangle from the point + var t = addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]) + + // recursively flip triangles from the point until they satisfy the Delaunay condition + hullTri[i] = legalize(t + 2) + hullTri[e] = t // keep track of boundary triangles on the hull + hullSize++ + + // walk forward through the hull, adding more triangles and flipping recursively + var next = hullNext[e] + q = hullNext[next] + + while (orient(x, y, coords[2 * next], coords[2 * next + 1], coords[2 * q], coords[2 * q + 1])) { + t = addTriangle(next, i, q, hullTri[i], -1, hullTri[next]) + hullTri[i] = legalize(t + 2) + hullNext[next] = next // mark as removed + hullSize-- + next = q + + q = hullNext[next] + } + + // walk backward from the other side, adding more triangles and flipping + if (e == start) { + q = hullPrev[e] + + while (orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1])) { + t = addTriangle(q, i, e, -1, hullTri[e], hullTri[q]) + legalize(t + 2) + hullTri[q] = t + hullNext[e] = e // mark as removed + hullSize-- + e = q + + q = hullPrev[e] + } + } + + // update the hull indices + hullPrev[i] = e + hullStart = e + hullPrev[next] = i + hullNext[e] = i + hullNext[i] = next + + // save the two new edges in the hash table + hullHash[hashKey(x, y)] = i + hullHash[hashKey(coords[2 * e], coords[2 * e + 1])] = e + } + + hull = Array(hullSize) { 0 } + var s = hullStart + for (i in 0 until hullSize) { + hull[i] = s + s = hullNext[s] + } + + // get rid of temporary arrays + hullPrev.clear() + hullNext.clear() + hullTri.clear() + + //// trim typed triangle mesh arrays + triangles = triangles.take(trianglesLen).toTypedArray() + halfedges = halfedges.take(trianglesLen).toTypedArray() + } + + private fun hashKey(x: Double, y: Double): Int { + return (floor(pseudoAngle(x - cx, y - cy) * hashSize) % hashSize).toInt() + } + + private fun pseudoAngle(dx: Double, dy: Double): Double { + val p = dx / (abs(dx) + abs(dy)) + return (if (dy > 0) 3 - p else 1 + p) / 4 // [0..1] + } + + private fun legalize(index: Int): Int { + var a = index + var i = 0 + var ar: Int + + // recursion eliminated with a fixed-size stack + while (true) { + val b = halfedges[a] + + /* if the pair of triangles doesn't satisfy the Delaunay condition + * (p1 is inside the circumcircle of [p0, pl, pr]), flip them, + * then do the same check/flip recursively for the new pair of triangles + * + * pl pl + * /||\ / \ + * al/ || \bl al/ \a + * / || \ / \ + * / a||b \ flip /___ar___\ + * p0\ || /p1 => p0\---bl---/p1 + * \ || / \ / + * ar\ || /br b\ /br + * \||/ \ / + * pr pr + */ + val a0 = a - a % 3 + ar = a0 + (a + 2) % 3 + + if (b == -1) { // convex hull edge + if (i == 0) break + a = edgeStack[--i] + continue + } + + val b0 = b - b % 3 + val al = a0 + (a + 1) % 3 + val bl = b0 + (b + 2) % 3 + + val p0 = triangles[ar] + val pr = triangles[a] + val pl = triangles[al] + val p1 = triangles[bl] + + val illegal = inCircle( + coords[2 * p0], coords[2 * p0 + 1], + coords[2 * pr], coords[2 * pr + 1], + coords[2 * pl], coords[2 * pl + 1], + coords[2 * p1], coords[2 * p1 + 1] + ) + + if (illegal) { + triangles[a] = p1 + triangles[b] = p0 + + val hbl = halfedges[bl] + + // edge swapped on the other side of the hull (rare); fix the halfedge reference + if (hbl == -1) { + var e = hullStart + do { + if (hullTri[e] == bl) { + hullTri[e] = a + break + } + e = hullNext[e] + } while (e != hullStart) + } + link(a, hbl) + link(b, halfedges[ar]) + link(ar, bl) + + val br = b0 + (b + 1) % 3 + + // don't worry about hitting the cap: it can only happen on extremely degenerate input + if (i < edgeStack.size) { + edgeStack[i++] = br + } + } else { + if (i == 0) break + a = edgeStack[--i] + } + } + + return ar + } + + private fun inCircle( + ax: Double, + ay: Double, + bx: Double, + by: Double, + cx: Double, + cy: Double, + px: Double, + py: Double + ): Boolean { + val dx = ax - px + val dy = ay - py + val ex = bx - px + val ey = by - py + val fx = cx - px + val fy = cy - py + val ap = dx * dx + dy * dy + val bp = ex * ex + ey * ey + val cp = fx * fx + fy * fy + return dx * (ey * cp - bp * fy) - + dy * (ex * cp - bp * fx) + + ap * (ex * fy - ey * fx) < 0 + } + + private fun link(a: Int, b: Int) { + halfedges[a] = b + if (b != -1) halfedges[b] = a + } + + private fun circumRadius( + ax: Double, + ay: Double, + bx: Double, + by: Double, + cx: Double, + cy: Double + ): Double { + val dx = bx - ax + val dy = by - ay + val ex = cx - ax + val ey = cy - ay + val bl = dx * dx + dy * dy + val cl = ex * ex + ey * ey + val d = 0.5 / (dx * ey - dy * ex) + val x = (ey * bl - dy * cl) * d + val y = (dx * cl - ex * bl) * d + return x * x + y * y + } + + private fun quicksort(ids: Array, dists: Array, left: Int, right: Int) { + if (right - left <= 20) { + for (i in left + 1..right) { + val temp = ids[i] + val tempDist = dists[temp] + var j = i - 1 + while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--] + ids[j + 1] = temp + } + } else { + val median = left + right shr 1 + var i = left + 1 + var j = right + swap(ids, median, i) + if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right) + if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right) + if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i) + val temp = ids[i] + val tempDist = dists[temp] + while (true) { + do i++ while (dists[ids[i]] < tempDist) + do j-- while (dists[ids[j]] > tempDist) + if (j < i) break + swap(ids, i, j) + } + ids[left + 1] = ids[j] + ids[j] = temp + if (right - i + 1 >= j - left) { + quicksort(ids, dists, i, right) + quicksort(ids, dists, left, j - 1) + } else { + quicksort(ids, dists, left, j - 1) + quicksort(ids, dists, i, right) + } + } + } + + private fun swap(arr: Array, i: Int, j: Int) { + val tmp = arr[i] + arr[i] = arr[j] + arr[j] = tmp + } + + private fun circumCenter( + ax: Double, + ay: Double, + bx: Double, + by: Double, + cx: Double, + cy: Double + ): Point { + val dx = bx - ax + val dy = by - ay + val ex = cx - ax + val ey = cy - ay + val bl = dx * dx + dy * dy + val cl = ex * ex + ey * ey + val d = 0.5 / (dx * ey - dy * ex) + val x = ax + (ey * bl - dy * cl) * d + val y = ay + (dx * cl - ex * bl) * d + return Point(x, y) + } + + private fun orient(px: Double, py: Double, qx: Double, qy: Double, rx: Double, ry: Double): Boolean { + return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) < 0 + } + + private fun addTriangle(i0: Int, i1: Int, i2: Int, a: Int, b: Int, c: Int): Int { + val t = trianglesLen + triangles[t] = i0 + triangles[t + 1] = i1 + triangles[t + 2] = i2 + link(t, a) + link(t + 1, b) + link(t + 2, c) + trianglesLen += 3 + return t + } + + private fun dist(ax: Double, ay: Double, bx: Double, by: Double): Double { + val dx = ax - bx + val dy = ay - by + return dx * dx + dy * dy + } + + private fun createHull(points: List): List { + return points.mapIndexed { index: Int, point: T -> + if (points.lastIndex == index) { + Edge(0, point, points.first()) + } else { + Edge(0, point, points[index + 1]) + } + } + } + + private fun getHullPoints(): List { + return hull.map { x -> points[x] } + } + + override fun getHullEdges(): List { + return createHull(getHullPoints()) + } + + override fun getVoronoiCells(): Sequence { + return sequence { + val seen = HashSet() // of point ids + for (triangleId in triangles.indices) { + val id = triangles[nextHalfedgeIndex(triangleId)] + if (!seen.contains(id)) { + seen.add(id) + val edges = edgesAroundPoint(triangleId) + val triangles = edges.map { x -> triangleOfEdge(x) } + val vertices = triangles.map { x -> getTriangleCenter(x) } + yield(VoronoiCell(id, vertices.toList())) + } + } + } + } + + private fun getTriangleCenter(t: Int): IPoint { + val vertices = getTrianglePoints(t) + return getCentroid(vertices) + } + + private fun getCentroid(points: List): IPoint { + + var accumulatedArea = 0.0 + var centerX = 0.0 + var centerY = 0.0 + var j = points.size - 1 + for (i in points.indices) { + val temp = points[i].x * points[j].y - points[j].x * points[i].y + accumulatedArea += temp + centerX += (points[i].x + points[j].x) * temp + centerY += (points[i].y + points[j].y) * temp + j = i + } + + accumulatedArea *= 3.0 + return Point( + centerX / accumulatedArea, + centerY / accumulatedArea + ) + } + + private fun getTrianglePoints(t: Int): List { + return pointsOfTriangle(t).map { p -> points[p] } + } + + private fun pointsOfTriangle(t: Int): List { + return edgesOfTriangle(t).map { e -> triangles[e] } + } + + private fun edgesOfTriangle(t: Int): List { + return listOf(3 * t, 3 * t + 1, 3 * t + 2) + } + + private fun triangleOfEdge(e: Int): Int { + return floor(e / 3.0).toInt() + } + + private fun edgesAroundPoint(start: Int): Sequence { + return sequence { + var incoming = start + do { + yield(incoming) + val outgoing = nextHalfedgeIndex(incoming) + incoming = halfedges[outgoing] + } while (incoming != -1 && incoming != start) + } + } + + private fun nextHalfedgeIndex(e: Int): Int { + return if (e % 3 == 2) e - 2 else e + 1 + } + + override fun getEdges(): Sequence { + return sequence { + for (e in triangles.indices) { + if (e > halfedges[e]) { + val p = points[triangles[e]] + val q = points[triangles[nextHalfedgeIndex(e)]] + yield(Edge(e, p, q)) + } + } + } + } + +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/Edge.kt b/delaunator/src/main/java/com/icegps/geotools/model/Edge.kt new file mode 100644 index 0000000..70d8a6c --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/Edge.kt @@ -0,0 +1,7 @@ +package com.icegps.geotools.model + +class Edge( + override val index: Int, + override val p: IPoint, + override val q: IPoint +) : IEdge \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/IEdge.kt b/delaunator/src/main/java/com/icegps/geotools/model/IEdge.kt new file mode 100644 index 0000000..0b0c73c --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/IEdge.kt @@ -0,0 +1,7 @@ +package com.icegps.geotools.model + +interface IEdge { + val p: IPoint + val q: IPoint + val index: Int +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/IPoint.kt b/delaunator/src/main/java/com/icegps/geotools/model/IPoint.kt new file mode 100644 index 0000000..1215d00 --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/IPoint.kt @@ -0,0 +1,6 @@ +package com.icegps.geotools.model + +interface IPoint { + var x: Double + var y: Double +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/ITriangle.kt b/delaunator/src/main/java/com/icegps/geotools/model/ITriangle.kt new file mode 100644 index 0000000..4e29f10 --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/ITriangle.kt @@ -0,0 +1,6 @@ +package com.icegps.geotools.model + +interface ITriangle { + val points: List + val Index: Int +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/IVoronoiCell.kt b/delaunator/src/main/java/com/icegps/geotools/model/IVoronoiCell.kt new file mode 100644 index 0000000..db20781 --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/IVoronoiCell.kt @@ -0,0 +1,6 @@ +package com.icegps.geotools.model + +interface IVoronoiCell { + val points: List + val index: Int +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/Point.kt b/delaunator/src/main/java/com/icegps/geotools/model/Point.kt new file mode 100644 index 0000000..c20ec9e --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/Point.kt @@ -0,0 +1,19 @@ +package com.icegps.geotools.model + +data class Point(override var x: Double, override var y: Double) : IPoint { + + override fun toString() = "{$x},{$y}" + + operator fun minus(other: Point): Point { + return Point(x - other.x, y - other.y) + } + + operator fun plus(other: Point): Point { + return Point(x + other.x, y + other.y) + } + + operator fun div(other: Int): Point { + return Point(x / other, y / other) + } + +} \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/Triangle.kt b/delaunator/src/main/java/com/icegps/geotools/model/Triangle.kt new file mode 100644 index 0000000..0d6545b --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/Triangle.kt @@ -0,0 +1,6 @@ +package com.icegps.geotools.model + +class Triangle( + override val points: List, + override val Index: Int +) : ITriangle \ No newline at end of file diff --git a/delaunator/src/main/java/com/icegps/geotools/model/VoronoiCell.kt b/delaunator/src/main/java/com/icegps/geotools/model/VoronoiCell.kt new file mode 100644 index 0000000..e9c23f3 --- /dev/null +++ b/delaunator/src/main/java/com/icegps/geotools/model/VoronoiCell.kt @@ -0,0 +1,6 @@ +package com.icegps.geotools.model + +class VoronoiCell( + override val index: Int, + override val points: List +) : IVoronoiCell \ No newline at end of file diff --git a/delaunator/src/test/java/com/icegps/geotools/ExampleUnitTest.kt b/delaunator/src/test/java/com/icegps/geotools/ExampleUnitTest.kt new file mode 100644 index 0000000..f7addbd --- /dev/null +++ b/delaunator/src/test/java/com/icegps/geotools/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.icegps.geotools + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..7446f83 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +agp = "8.10.1" +kotlin = "2.0.21" +coreKtx = "1.17.0" +junit = "4.13.2" +junitVersion = "1.3.0" +espressoCore = "3.7.0" +appcompat = "1.7.1" +material = "1.12.0" +activity = "1.11.0" +constraintlayout = "2.2.1" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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 YhOdRl$_CP6#$d+qN{E5M`3i^!03mLhYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..77d3bbd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Nov 05 13:47:37 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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="-Xmx64m" "-Xms64m" + +@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 execute + +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 execute + +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 + +: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 %* + +: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/math/.gitignore b/math/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/math/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/math/build.gradle b/math/build.gradle new file mode 100644 index 0000000..00aa9cf --- /dev/null +++ b/math/build.gradle @@ -0,0 +1,17 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlin.jvm) +} +java { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 +} +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_1_8 + } +} +dependencies { + testImplementation libs.kotlin.test +} \ No newline at end of file diff --git a/math/src/main/java/com/icegps/io/util/NumberExt.kt b/math/src/main/java/com/icegps/io/util/NumberExt.kt new file mode 100644 index 0000000..a44f689 --- /dev/null +++ b/math/src/main/java/com/icegps/io/util/NumberExt.kt @@ -0,0 +1,75 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.icegps.io.util + +import kotlin.math.* + +//private fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this +private val MINUS_ZERO_D = -0.0 +private fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this + +fun Double.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String { + if (this.isNanOrInfinite()) return this.toString() + + //val bits = this.toRawBits() + //val sign = (bits ushr 63) != 0L + //val exponent = (bits ushr 52) and 0b11111111111 + //val fraction = bits and ((1L shl 52) - 1L) + + val res = this.roundDecimalPlaces(decimalPlaces).normalizeZero().toString() + + val eup = res.indexOf('E') + val elo = res.indexOf('e') + val eIndex = if (eup >= 0) eup else elo + val rez = if (eIndex >= 0) { + val base = res.substring(0, eIndex) + val exp = res.substring(eIndex + 1).toInt() + val rbase = if (base.contains(".")) base else "$base.0" + val zeros = "0".repeat(exp.absoluteValue + 2) + val part = if (exp > 0) "$rbase$zeros" else "$zeros$rbase" + val pointIndex2 = part.indexOf(".") + val pointIndex = if (pointIndex2 < 0) part.length else pointIndex2 + val outIndex = pointIndex + exp + val part2 = part.replace(".", "") + buildString { + if ((0 until outIndex).all { part2[it] == '0' }) { + append('0') + } else { + append(part2, 0, outIndex) + } + append('.') + append(part2, outIndex, part2.length) + } + } else { + res + } + + val pointIndex = rez.indexOf('.') + val integral = if (pointIndex >= 0) rez.substring(0, pointIndex) else rez + if (decimalPlaces == 0) return integral + + val decimal = if (pointIndex >= 0) rez.substring(pointIndex + 1).trimEnd('0') else "" + return buildString(2 + integral.length + decimalPlaces) { + append(integral) + if (decimal.isNotEmpty() || !skipTrailingZeros) { + val decimalCount = min(decimal.length, decimalPlaces) + val allZeros = (0 until decimalCount).all { decimal[it] == '0' } + if (!skipTrailingZeros || !allZeros) { + append('.') + append(decimal, 0, decimalCount) + if (!skipTrailingZeros) repeat(decimalPlaces - decimalCount) { append('0') } + } + } + } +} + +fun Float.toStringDecimal(decimalPlaces: Int, skipTrailingZeros: Boolean = false): String = this.toDouble().toStringDecimal(decimalPlaces, skipTrailingZeros) + +private fun Double.roundDecimalPlaces(places: Int): Double { + if (places < 0) return this + val placesFactor: Double = 10.0.pow(places.toDouble()) + return round(this * placesFactor) / placesFactor +} + +private fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite() +private fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite() diff --git a/math/src/main/java/com/icegps/io/util/NumberParser.kt b/math/src/main/java/com/icegps/io/util/NumberParser.kt new file mode 100644 index 0000000..c9cab61 --- /dev/null +++ b/math/src/main/java/com/icegps/io/util/NumberParser.kt @@ -0,0 +1,78 @@ +package com.icegps.io.util + +import kotlin.math.* + +object NumberParser { + const val END = '\u0000' + + fun parseInt(str: String, start: Int = 0, end: Int = str.length, radix: Int = 10): Int { + var n = start + return parseInt(radix) { if (n >= end) END else str[n++] } + } + + fun parseDouble(str: String, start: Int = 0, end: Int = str.length): Double { + var n = start + return parseDouble { if (n >= end) END else str[n++] } + } + + inline fun parseInt(radix: Int = 10, gen: (Int) -> Char): Int { + var positive = true + var out = 0 + var n = 0 + while (true) { + val c = gen(n++) + if (c == END) break + if (c == '-' || c == '+') { + positive = (c == '+') + } else { + val value = c.ctypeAsInt() + if (value < 0) break + out *= radix + out += value + } + } + return if (positive) out else -out + } + + inline fun parseDouble(gen: (Int) -> Char): Double { + var out = 0.0 + var frac = 1.0 + var pointSeen = false + var eSeen = false + var negate = false + var negateExponent = false + var exponent = 0 + var n = 0 + while (true) { + val c = gen(n++) + if (c == END) break + when (c) { + 'e', 'E' -> eSeen = true + '-' -> { + if (eSeen) negateExponent = true else negate = true + } + '.' -> pointSeen = true + else -> { + if (eSeen) { + exponent *= 10 + exponent += c.ctypeAsInt() + } else { + if (pointSeen) frac /= 10 + out *= 10 + out += c.ctypeAsInt() + } + } + } + } + val res = (out * frac) * 10.0.pow(if (negateExponent) -exponent else exponent) + return if (negate) -res else res + } +} + +@Suppress("ConvertTwoComparisonsToRangeCheck") // @TODO: Kotlin-Native doesn't optimize ranges +@PublishedApi internal fun Char.ctypeAsInt(): Int = when { + this >= '0' && this <= '9' -> this - '0' + this >= 'a' && this <= 'z' -> this - 'a' + 10 + this >= 'A' && this <= 'Z' -> this - 'A' + 10 + else -> -1 +} diff --git a/math/src/main/java/com/icegps/math/Alignment.kt b/math/src/main/java/com/icegps/math/Alignment.kt new file mode 100644 index 0000000..28b2eb1 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Alignment.kt @@ -0,0 +1,47 @@ +package com.icegps.math + +import kotlin.math.absoluteValue + +//////////////////// +//////////////////// + +/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Int.nextAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align) +/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Long.nextAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else (((this / align) + 1) * align) +/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Float.nextAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align) +/** Returns the next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Double.nextAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else (((this / align).toInt() + 1) * align) + +/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Int.prevAlignedTo(align: Int): Int = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align +/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Long.prevAlignedTo(align: Long): Long = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align +/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Float.prevAlignedTo(align: Float): Float = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align +/** Returns the previous value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Double.prevAlignedTo(align: Double): Double = if (this.isAlignedTo(align)) this else nextAlignedTo(align) - align + +/** Returns whether [this] is multiple of [alignment] */ +public fun Int.isAlignedTo(alignment: Int): Boolean = alignment == 0 || (this % alignment) == 0 +/** Returns whether [this] is multiple of [alignment] */ +public fun Long.isAlignedTo(alignment: Long): Boolean = alignment == 0L || (this % alignment) == 0L +/** Returns whether [this] is multiple of [alignment] */ +public fun Float.isAlignedTo(alignment: Float): Boolean = alignment == 0f || (this % alignment) == 0f +/** Returns whether [this] is multiple of [alignment] */ +public fun Double.isAlignedTo(alignment: Double): Boolean = alignment == 0.0 || (this % alignment) == 0.0 + +/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Float.nearestAlignedTo(align: Float): Float { + val prev = this.prevAlignedTo(align) + val next = this.nextAlignedTo(align) + return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next +} +/** Returns the previous or next value of [this] that is multiple of [align]. If [this] is already multiple, returns itself. */ +public fun Double.nearestAlignedTo(align: Double): Double { + val prev = this.prevAlignedTo(align) + val next = this.nextAlignedTo(align) + return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next +} + diff --git a/math/src/main/java/com/icegps/math/BooleanConversion.kt b/math/src/main/java/com/icegps/math/BooleanConversion.kt new file mode 100644 index 0000000..9327a26 --- /dev/null +++ b/math/src/main/java/com/icegps/math/BooleanConversion.kt @@ -0,0 +1,9 @@ +package com.icegps.math + +//////////////////// +//////////////////// + +/** Converts this [Boolean] into integer: 1 for true, 0 for false */ +inline fun Boolean.toInt(): Int = if (this) 1 else 0 +inline fun Boolean.toByte(): Byte = if (this) 1 else 0 +inline fun Byte.toBoolean(): Boolean = this.toInt() != 0 diff --git a/math/src/main/java/com/icegps/math/Clamp.kt b/math/src/main/java/com/icegps/math/Clamp.kt new file mode 100644 index 0000000..c631771 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Clamp.kt @@ -0,0 +1,38 @@ +package com.icegps.math + +/** Clamps [this] value into the range [min] and [max] */ +fun Int.clamp(min: Int, max: Int): Int = if (this < min) min else if (this > max) max else this +/** Clamps [this] value into the range [min] and [max] */ +fun Long.clamp(min: Long, max: Long): Long = if (this < min) min else if (this > max) max else this +/** Clamps [this] value into the range [min] and [max] */ +fun Double.clamp(min: Double, max: Double): Double = if (this < min) min else if (this > max) max else this +/** Clamps [this] value into the range [min] and [max] */ +fun Float.clamp(min: Float, max: Float): Float = if ((this < min)) min else if ((this > max)) max else this + +/** Clamps [this] value into the range 0 and 1 */ +fun Double.clamp01(): Double = clamp(0.0, 1.0) +/** Clamps [this] value into the range 0 and 1 */ +fun Float.clamp01(): Float = clamp(0f, 1f) + +/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int]. The default parameters will cover the whole range of values. */ +fun Long.toIntClamp(min: Int = Int.MIN_VALUE, max: Int = Int.MAX_VALUE): Int { + if (this < min) return min + if (this > max) return max + return this.toInt() +} + +/** Clamps [this] [Long] value into the range [min] and [max] converting it into [Int] (where [min] must be zero or positive). The default parameters will cover the whole range of positive and zero values. */ +fun Long.toUintClamp(min: Int = 0, max: Int = Int.MAX_VALUE): Int = this.toIntClamp(min, max) + +/** Clamps the integer value in the 0..255 range */ +fun Int.clampUByte(): Int { + val n = this and -(if (this >= 0) 1 else 0) + return (n or (0xFF - n shr 31)) and 0xFF +} +fun Int.clampUShort(): Int { + val n = this and -(if (this >= 0) 1 else 0) + return (n or (0xFFFF - n shr 31)) and 0xFFFF +} + +fun Int.toShortClamped(): Short = this.clamp(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() +fun Int.toByteClamped(): Byte = this.clamp(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte() diff --git a/math/src/main/java/com/icegps/math/ConvertRange.kt b/math/src/main/java/com/icegps/math/ConvertRange.kt new file mode 100644 index 0000000..864c799 --- /dev/null +++ b/math/src/main/java/com/icegps/math/ConvertRange.kt @@ -0,0 +1,24 @@ +package com.icegps.math + +//////////////////// +//////////////////// + +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */ +fun Float.convertRange(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin))) +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */ +fun Double.convertRange(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = (dstMin + (dstMax - dstMin) * ((this - srcMin) / (srcMax - srcMin))) +//fun Double.convertRange(minSrc: Double, maxSrc: Double, minDst: Double, maxDst: Double): Double = (((this - minSrc) / (maxSrc - minSrc)) * (maxDst - minDst)) + minDst +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */ +fun Int.convertRange(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toInt() +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be outside the destination range */ +fun Long.convertRange(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = (dstMin + (dstMax - dstMin) * ((this - srcMin).toDouble() / (srcMax - srcMin).toDouble())).toLong() + +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */ +fun Float.convertRangeClamped(srcMin: Float, srcMax: Float, dstMin: Float, dstMax: Float): Float = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax) +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */ +fun Double.convertRangeClamped(srcMin: Double, srcMax: Double, dstMin: Double, dstMax: Double): Double = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax) +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */ +fun Int.convertRangeClamped(srcMin: Int, srcMax: Int, dstMin: Int, dstMax: Int): Int = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax) +/** Converts this value considering it was in the range [srcMin]..[srcMax] into [dstMin]..[dstMax], if the value is not inside the range the output value will be clamped to the nearest bound */ +fun Long.convertRangeClamped(srcMin: Long, srcMax: Long, dstMin: Long, dstMax: Long): Long = convertRange(srcMin, srcMax, dstMin, dstMax).clamp(dstMin, dstMax) + diff --git a/math/src/main/java/com/icegps/math/Division.kt b/math/src/main/java/com/icegps/math/Division.kt new file mode 100644 index 0000000..3599449 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Division.kt @@ -0,0 +1,19 @@ +package com.icegps.math + +import kotlin.math.roundToInt + +//////////////////// +//////////////////// + +/** Divides [this] into [that] rounding to the floor */ +public infix fun Int.divFloor(that: Int): Int = this / that +/** Divides [this] into [that] rounding to the ceil */ +public infix fun Int.divCeil(that: Int): Int = if (this % that != 0) (this / that) + 1 else (this / that) +/** Divides [this] into [that] rounding to the round */ +public infix fun Int.divRound(that: Int): Int = (this.toDouble() / that.toDouble()).roundToInt() + +public infix fun Long.divCeil(other: Long): Long { + val res = this / other + if (this % other != 0L) return res + 1 + return res +} diff --git a/math/src/main/java/com/icegps/math/Fract.kt b/math/src/main/java/com/icegps/math/Fract.kt new file mode 100644 index 0000000..3f9185c --- /dev/null +++ b/math/src/main/java/com/icegps/math/Fract.kt @@ -0,0 +1,4 @@ +package com.icegps.math + +public inline fun fract(value: Float): Float = value - value.toIntFloor() +public inline fun fract(value: Double): Double = value - value.toIntFloor() diff --git a/math/src/main/java/com/icegps/math/ILog.kt b/math/src/main/java/com/icegps/math/ILog.kt new file mode 100644 index 0000000..0ca6c55 --- /dev/null +++ b/math/src/main/java/com/icegps/math/ILog.kt @@ -0,0 +1,9 @@ +package com.icegps.math + +//////////////////// +//////////////////// + +/** Performs a fast integral logarithmic of base two */ +fun ilog2(v: Int): Int = if (v == 0) (-1) else (31 - v.countLeadingZeroBits()) +// fun ilog2(v: Int): Int = kotlin.math.log2(v.toDouble()).toInt() +fun ilog2Ceil(v: Int): Int = kotlin.math.ceil(kotlin.math.log2(v.toDouble())).toInt() diff --git a/math/src/main/java/com/icegps/math/IsAlmostEquals.kt b/math/src/main/java/com/icegps/math/IsAlmostEquals.kt new file mode 100644 index 0000000..1ff18a9 --- /dev/null +++ b/math/src/main/java/com/icegps/math/IsAlmostEquals.kt @@ -0,0 +1,14 @@ +package com.icegps.math + +import kotlin.math.* + +interface IsAlmostEquals { + fun isAlmostEquals(other: T, epsilon: Double = 0.000001): Boolean +} + +interface IsAlmostEqualsF { + fun isAlmostEquals(other: T, epsilon: Float = 0.0001f): Boolean +} + +fun Float.isAlmostEquals(other: Float, epsilon: Float = 0.000001f): Boolean = (this - other).absoluteValue < epsilon +fun Double.isAlmostEquals(other: Double, epsilon: Double = 0.000001): Boolean = (this - other).absoluteValue < epsilon diff --git a/math/src/main/java/com/icegps/math/IsAlmostZero.kt b/math/src/main/java/com/icegps/math/IsAlmostZero.kt new file mode 100644 index 0000000..7ef8e7a --- /dev/null +++ b/math/src/main/java/com/icegps/math/IsAlmostZero.kt @@ -0,0 +1,4 @@ +package com.icegps.math + +fun Double.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-19 +fun Float.isAlmostZero(): Boolean = kotlin.math.abs(this) <= 1e-6 diff --git a/math/src/main/java/com/icegps/math/IsEven.kt b/math/src/main/java/com/icegps/math/IsEven.kt new file mode 100644 index 0000000..784e8b6 --- /dev/null +++ b/math/src/main/java/com/icegps/math/IsEven.kt @@ -0,0 +1,9 @@ +package com.icegps.math + +//////////////////// +//////////////////// + +/** Checks if [this] is odd (not multiple of two) */ +val Int.isOdd: Boolean get() = (this % 2) == 1 +/** Checks if [this] is even (multiple of two) */ +val Int.isEven: Boolean get() = (this % 2) == 0 diff --git a/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt b/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt new file mode 100644 index 0000000..40449b6 --- /dev/null +++ b/math/src/main/java/com/icegps/math/IsNanOrInfinite.kt @@ -0,0 +1,11 @@ +package com.icegps.math + +///** Check if [this] floating point value is not a number or infinite */ +//public fun Float.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite() +///** Check if [this] floating point value is not a number or infinite */ +//public fun Double.isNanOrInfinite(): Boolean = this.isNaN() || this.isInfinite() + + +fun Double.isNanOrInfinite() = this.isNaN() || this.isInfinite() + +fun Float.isNanOrInfinite() = this.isNaN() || this.isInfinite() diff --git a/math/src/main/java/com/icegps/math/Math.kt b/math/src/main/java/com/icegps/math/Math.kt new file mode 100644 index 0000000..5dfc1e5 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Math.kt @@ -0,0 +1,133 @@ +package com.icegps.math + +import kotlin.math.* + +const val PIF = PI.toFloat() +const val PI2F = (PI * 2).toFloat() + +fun Double.betweenInclusive(min: Double, max: Double): Boolean = (this >= min) && (this <= max) + +fun almostEquals(a: Float, b: Float) = almostZero(a - b) +fun almostZero(a: Float) = abs(a) <= 0.0000001 + +fun almostEquals(a: Double, b: Double) = almostZero(a - b) +fun almostZero(a: Double) = abs(a) <= 0.0000001 + +fun isEquivalent(a: Double, b: Double, epsilon: Double = 0.0001): Boolean = (a - epsilon < b) && (a + epsilon > b) + +fun Double.smoothstep(edge0: Double, edge1: Double): Double { + if (this < edge0) return 0.0 + if (this >= edge1) return 1.0 + val v = ((this - edge0) / (edge1 - edge0))//.clamp(0.0, 1.0) + return v * v * (3 - 2 * v) +} + +fun log(v: Int, base: Int): Int = log(v.toDouble(), base.toDouble()).toInt() +fun ln(v: Int): Int = ln(v.toDouble()).toInt() +fun log2(v: Int): Int = log(v.toDouble(), 2.0).toInt() +fun log10(v: Int): Int = log(v.toDouble(), 10.0).toInt() + +@Deprecated("", ReplaceWith("v.squared()")) +fun sq(v: Int): Int = v.squared() +@Deprecated("", ReplaceWith("v.squared()")) +fun sq(v: Float): Float = v.squared() +@Deprecated("", ReplaceWith("v.squared()")) +fun sq(v: Double): Double = v.squared() + +/** Signs of the value. Zero will be converted into -1 */ +val Int.signM1: Int get() = signNonZeroM1(this) +/** Signs of the value. Zero will be converted into -1 */ +val Float.signM1: Float get() = signNonZeroM1(this).toFloat() +/** Signs of the value. Zero will be converted into -1 */ +val Double.signM1: Double get() = signNonZeroM1(this).toDouble() + +/** Signs of the value. Zero will be converted into +1 */ +val Int.signP1: Int get() = signNonZeroP1(this) +/** Signs of the value. Zero will be converted into +1 */ +val Float.signP1: Float get() = signNonZeroP1(this).toFloat() +/** Signs of the value. Zero will be converted into +1 */ +val Double.signP1: Double get() = signNonZeroP1(this).toDouble() + +/** Signs of the value. Zero will be converted into -1 */ +fun signNonZeroM1(x: Int): Int = if (x <= 0) -1 else +1 +/** Signs of the value. Zero will be converted into -1 */ +fun signNonZeroM1(x: Float): Int = if (x <= 0) -1 else +1 +/** Signs of the value. Zero will be converted into -1 */ +fun signNonZeroM1(x: Double): Int = if (x <= 0) -1 else +1 + + +/** Signs of the value. Zero will be converted into +1 */ +fun signNonZeroP1(x: Int): Int = if (x >= 0) +1 else -1 +/** Signs of the value. Zero will be converted into +1 */ +fun signNonZeroP1(x: Float): Int = if (x >= 0) +1 else -1 +/** Signs of the value. Zero will be converted into +1 */ +fun signNonZeroP1(x: Double): Int = if (x >= 0) +1 else -1 + +fun Float.normalizeAlmostZero() = if (this.isAlmostZero()) 0f else this + +fun Double.closestMultipleOf(multiple: Double): Double { + val prev = prevMultipleOf(multiple) + val next = nextMultipleOf(multiple) + return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next +} +fun Int.closestMultipleOf(multiple: Int): Int { + val prev = prevMultipleOf(multiple) + val next = nextMultipleOf(multiple) + return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next +} +fun Long.closestMultipleOf(multiple: Long): Long { + val prev = prevMultipleOf(multiple) + val next = nextMultipleOf(multiple) + return if ((this - prev).absoluteValue < (this - next).absoluteValue) prev else next +} + +fun Double.nextMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple) +fun Int.nextMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple) +fun Long.nextMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else (((this / multiple) + 1) * multiple) + +fun Double.prevMultipleOf(multiple: Double) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple +fun Int.prevMultipleOf(multiple: Int) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple +fun Long.prevMultipleOf(multiple: Long) = if (this.isMultipleOf(multiple)) this else nextMultipleOf(multiple) - multiple + +fun Double.isMultipleOf(multiple: Double) = multiple.isAlmostZero() || (this % multiple).isAlmostZero() +fun Int.isMultipleOf(multiple: Int) = multiple == 0 || (this % multiple) == 0 +fun Long.isMultipleOf(multiple: Long) = multiple == 0L || (this % multiple) == 0L + +fun Double.squared(): Double = this * this +fun Float.squared(): Float = this * this +fun Int.squared(): Int = this * this + +fun min(a: Int, b: Int, c: Int) = min(min(a, b), c) +fun min(a: Float, b: Float, c: Float) = min(min(a, b), c) +fun min(a: Double, b: Double, c: Double) = min(min(a, b), c) + +fun min(a: Int, b: Int, c: Int, d: Int) = min(min(min(a, b), c), d) +fun min(a: Float, b: Float, c: Float, d: Float) = min(min(min(a, b), c), d) +fun min(a: Double, b: Double, c: Double, d: Double) = min(min(min(a, b), c), d) + +fun min(a: Int, b: Int, c: Int, d: Int, e: Int) = min(min(min(min(a, b), c), d), e) +fun min(a: Float, b: Float, c: Float, d: Float, e: Float) = min(min(min(min(a, b), c), d), e) +fun min(a: Double, b: Double, c: Double, d: Double, e: Double) = min(min(min(min(a, b), c), d), e) + +fun max(a: Int, b: Int, c: Int) = max(max(a, b), c) +fun max(a: Float, b: Float, c: Float) = max(max(a, b), c) +fun max(a: Double, b: Double, c: Double) = max(max(a, b), c) + +fun max(a: Int, b: Int, c: Int, d: Int) = max(max(max(a, b), c), d) +fun max(a: Float, b: Float, c: Float, d: Float) = max(max(max(a, b), c), d) +fun max(a: Double, b: Double, c: Double, d: Double) = max(max(max(a, b), c), d) + +fun max(a: Int, b: Int, c: Int, d: Int, e: Int) = max(max(max(max(a, b), c), d), e) +fun max(a: Float, b: Float, c: Float, d: Float, e: Float) = max(max(max(max(a, b), c), d), e) +fun max(a: Double, b: Double, c: Double, d: Double, e: Double) = max(max(max(max(a, b), c), d), e) + +//////////////////// +//////////////////// + + +// @TODO: Optimize this +fun Int.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length +fun Long.numberOfDigits(radix: Int = 10): Int = radix.toString(radix).length + +fun Int.cycle(min: Int, max: Int): Int = ((this - min) umod (max - min + 1)) + min +fun Int.cycleSteps(min: Int, max: Int): Int = (this - min) / (max - min + 1) diff --git a/math/src/main/java/com/icegps/math/NormalizeZero.kt b/math/src/main/java/com/icegps/math/NormalizeZero.kt new file mode 100644 index 0000000..14dc9bf --- /dev/null +++ b/math/src/main/java/com/icegps/math/NormalizeZero.kt @@ -0,0 +1,7 @@ +package com.icegps.math + +//fun Double.normalizeZero(): Double = if (this.isAlmostZero()) 0.0 else this +private val MINUS_ZERO_D = -0.0 +private val MINUS_ZERO_F = -0.0f +fun Double.normalizeZero(): Double = if (this == MINUS_ZERO_D) 0.0 else this +fun Float.normalizeZero(): Float = if (this == MINUS_ZERO_F) 0f else this diff --git a/math/src/main/java/com/icegps/math/PowerOfTwo.kt b/math/src/main/java/com/icegps/math/PowerOfTwo.kt new file mode 100644 index 0000000..6076d1b --- /dev/null +++ b/math/src/main/java/com/icegps/math/PowerOfTwo.kt @@ -0,0 +1,22 @@ +package com.icegps.math + + +/** Returns the next power of two of [this] */ +val Int.nextPowerOfTwo: Int get() { + var v = this + v-- + v = v or (v shr 1) + v = v or (v shr 2) + v = v or (v shr 4) + v = v or (v shr 8) + v = v or (v shr 16) + v++ + return v +} +/** Checks if [this] value is power of two */ +val Int.isPowerOfTwo: Boolean get() = this.nextPowerOfTwo == this + +/** Returns the previous power of two of [this] */ +val Int.prevPowerOfTwo: Int get() = if (isPowerOfTwo) this else (nextPowerOfTwo ushr 1) + + diff --git a/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt b/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt new file mode 100644 index 0000000..3598630 --- /dev/null +++ b/math/src/main/java/com/icegps/math/RoundDecimalPlaces.kt @@ -0,0 +1,16 @@ +package com.icegps.math + +import kotlin.math.* + +fun Float.roundDecimalPlaces(places: Int): Float { + if (places < 0) return this + val placesFactor: Float = 10f.pow(places.toFloat()) + return round(this * placesFactor) / placesFactor +} + +fun Double.roundDecimalPlaces(places: Int): Double { + if (places < 0) return this + val placesFactor: Double = 10.0.pow(places.toDouble()) + return round(this * placesFactor) / placesFactor +} + diff --git a/math/src/main/java/com/icegps/math/ToIntegerConverters.kt b/math/src/main/java/com/icegps/math/ToIntegerConverters.kt new file mode 100644 index 0000000..333a902 --- /dev/null +++ b/math/src/main/java/com/icegps/math/ToIntegerConverters.kt @@ -0,0 +1,29 @@ +package com.icegps.math + +import kotlin.math.* + +//////////////////// +//////////////////// + +/** Converts [this] into [Int] rounding to the ceiling */ +fun Float.toIntCeil(): Int = ceil(this).toInt() +/** Converts [this] into [Int] rounding to the ceiling */ +fun Double.toIntCeil(): Int = ceil(this).toInt() + +/** Converts [this] into [Int] rounding to the nearest */ +fun Float.toIntRound(): Int = round(this).toInt() +/** Converts [this] into [Int] rounding to the nearest */ +fun Double.toIntRound(): Int = round(this).toInt() + +/** Converts [this] into [Int] rounding to the nearest */ +fun Float.toLongRound(): Long = round(this).toLong() +/** Converts [this] into [Int] rounding to the nearest */ +fun Double.toLongRound(): Long = round(this).toLong() + +/** Convert this [Long] into an [Int] but throws an [IllegalArgumentException] in the case that operation would produce an overflow */ +fun Long.toIntSafe(): Int = if (this in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) this.toInt() else throw IllegalArgumentException("Long doesn't fit Integer") + +/** Converts [this] into [Int] rounding to the floor */ +fun Float.toIntFloor(): Int = floor(this).toInt() +/** Converts [this] into [Int] rounding to the floor */ +fun Double.toIntFloor(): Int = floor(this).toInt() diff --git a/math/src/main/java/com/icegps/math/Umod.kt b/math/src/main/java/com/icegps/math/Umod.kt new file mode 100644 index 0000000..14fd1c1 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Umod.kt @@ -0,0 +1,35 @@ +package com.icegps.math + +private val MINUS_ZERO_F = -0.0f + +//////////////////// +//////////////////// + +/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */ +public infix fun Int.umod(other: Int): Int { + val rm = this % other + val remainder = if (rm == -0) 0 else rm + return when { + remainder < 0 -> remainder + other + else -> remainder + } +} + +/** Performs the unsigned modulo between [this] and [other] (negative values would wrap) */ +public infix fun Double.umod(other: Double): Double { + val rm = this % other + val remainder = if (rm == -0.0) 0.0 else rm + return when { + remainder < 0.0 -> remainder + other + else -> remainder + } +} + +public infix fun Float.umod(other: Float): Float { + val rm = this % other + val remainder = if (rm == MINUS_ZERO_F) 0f else rm + return when { + remainder < 0f -> remainder + other + else -> remainder + } +} diff --git a/math/src/main/java/com/icegps/math/Unsigned.kt b/math/src/main/java/com/icegps/math/Unsigned.kt new file mode 100644 index 0000000..13c2ea6 --- /dev/null +++ b/math/src/main/java/com/icegps/math/Unsigned.kt @@ -0,0 +1,13 @@ +package com.icegps.math + +//////////////////// +//////////////////// + +/** Returns an [Int] representing this [Byte] as if it was unsigned 0x00..0xFF */ +inline val Byte.unsigned: Int get() = this.toInt() and 0xFF + +/** Returns an [Int] representing this [Short] as if it was unsigned 0x0000..0xFFFF */ +inline val Short.unsigned: Int get() = this.toInt() and 0xFFFF + +/** Returns a [Long] representing this [Int] as if it was unsigned 0x00000000L..0xFFFFFFFFL */ +inline val Int.unsigned: Long get() = this.toLong() and 0xFFFFFFFFL diff --git a/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt b/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt new file mode 100644 index 0000000..4510737 --- /dev/null +++ b/math/src/main/java/com/icegps/math/annotations/_Math_annotations.kt @@ -0,0 +1,41 @@ +@file:Suppress("PackageDirectoryMismatch") + +package com.icegps.math.annotations + +@DslMarker +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) +annotation class KorDslMarker + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) +@DslMarker +annotation class ViewDslMarker + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) +@DslMarker +annotation class RootViewDslMarker + +@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS) +@DslMarker +annotation class VectorDslMarker + +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +annotation class KormaExperimental(val reason: String = "") + +//@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +/** + * Mutable APIs follow the following convention: + * + * ```kotlin + * interface IType { val ... } + * class MType : IType(override var ...) : IType + * ``` + * + * Then in usage places: + * + * ```kotlin + * fun doSomethingWith(a: IType, out: MType = MType()): MType + * ``` + * + * This convention supports allocation-free APIs by being able to preallocate instances and passing them as the output. + */ +annotation class KormaMutableApi diff --git a/math/src/main/java/com/icegps/math/geometry/AABB3D.kt b/math/src/main/java/com/icegps/math/geometry/AABB3D.kt new file mode 100644 index 0000000..ea4fe92 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/AABB3D.kt @@ -0,0 +1,55 @@ +package com.icegps.math.geometry + +import com.icegps.math.geometry.shape.* +import kotlin.math.* + +data class AABB3D(val min: Vector3F = Vector3F(), val max: Vector3F = Vector3F()) : SimpleShape3D { + val minX: Float get() = min.x + val minY: Float get() = min.y + val minZ: Float get() = min.z + + val maxX: Float get() = max.x + val maxY: Float get() = max.y + val maxZ: Float get() = max.z + + val sizeX: Float get() = maxX - minX + val sizeY: Float get() = maxY - minY + val sizeZ: Float get() = maxZ - minZ + + companion object { + operator fun invoke(min: Float = Float.POSITIVE_INFINITY, max: Float = Float.NEGATIVE_INFINITY): AABB3D = + AABB3D(Vector3F(min, min, min), Vector3F(max, max, max)) + + fun fromSphere(pos: Vector3F, radius: Float): AABB3D = AABB3D( + Vector3F(pos.x - radius, pos.y - radius, pos.z - radius), + Vector3F(pos.x + radius, pos.y + radius, pos.z + radius) + ) + } + + fun expandedToFit(that: AABB3D): AABB3D { + val a = this + val b = that + return AABB3D( + min = Vector3F(min(a.minX, b.minX), min(a.minY, b.minY), min(a.minZ, b.minZ)), + max = Vector3F(max(a.maxX, b.maxX), max(a.maxY, b.maxY), max(a.maxZ, b.maxZ)), + ) + } + + fun intersectsSphere(sphere: Sphere3D): Boolean = intersectsSphere(sphere.center, sphere.radius) + fun intersectsSphere(origin: Vector3F, radius: Float): Boolean = !(origin.x + radius < minX || + origin.y + radius < minY || + origin.z + radius < minZ || + origin.x - radius > maxX || + origin.y - radius > maxY || + origin.z - radius > maxZ) + + fun intersectsAABB(box: AABB3D): Boolean = max.x > box.min.x && min.x < box.max.x && + max.y > box.min.y && min.y < box.max.y && + max.z > box.min.z && min.z < box.max.z + + override val center: Vector3F get() = (min + max) * 0.5f + override val volume: Float get() { + val v = (max - min) + return v.x * v.y * v.z + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/Anchor.kt b/math/src/main/java/com/icegps/math/geometry/Anchor.kt new file mode 100644 index 0000000..108a655 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Anchor.kt @@ -0,0 +1,103 @@ +package com.icegps.math.geometry + +import com.icegps.math.interpolation.* + +typealias Anchor = Anchor2D +typealias Anchor3 = Anchor3F + +data class Anchor2D(val sx: Double, val sy: Double) : Interpolable { + fun toVector(): Vector2D = Vector2D(sx, sy) + + val ratioX: Ratio get() = sx.toRatio() + val ratioY: Ratio get() = sy.toRatio() + + constructor(sx: Float, sy: Float) : this(sx.toDouble(), sy.toDouble()) + constructor(sx: Int, sy: Int) : this(sx.toDouble(), sy.toDouble()) + + inline fun withX(sx: Number): Anchor = Anchor(sx.toDouble(), sy) + inline fun withY(sy: Number): Anchor = Anchor(sx, sy.toDouble()) + + inline fun withX(ratioX: Ratio): Anchor = Anchor(ratioX.toDouble(), sy) + inline fun withY(ratioY: Ratio): Anchor = Anchor(sx, ratioY.toDouble()) + + companion object { + inline operator fun invoke(sx: Ratio, sy: Ratio): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble()) + inline operator fun invoke(sx: Number, sy: Number): Anchor2D = Anchor2D(sx.toDouble(), sy.toDouble()) + + val TOP_LEFT: Anchor = Anchor(0f, 0f) + val TOP_CENTER: Anchor = Anchor(.5f, 0f) + val TOP_RIGHT: Anchor = Anchor(1f, 0f) + + val MIDDLE_LEFT: Anchor = Anchor(0f, .5f) + val MIDDLE_CENTER: Anchor = Anchor(.5f, .5f) + val MIDDLE_RIGHT: Anchor = Anchor(1f, .5f) + + val BOTTOM_LEFT: Anchor = Anchor(0f, 1f) + val BOTTOM_CENTER: Anchor = Anchor(.5f, 1f) + val BOTTOM_RIGHT: Anchor = Anchor(1f, 1f) + + val TOP: Anchor get() = TOP_CENTER + val LEFT: Anchor get() = MIDDLE_LEFT + val RIGHT: Anchor get() = MIDDLE_RIGHT + val BOTTOM: Anchor get() = BOTTOM_CENTER + val CENTER: Anchor get() = MIDDLE_CENTER + } + + override fun interpolateWith(ratio: Ratio, other: Anchor): Anchor = Anchor( + ratio.interpolate(this.sx, other.sx), + ratio.interpolate(this.sy, other.sy) + ) + + fun toNamedString(): String = when (this) { + TOP_LEFT -> "Anchor.TOP_LEFT" + TOP -> "Anchor.TOP" + TOP_RIGHT -> "Anchor.TOP_RIGHT" + LEFT -> "Anchor.LEFT" + CENTER -> "Anchor.MIDDLE_CENTER" + RIGHT -> "Anchor.RIGHT" + BOTTOM_LEFT -> "Anchor.BOTTOM_LEFT" + BOTTOM_CENTER -> "Anchor.BOTTOM_CENTER" + BOTTOM_RIGHT -> "Anchor.BOTTOM_RIGHT" + else -> toString() + } +} + +operator fun Size.times(anchor: Anchor): Point = this.toVector() * anchor.toVector() +//operator fun SizeInt.times(anchor: Anchor): PointInt = (this.toVector().toFloat() * anchor.toVector()).toInt() + +data class Anchor3F(val sx: Float, val sy: Float, val sz: Float) : Interpolable { + fun toVector(): Vector3F = Vector3F(sx, sy, sz) + + val floatX: Float get() = sx + val floatY: Float get() = sy + val floatZ: Float get() = sz + + val doubleX: Double get() = sx.toDouble() + val doubleY: Double get() = sy.toDouble() + val doubleZ: Double get() = sz.toDouble() + + val ratioX: Ratio get() = sx.toRatio() + val ratioY: Ratio get() = sy.toRatio() + val ratioZ: Ratio get() = sz.toRatio() + + constructor(sx: Double, sy: Double, sz: Double) : this(sx.toFloat(), sy.toFloat(), sz.toFloat()) + constructor(sx: Int, sy: Int, sz: Int) : this(sx.toFloat(), sy.toFloat(), sz.toFloat()) + + fun withX(sx: Float): Anchor3F = Anchor3F(sx, sy, sz) + fun withX(sx: Int): Anchor3F = Anchor3F(sx.toFloat(), sy, sz) + fun withX(sx: Double): Anchor3F = Anchor3F(sx.toFloat(), sy, sz) + + fun withY(sy: Float): Anchor3F = Anchor3F(sx, sy, sz) + fun withY(sy: Int): Anchor3F = Anchor3F(sx, sy.toFloat(), sz) + fun withY(sy: Double): Anchor3F = Anchor3F(sx, sy.toFloat(), sz) + + fun withZ(sz: Float): Anchor3F = Anchor3F(sx, sy, sz) + fun withZ(sz: Int): Anchor3F = Anchor3F(sx, sy, sz.toFloat()) + fun withZ(sz: Double): Anchor3F = Anchor3F(sx, sy, sz.toFloat()) + + override fun interpolateWith(ratio: Ratio, other: Anchor3F): Anchor3F = Anchor3F( + ratio.interpolate(this.sx, other.sx), + ratio.interpolate(this.sy, other.sy), + ratio.interpolate(this.sz, other.sz), + ) +} diff --git a/math/src/main/java/com/icegps/math/geometry/Angle.kt b/math/src/main/java/com/icegps/math/geometry/Angle.kt new file mode 100644 index 0000000..534e8e9 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Angle.kt @@ -0,0 +1,250 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.interpolation.* +import com.icegps.math.range.* +import com.icegps.number.* +import kotlin.math.* + +@PublishedApi internal const val PI2 = PI * 2.0 +@PublishedApi internal const val DEG2RAD = PI / 180.0 +@PublishedApi internal const val RAD2DEG = 180.0 / PI + +@PublishedApi internal fun Angle_shortDistanceTo(from: Angle, to: Angle): Angle { + val r0 = from.ratio.toDouble() umod 1.0 + val r1 = to.ratio.toDouble() umod 1.0 + val diff = (r1 - r0 + 0.5) % 1.0 - 0.5 + return if (diff < -0.5) Angle.fromRatio(diff + 1.0) else Angle.fromRatio(diff) +} + +@PublishedApi internal fun Angle_longDistanceTo(from: Angle, to: Angle): Angle { + val short = Angle_shortDistanceTo(from, to) + return when { + short == Angle.ZERO -> Angle.ZERO + short < Angle.ZERO -> Angle.FULL + short + else -> -Angle.FULL + short + } +} + +@PublishedApi internal fun Angle_between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle { + val angle = Angle.atan2(y1 - y0, x1 - x0) + return (if (angle < Angle.ZERO) angle + Angle.FULL else angle).adjustFromUp(up) +} + +@PublishedApi internal fun Angle.adjustFromUp(up: Vector2D): Angle { + Orientation.checkValidUpVector(up) + return if (up.y > 0) this else -this +} + +/** + * Represents an [Angle], [ratio] is in [0, 1] range, [radians] is in [0, 2PI] range, and [degrees] in [0, 360] range + * The internal representation is in [0, 1] range to reduce rounding errors, since floating points can represent + * a lot of values in that range. + * + * The equivalent old [Angle] constructor is now [Angle.fromRadians] + * + * Angles advance counter-clock-wise, starting with 0.degrees representing the right vector: + * + * Depending on what the up vector means, then numeric values of sin might be negated. + * + * 0.degrees represent right: up=Vector2.UP: cos =+1, sin= 0 || up=Vector2.UP_SCREEN: cos =+1, sin= 0 + * 90.degrees represents up: up=Vector2.UP: cos = 0, sin=+1 || up=Vector2.UP_SCREEN: cos = 0, sin=-1 + * 180.degrees represents left: up=Vector2.UP: cos =-1, sin= 0 || up=Vector2.UP_SCREEN: cos =-1, sin= 0 + * 270.degrees represents down: up=Vector2.UP: cos = 0, sin=-1 || up=Vector2.UP_SCREEN: cos = 0, sin=+1 + */ +//@KormaValueApi +inline class Angle @PublishedApi internal constructor( + /** [0..1] ratio -> [0..360] degrees */ + val radians: Double +) : Comparable, IsAlmostEquals { + @PublishedApi inline internal val internal: Double get() = radians + + /** [0..PI * 2] radians -> [0..360] degrees */ + val ratio: Ratio get() = radiansToRatio(radians) + /** [0..360] degrees -> [0..PI * 2] radians -> [0..1] ratio */ + val degrees: Double get() = radiansToDegrees(radians) + + val cosine: Double get() = kotlin.math.cos(radians) + val sine: Double get() = kotlin.math.sin(radians) + val tangent: Double get() = kotlin.math.tan(radians) + + fun cosine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).cosine + fun sine(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).sine + fun tangent(up: Vector2D = Vector2D.UP): Double = adjustFromUp(up).tangent + + val absoluteValue: Angle get() = Angle(internal.absoluteValue) + fun shortDistanceTo(other: Angle): Angle = Angle.shortDistanceTo(this, other) + fun longDistanceTo(other: Angle): Angle = Angle.longDistanceTo(this, other) + + operator fun times(scale: Double): Angle = Angle(this.internal * scale) + operator fun div(scale: Double): Angle = Angle(this.internal / scale) + operator fun times(scale: Float): Angle = Angle(this.internal * scale) + operator fun div(scale: Float): Angle = Angle(this.internal / scale) + operator fun times(scale: Int): Angle = Angle(this.internal * scale) + operator fun div(scale: Int): Angle = Angle(this.internal / scale) + operator fun rem(angle: Angle): Angle = Angle(this.internal % angle.internal) + infix fun umod(angle: Angle): Angle = Angle(this.internal umod angle.internal) + + operator fun div(other: Angle): Double = this.internal / other.internal // Ratio + operator fun plus(other: Angle): Angle = Angle(this.internal + other.internal) + operator fun minus(other: Angle): Angle = Angle(this.internal - other.internal) + operator fun unaryMinus(): Angle = Angle(-internal) + operator fun unaryPlus(): Angle = Angle(+internal) + + fun inBetweenInclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = true) + fun inBetweenExclusive(min: Angle, max: Angle): Boolean = inBetween(min, max, inclusive = false) + + infix fun inBetween(range: ClosedRange): Boolean = inBetween(range.start, range.endInclusive, inclusive = true) + infix fun inBetween(range: OpenRange): Boolean = inBetween(range.start, range.endExclusive, inclusive = false) + + fun inBetween(min: Angle, max: Angle, inclusive: Boolean): Boolean { + val nthis = this.normalized + val nmin = min.normalized + val nmax = max.normalized + @Suppress("ConvertTwoComparisonsToRangeCheck") + return when { + nmin > nmax -> nthis >= nmin || (if (inclusive) nthis <= nmax else nthis < nmax) + else -> nthis >= nmin && (if (inclusive) nthis <= nmax else nthis < nmax) + } + } + + override fun isAlmostEquals(other: Angle, epsilon: Double): Boolean = this.radians.isAlmostEquals(other.radians, epsilon) + fun isAlmostZero(epsilon: Double = 0.001): Boolean = isAlmostEquals(ZERO, epsilon) + + /** Normalize between 0..1 ... 0..(PI*2).radians ... 0..360.degrees */ + val normalized: Angle get() = fromRatio(ratio.toDouble() umod 1.0) + /** Normalize between -.5..+.5 ... -PI..+PI.radians ... -180..+180.degrees */ + val normalizedHalf: Angle get() { + val res = normalized + return if (res > Angle.HALF) -Angle.FULL + res else res + } + + override operator fun compareTo(other: Angle): Int = this.ratio.compareTo(other.ratio) + + //override fun compareTo(other: Angle): Int { + // //return this.radians.compareTo(other.radians) // @TODO: Double.compareTo calls EnterFrame/LeaveFrame! because it uses a Double companion object + // val left = this.ratio + // val right = other.ratio + // // @TODO: Handle infinite/NaN? Though usually this won't happen + // if (left < right) return -1 + // if (left > right) return +1 + // return 0 + //} + + override fun toString(): String = "${degrees.roundDecimalPlaces(2).niceStr}.degrees" + + @Suppress("MemberVisibilityCanBePrivate") + companion object { + val EPSILON = Angle.fromRatio(0.00001) + val ZERO = Angle.fromRatio(0.0) + val QUARTER = Angle.fromRatio(0.25) + val HALF = Angle.fromRatio(0.5) + val THREE_QUARTERS = Angle.fromRatio(0.75) + val FULL = Angle.fromRatio(1.0) + + inline fun fromRatio(ratio: Float): Angle = Angle(ratioToRadians(ratio.toRatio())) + inline fun fromRatio(ratio: Double): Angle = Angle(ratioToRadians(ratio.toRatio())) + inline fun fromRatio(ratio: Ratio): Angle = Angle(ratioToRadians(ratio)) + + inline fun fromRadians(radians: Double): Angle = Angle(radians) + inline fun fromRadians(radians: Float) = Angle(radians.toDouble()) + inline fun fromRadians(radians: Int) = Angle(radians.toDouble()) + + inline fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees)) + inline fun fromDegrees(degrees: Float) = Angle(degreesToRadians(degrees.toDouble())) + inline fun fromDegrees(degrees: Int) = Angle(degreesToRadians(degrees.toDouble())) + + @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).cosineD")) + inline fun cos01(ratio: Double): Double = Angle.fromRatio(ratio).cosine + @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).sineD")) + inline fun sin01(ratio: Double): Double = Angle.fromRatio(ratio).sine + @Deprecated("", ReplaceWith("Angle.fromRatio(ratio).tangentD")) + inline fun tan01(ratio: Double): Double = Angle.fromRatio(ratio).tangent + + inline fun atan2(x: Float, y: Float, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up) + inline fun atan2(x: Double, y: Double, up: Vector2D = Vector2D.UP): Angle = fromRadians(kotlin.math.atan2(x, y)).adjustFromUp(up) + inline fun atan2(p: Point, up: Vector2D = Vector2D.UP): Angle = atan2(p.x, p.y, up) + + inline fun asin(v: Double): Angle = kotlin.math.asin(v).radians + inline fun asin(v: Float): Angle = kotlin.math.asin(v).radians + + inline fun acos(v: Double): Angle = kotlin.math.acos(v).radians + inline fun acos(v: Float): Angle = kotlin.math.acos(v).radians + + fun arcCosine(v: Double): Angle = kotlin.math.acos(v).radians + fun arcCosine(v: Float): Angle = kotlin.math.acos(v).radians + + fun arcSine(v: Double): Angle = kotlin.math.asin(v).radians + fun arcSine(v: Float): Angle = kotlin.math.asin(v).radians + + fun arcTangent(x: Double, y: Double): Angle = kotlin.math.atan2(x, y).radians + fun arcTangent(x: Float, y: Float): Angle = kotlin.math.atan2(x, y).radians + fun arcTangent(v: Vector2F): Angle = kotlin.math.atan2(v.x, v.y).radians + + inline fun ratioToDegrees(ratio: Ratio): Double = ratio * 360.0 + inline fun ratioToRadians(ratio: Ratio): Double = ratio * PI2 + + inline fun degreesToRatio(degrees: Double): Ratio = Ratio(degrees / 360.0) + inline fun degreesToRadians(degrees: Double): Double = degrees * DEG2RAD + + inline fun radiansToRatio(radians: Double): Ratio = Ratio(radians / PI2) + inline fun radiansToDegrees(radians: Double): Double = radians * RAD2DEG + + inline fun shortDistanceTo(from: Angle, to: Angle): Angle = Angle_shortDistanceTo(from, to) + inline fun longDistanceTo(from: Angle, to: Angle): Angle = Angle_longDistanceTo(from, to) + inline fun between(x0: Double, y0: Double, x1: Double, y1: Double, up: Vector2D = Vector2D.UP): Angle = Angle_between(x0, y0, x1, y1, up) + + inline fun between(x0: Int, y0: Int, x1: Int, y1: Int, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up) + inline fun between(x0: Float, y0: Float, x1: Float, y1: Float, up: Vector2D = Vector2D.UP): Angle = between(x0.toDouble(), y0.toDouble(), x1.toDouble(), y1.toDouble(), up) + + inline fun between(p0: Point, p1: Point, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up) + inline fun between(p0: Vector2F, p1: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(p0.x, p0.y, p1.x, p1.y, up) + + inline fun between(ox: Double, oy: Double, x1: Double, y1: Double, x2: Double, y2: Double, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up) + inline fun between(ox: Float, oy: Float, x1: Float, y1: Float, x2: Float, y2: Float, up: Vector2D = Vector2D.UP): Angle = between(x1 - ox, y1 - oy, x2 - ox, y2 - oy, up) + + inline fun between(o: Point, v1: Point, v2: Point, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up) + inline fun between(o: Vector2F, v1: Vector2F, v2: Vector2F, up: Vector2D = Vector2D.UP): Angle = between(o.x, o.y, v1.x, v1.y, v2.x, v2.y, up) + } +} + +inline fun cos(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.cosine(up) +inline fun sin(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.sine(up) +inline fun tan(angle: Angle, up: Vector2D = Vector2D.UP): Double = angle.tangent(up) + +inline fun cosf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.cosine(up).toFloat() +inline fun sinf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.sine(up).toFloat() +inline fun tanf(angle: Angle, up: Vector2D = Vector2D.UP): Float = angle.tangent(up).toFloat() + +inline fun abs(angle: Angle): Angle = angle.absoluteValue +inline fun min(a: Angle, b: Angle): Angle = Angle(min(a.internal, b.internal)) +inline fun max(a: Angle, b: Angle): Angle = Angle(max(a.internal, b.internal)) + +fun Angle.clamp(min: Angle, max: Angle): Angle = min(max(this, min), max) + +operator fun ClosedRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endInclusive, inclusive = true) +operator fun OpenRange.contains(angle: Angle): Boolean = angle.inBetween(this.start, this.endExclusive, inclusive = false) +infix fun Angle.until(other: Angle): OpenRange = OpenRange(this, other) + +val Double.degrees: Angle get() = Angle.fromDegrees(this) +val Double.radians: Angle get() = Angle.fromRadians(this) +val Int.degrees: Angle get() = Angle.fromDegrees(this) +val Int.radians: Angle get() = Angle.fromRadians(this) +val Float.degrees: Angle get() = Angle.fromDegrees(this) +val Float.radians: Angle get() = Angle.fromRadians(this) + +fun Ratio.interpolateAngle(l: Angle, r: Angle, minimizeAngle: Boolean): Angle = _interpolateAngleAny(this, l, r, minimizeAngle) +fun Ratio.interpolateAngle(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true) +fun Ratio.interpolateAngleNormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = true) +fun Ratio.interpolateAngleDenormalized(l: Angle, r: Angle): Angle = interpolateAngle(l, r, minimizeAngle = false) + +private fun _interpolateAngleAny(ratio: Ratio, l: Angle, r: Angle, minimizeAngle: Boolean = true): Angle { + if (!minimizeAngle) return Angle.fromRatio(ratio.interpolate(l.ratio, r.ratio)) + val ln = l.normalized + val rn = r.normalized + return when { + (rn - ln).absoluteValue <= Angle.HALF -> Angle.fromRadians(ratio.interpolate(ln.radians, rn.radians)) + ln < rn -> Angle.fromRadians(ratio.interpolate((ln + Angle.FULL).radians, rn.radians)).normalized + else -> Angle.fromRadians(ratio.interpolate(ln.radians, (rn + Angle.FULL).radians)).normalized + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt b/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt new file mode 100644 index 0000000..6409618 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/BoundsBuilder.kt @@ -0,0 +1,60 @@ +package com.icegps.math.geometry + +inline class BoundsBuilder(val bounds: Rectangle) { + val isEmpty: Boolean get() = bounds.isNIL + val isNotEmpty: Boolean get() = bounds.isNotNIL + + val xmin: Double get() = kotlin.math.min(bounds.left, bounds.right) + val xmax: Double get() = kotlin.math.max(bounds.left, bounds.right) + val ymin: Double get() = kotlin.math.min(bounds.top, bounds.bottom) + val ymax: Double get() = kotlin.math.max(bounds.top, bounds.bottom) + + /** Minimum value found for X. [default] if ![hasPoints] */ + fun xminOr(default: Double = 0.0): Double = if (hasPoints) xmin else default + /** Maximum value found for X. [default] if ![hasPoints] */ + fun xmaxOr(default: Double = 0.0): Double = if (hasPoints) xmax else default + /** Minimum value found for Y. [default] if ![hasPoints] */ + fun yminOr(default: Double = 0.0): Double = if (hasPoints) ymin else default + /** Maximum value found for Y. [default] if ![hasPoints] */ + fun ymaxOr(default: Double = 0.0): Double = if (hasPoints) ymax else default + + val hasPoints: Boolean get() = isNotEmpty + + companion object { + val EMPTY = BoundsBuilder(Rectangle.NIL) + + operator fun invoke(): BoundsBuilder = EMPTY + operator fun invoke(p1: Point): BoundsBuilder = BoundsBuilder(Rectangle(p1, Size(0, 0))) + operator fun invoke(p1: Point, p2: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2), Point.maxComponents(p1, p2))) + operator fun invoke(p1: Point, p2: Point, p3: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3), Point.maxComponents(p1, p2, p3))) + operator fun invoke(p1: Point, p2: Point, p3: Point, p4: Point): BoundsBuilder = BoundsBuilder(Rectangle.fromBounds(Point.minComponents(p1, p2, p3, p4), Point.maxComponents(p1, p2, p3, p4))) + operator fun invoke(size: Int, func: BoundsBuilder.(Int) -> BoundsBuilder): BoundsBuilder { + var bb = BoundsBuilder() + for (n in 0 until size) bb = func(bb, n) + return bb + } + } + fun plus(x: Double, y: Double): BoundsBuilder = this.plus(Point(x, y)) + operator fun plus(p: Point): BoundsBuilder { + if (bounds.isNIL) return BoundsBuilder(Rectangle(p, Size(0, 0))) + return BoundsBuilder(Rectangle.fromBounds(Point.minComponents(bounds.topLeft, p), Point.maxComponents(bounds.bottomRight, p))) + } + operator fun plus(bb: BoundsBuilder): BoundsBuilder = this + bb.bounds + operator fun plus(rect: Rectangle?): BoundsBuilder { + if (rect == null) return this + if (rect.isNIL) return this + return this + rect.topLeft + rect.bottomRight + } + operator fun plus(p: IPointList): BoundsBuilder { + var bb = this + for (n in 0 until p.size) bb = bb.plus(p[n]) + return bb + } + //operator fun plus(rect: Rectangle): BoundsBuilder = TODO() + operator fun plus(rects: List): BoundsBuilder { + var bb = this + for (it in rects) bb += it + return bb + } + fun boundsOrNull(): Rectangle? = if (isEmpty) null else bounds +} diff --git a/math/src/main/java/com/icegps/math/geometry/Circle.kt b/math/src/main/java/com/icegps/math/geometry/Circle.kt new file mode 100644 index 0000000..587f631 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Circle.kt @@ -0,0 +1,29 @@ +package com.icegps.math.geometry + +import com.icegps.math.geometry.shape.* +import kotlin.math.* + +data class Circle(override val center: Point, val radius: Double) : SimpleShape2D { + companion object { + inline operator fun invoke(center: Point, radius: Number) = Circle(center, radius.toDouble()) + inline operator fun invoke(x: Number, y: Number, radius: Number) = Circle(Point(x.toDouble(), y.toDouble()), radius.toDouble()) + } + + override val closed: Boolean get() = true + + override val area: Double get() = (PI * radius * radius) + override val perimeter: Double get() = (PI * 2.0 * radius) + override fun distance(p: Point): Double = (p - center).length - radius + override fun normalVectorAt(p: Point): Vector2D = (p - center).normalized + + val radiusSquared: Double get() = radius * radius + + fun distanceToCenterSquared(p: Point): Double = Point.distanceSquared(p, center) + // @TODO: Check if inside the circle + fun distanceClosestSquared(p: Point): Double = distanceToCenterSquared(p) - radiusSquared + // @TODO: Check if inside the circle + fun distanceFarthestSquared(p: Point): Double = distanceToCenterSquared(p) + radiusSquared + override fun projectedPoint(p: Point): Point = Point.polar(center, Angle.between(center, p), radius) + override fun containsPoint(p: Point): Boolean = (p - center).length <= radius + override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius, center.y - radius, center.x + radius, center.y + radius,) +} diff --git a/math/src/main/java/com/icegps/math/geometry/Ellipse.kt b/math/src/main/java/com/icegps/math/geometry/Ellipse.kt new file mode 100644 index 0000000..5bd0707 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Ellipse.kt @@ -0,0 +1,83 @@ +package com.icegps.math.geometry + +import com.icegps.math.geometry.shape.* +import kotlin.math.* + +data class Ellipse(override val center: Point, val radius: Size) : SimpleShape2D { + override val area: Double get() = (PI * radius.width * radius.height) + override val perimeter: Double get() { + if (radius.width == radius.height) return (PI * 2.0 * radius.width) // Circle formula + val (a, b) = radius + val h = ((a - b) * (a - b)) / ((a + b) * (a + b)) + return (PI * (a + b) * (1 + ((3 * h) / (10 + sqrt(4 - (3 * h)))))) + } + + override fun distance(p: Point): Double { + val p = p - center + val scaledPoint = Vector2D(p.x / radius.width, p.y / radius.height) + val length = scaledPoint.length + return (length - 1) * min(radius.width, radius.height) + } + + override fun normalVectorAt(p: Point): Vector2D { + val pointOnEllipse = p - center + val (a, b) = radius + val normal = Vector2D(pointOnEllipse.x / (a * a), pointOnEllipse.y / (b * b)) + return normal.normalized + //val d = p - center + //val r2 = radius.toVector() * radius.toVector() + //return (d / r2).normalized + } + + override fun projectedPoint(p: Point): Point { + val angle = Angle.between(center, p) + return center + Point(radius.width * angle.cosine, radius.height * angle.sine) + + //val k = (radius.width * radius.height) / sqrt() + //return projectPointOntoEllipse(p, center, radius.toVector()) + } + + override fun containsPoint(p: Point): Boolean { + if (radius.isEmpty()) return false + // Check if the point is inside the ellipse using the ellipse equation: + // (x - centerX)^2 / radiusX^2 + (y - centerY)^2 / radiusY^2 <= 1 + return ((p.x - center.x).pow(2) / radius.width.pow(2)) + ((p.y - center.y).pow(2) / radius.height.pow(2)) <= 1 + } + + override val closed: Boolean get() = true + override fun getBounds(): Rectangle = Rectangle.fromBounds(center.x - radius.width, center.y - radius.height, center.x + radius.width, center.y + radius.height) + + companion object { + private fun projectPointOntoEllipse(point: Vector2F, center: Vector2F, radius: Vector2F, tolerance: Double = 1e-6, maxIterations: Int = 100): Vector2F { + var currentPoint = point + var i = 0 + + while (i < maxIterations) { + val dx = currentPoint.x - center.x + val dy = currentPoint.y - center.y + val rx2 = radius.x * radius.x + val ry2 = radius.y * radius.y + + val f = Vector2F( + (dx * rx2 - dy * dx * dy) / (rx2 * ry2), + (dy * ry2 - dx * dy * dx) / (rx2 * ry2) + ) + + val df = Vector2F( + (ry2 - 2.0 * dy * dy) / (rx2 * ry2), + (rx2 - 2.0 * dx * dx) / (rx2 * ry2) + ) + + val nextPoint = currentPoint - f / df + val dist = (nextPoint - currentPoint).length + + if (dist < tolerance) return nextPoint + + currentPoint = nextPoint + i++ + } + + return currentPoint + } + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt b/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt new file mode 100644 index 0000000..796d3f7 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/EulerRotation.kt @@ -0,0 +1,330 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import kotlin.math.* + +/** + * Rotations around Z axis, then X axis, then Y axis in that order. + */ +inline class EulerRotation private constructor(val data: Vector4F) : IsAlmostEqualsF { + val config: Config get() = Config(data.w.toInt()) + val order: Order get() = config.order + val coordinateSystem: CoordinateSystem get() = config.coordinateSystem + + enum class Order( + val x: Int, val y: Int, val z: Int, val w: Int, val str: String, + ) { + INVALID(0, 0, 0, 0, "XXX"), + XYZ(+1, -1, +1, -1, "XYZ"), + XZY(-1, -1, +1, +1, "XZY"), + YXZ(+1, -1, -1, +1, "YXZ"), + YZX(+1, +1, -1, -1, "YZX"), + ZXY(-1, +1, +1, -1, "ZXY"), + ZYX(-1, +1, -1, +1, "ZYX"), + ; + + fun withCoordinateSystem(coordinateSystem: CoordinateSystem) = if (coordinateSystem.sign < 0) reversed() else this + + fun reversed(): Order = when (this) { + INVALID -> INVALID + XYZ -> ZYX + XZY -> YZX + YXZ -> ZXY + YZX -> XZY + ZXY -> YXZ + ZYX -> XYZ + } + + fun indexAt(pos: Int, reversed: Boolean = false): Int = str[(if (reversed) 2 - pos else pos) umod 3] - 'X' + + override fun toString(): String = "$name [$x, $y, $z, $w]" + + companion object { + val VALUES = values() + val DEFAULT = XYZ + } + } + //enum class Normalized { NO, FULL_ANGLE, HALF_ANGLE } + inline class Config(val id: Int) { + //constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.ordinal * coordinateSystem.sign) + constructor(order: Order, coordinateSystem: CoordinateSystem) : this(order.withCoordinateSystem(coordinateSystem).ordinal) + + val order: Order get() = Order.VALUES[id.absoluteValue] + val coordinateSystem: CoordinateSystem get() = if (id < 0) CoordinateSystem.LEFT_HANDED else CoordinateSystem.RIGHT_HANDED + + override fun toString(): String = "EulerRotation.Config(order=$order, coordinateSystem=$coordinateSystem)" + + companion object { + val UNITY get() = Config(Order.ZXY, CoordinateSystem.LEFT_HANDED) + //val UNITY get() = LIBGDX + val UNREAL get() = Config(Order.ZYX, CoordinateSystem.LEFT_HANDED) + //val UNREAL get() = THREEJS + val GODOT get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED) + val LIBGDX get() = Config(Order.YXZ, CoordinateSystem.RIGHT_HANDED) + val THREEJS get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED) + + // Same as Three.JS + val DEFAULT get() = Config(Order.XYZ, CoordinateSystem.RIGHT_HANDED) + } + } + enum class CoordinateSystem(val sign: Int) { + LEFT_HANDED(-1), RIGHT_HANDED(+1); + val rsign = -sign + } + + val roll: Angle get() = Angle.fromRatio(data.x) + val pitch: Angle get() = Angle.fromRatio(data.y) + val yaw: Angle get() = Angle.fromRatio(data.z) + + @Deprecated("", ReplaceWith("roll")) val x: Angle get() = roll + @Deprecated("", ReplaceWith("pitch")) val y: Angle get() = pitch + @Deprecated("", ReplaceWith("yaw")) val z: Angle get() = yaw + + override fun toString(): String = "EulerRotation(roll=$roll, pitch=$pitch, yaw=$yaw)" + + fun copy(roll: Angle = this.roll, pitch: Angle = this.pitch, yaw: Angle = this.yaw): EulerRotation = EulerRotation(roll, pitch, yaw) + constructor() : this(Angle.ZERO, Angle.ZERO, Angle.ZERO) + constructor(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT) + : this(Vector4F(roll.ratio.toFloat(), pitch.ratio.toFloat(), yaw.ratio.toFloat(), config.id.toFloat())) + + fun normalized(): EulerRotation = EulerRotation(roll.normalized, pitch.normalized, yaw.normalized) + fun normalizedHalf(): EulerRotation = EulerRotation(roll.normalizedHalf, pitch.normalizedHalf, yaw.normalizedHalf) + + fun toMatrix(): Matrix4 = toQuaternion().toMatrix() + fun toQuaternion(): Quaternion = _toQuaternion(x, y, z, config) + override fun isAlmostEquals(other: EulerRotation, epsilon: Float): Boolean = + this.data.isAlmostEquals(other.data, epsilon) + + companion object { + fun toQuaternion(roll: Angle, pitch: Angle, yaw: Angle, config: Config = Config.DEFAULT): Quaternion { + return _toQuaternion(roll, pitch, yaw, config) + } + // http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m + private fun _toQuaternion(x: Angle, y: Angle, z: Angle, config: Config = Config.DEFAULT): Quaternion { + val order = config.order + val coordinateSystem = config.coordinateSystem + val sign = coordinateSystem.sign + //println("ORDER=$order, coordinateSystem=$coordinateSystem, sign=$sign") + + val c1 = cos(x / 2) + val c2 = cos(y / 2) + val c3 = cos(z / 2) + val s1 = sin(x / 2) + val s2 = sin(y / 2) + val s3 = sin(z / 2) + + return Quaternion( + ((s1 * c2 * c3) + ((c1 * s2 * s3) * order.x * sign)), + ((c1 * s2 * c3) + ((s1 * c2 * s3) * order.y * sign)), + ((c1 * c2 * s3) + ((s1 * s2 * c3) * order.z * sign)), + ((c1 * c2 * c3) + ((s1 * s2 * s3) * order.w * sign)), + ) + } + + fun fromRotationMatrix(m: Matrix3, config: Config = Config.DEFAULT): EulerRotation { + //val config = if (config == Config.UNITY) Config.LIBGDX else config + val order = config.order + val coordinateSystem = config.coordinateSystem + + val sign = coordinateSystem.sign + + //val m = if (sign < 0) m.transposed() else m + //val m = m + + val m11 = m.v00 + val m12 = m.v01 + val m13 = m.v02 + + val m21 = m.v10 + val m22 = m.v11 + val m23 = m.v12 + + val m31 = m.v20 + val m32 = m.v21 + val m33 = m.v22 + + val x: Angle + val y: Angle + val z: Angle + + when (order) { + Order.XYZ -> { + x = if (m13.absoluteNotAlmostOne) Angle.atan2(-m23, m33) else Angle.atan2(m32, m22) + y = Angle.asin(m13.clamp(-1f, +1f)) + z = if (m13.absoluteNotAlmostOne) Angle.atan2(-m12, m11) else Angle.ZERO + } + Order.YXZ -> { + x = Angle.asin(-(m23.clamp(-1f, +1f))) + y = if (m23.absoluteNotAlmostOne) Angle.atan2(m13, m33) else Angle.atan2(-m31, m11) + z = if (m23.absoluteNotAlmostOne) Angle.atan2(m21, m22) else Angle.ZERO + } + Order.ZXY -> { + y = Angle.asin(m32.clamp(-1f, +1f)) + x = if (m32.absoluteNotAlmostOne) Angle.atan2(-m31, m33) else Angle.ZERO + z = if (m32.absoluteNotAlmostOne) Angle.atan2(-m12, m22) else Angle.atan2(m21, m11) + } + Order.ZYX -> { + x = if (m31.absoluteNotAlmostOne) Angle.atan2(m32, m33) else Angle.ZERO + y = Angle.asin(-(m31.clamp(-1f, +1f))) + z = if (m31.absoluteNotAlmostOne) Angle.atan2(m21, m11) else Angle.atan2(-m12, m22) + } + Order.YZX -> { + x = if (m21.absoluteNotAlmostOne) Angle.atan2(-m23, m22) else Angle.ZERO + y = if (m21.absoluteNotAlmostOne) Angle.atan2(-m31, m11) else Angle.atan2(m13, m33) + z = Angle.asin(m21.clamp(-1f, +1f)) + } + Order.XZY -> { + x = if (m12.absoluteNotAlmostOne) Angle.atan2(m32, m22) else Angle.atan2(-m23, m33) + y = if (m12.absoluteNotAlmostOne) Angle.atan2(m13, m11) else Angle.ZERO + z = Angle.asin(-(m12.clamp(-1f, +1f))) + } + Order.INVALID -> error("Invalid") + } + + //println("order=$order, coordinateSystem=$coordinateSystem : ${coordinateSystem.sign}, x=$x, y=$y, z=$z") + + //val sign = coordinateSystem.sign + //return EulerRotation(x * coordinateSystem.sign, y * coordinateSystem.sign, z * coordinateSystem.sign, config) + //return EulerRotation(x * sign, y * sign, z * sign, config) + return EulerRotation(x, y, z, config) + } + + private val Float.absoluteNotAlmostOne: Boolean get() = absoluteValue < 0.9999999 + + + fun fromQuaternion(q: Quaternion, config: Config = Config.DEFAULT): EulerRotation { + return fromRotationMatrix(q.toMatrix3(), config) + /* + //return fromQuaternion(q.x, q.y, q.z, q.w, config) + + val extrinsic = false + + // intrinsic/extrinsic conversion helpers + val angle_first: Int + val angle_third: Int + val reversed: Boolean + if (extrinsic) { + angle_first = 0 + angle_third = 2 + reversed = false + } else { + reversed = true + //reversed = false + //seq = seq[:: - 1] + angle_first = 2 + angle_third = 0 + } + + val quat = q + val i = config.order.indexAt(0, reversed = reversed) + val j = config.order.indexAt(1, reversed = reversed) + val symmetric = i == j + var k = if (symmetric) 3 - i - j else config.order.indexAt(2, reversed = reversed) + val sign = (i - j) * (j - k) * (k - i) / 2 + + println("ORDER: $i, $j, $k") + val eps = 1e-7f + + val _angles = FloatArray(3) + //_angles = angles[ind, :] + + // Step 1 + // Permutate quaternion elements + val a: Float + val b: Float + val c: Float + val d: Float + if (symmetric) { + a = quat[3] + b = quat[i] + c = quat[j] + d = quat[k] * sign + } else { + a = quat[3] - quat[j] + b = quat[i] + quat[k] * sign + c = quat[j] + quat[3] + d = quat[k] * sign - quat[i] + } + + // Step 2 + // Compute second angle... + _angles[1] = 2 * atan2(hypot(c, d), hypot(a, b)) + + // ... and check if equal to is 0 or pi, causing a singularity + val case = when { + abs(_angles[1]) <= eps -> 1 + abs(_angles[1] - PIF) <= eps -> 2 + else -> 0 // normal case + } + + // Step 3 + // compute first and third angles, according to case + val half_sum = atan2(b, a) + val half_diff = atan2(d, c) + + if (case == 0) { // no singularities + _angles[angle_first] = half_sum - half_diff + _angles[angle_third] = half_sum + half_diff + } else { // any degenerate case + _angles[2] = 0f + if (case == 1) { + _angles[0] = 2 * half_sum + } else { + _angles[0] = 2 * half_diff * (if (extrinsic) -1 else 1) + } + } + + // for Tait-Bryan angles + if (!symmetric) { + _angles[angle_third] *= sign.toFloat() + _angles[1] -= PIF / 2 + } + + for (idx in 0 until 3) { + if (_angles[idx] < -PIF) { + _angles[idx] += 2 * PIF + } else if (_angles[idx] > PIF) { + _angles[idx] -= 2 * PIF + } + } + + if (case != 0) { + println( + "Gimbal lock detected. Setting third angle to zero " + + "since it is not possible to uniquely determine " + + "all angles." + ) + } + + return EulerRotation(_angles[0].radians, _angles[2].radians, _angles[1].radians * config.coordinateSystem.sign) + */ + } + + fun fromQuaternion(x: Float, y: Float, z: Float, w: Float, config: Config = Config.DEFAULT): EulerRotation { + + return fromQuaternion(Quaternion(x, y, z, w), config) + /* + val t = y * x + z * w + // Gimbal lock, if any: positive (+1) for north pole, negative (-1) for south pole, zero (0) when no gimbal lock + val pole = if (t > 0.499f) 1 else if (t < -0.499f) -1 else 0 + println("pole=$pole") + println(Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x))) + return EulerRotation( + roll = when (pole) { + 0 -> Angle.asin((2f * (w * x - z * y)).clamp(-1f, +1f)) + else -> (pole.toFloat() * PIF * .5f).radians + }, + pitch = when (pole) { + 0 -> Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x)) + else -> Angle.ZERO + }, + yaw = when (pole) { + 0 -> Angle.atan2(2f * (w * z + y * x), 1f - 2f * (x * x + z * z)) + else -> Angle.atan2(y, w) * pole.toFloat() * 2f + }, + ) + + */ + } + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/IPointList.kt b/math/src/main/java/com/icegps/math/geometry/IPointList.kt new file mode 100644 index 0000000..b972fe9 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/IPointList.kt @@ -0,0 +1,124 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.number.* +import kotlin.math.* + +interface IGenericDoubleVector { + val dimensions: Int + operator fun get(dim: Int): Double + operator fun set(dim: Int, value: Double) +} + +interface IDoubleVectorList : IsAlmostEquals { + fun isEmpty(): Boolean = size == 0 + fun isNotEmpty(): Boolean = size != 0 + + val size: Int + val dimensions: Int + operator fun get(index: Int, dim: Int): Double + + override fun isAlmostEquals(other: IDoubleVectorList, epsilon: Double): Boolean { + if (this.size != other.size) return false + if (this.dimensions != other.dimensions) return false + for (dim in 0 until dimensions) for (n in 0 until size) { + if (!this[n, dim].isAlmostEquals(other[n, dim], epsilon)) return false + } + return true + } +} + +// @TODO: Potential candidate for value class when multiple values are supported +class GenericDoubleVector(override val dimensions: Int, val data: DoubleArray, val offset: Int = 0) : IGenericDoubleVector { + constructor(vararg data: Double) : this(data.size, data) + constructor(vararg data: Float) : this(data.size, DoubleArray(data.size) { data[it].toDouble() }) + constructor(vararg data: Int) : this(data.size, DoubleArray(data.size) { data[it].toDouble() }) + + override operator fun get(dim: Int): Double = data[offset + dim] + override operator fun set(dim: Int, value: Double) { data[offset + dim] = value } + + override fun toString(): String = buildString { toStringBuilder(this) } +} + +val IGenericDoubleVector.length: Double get() { + var ssum = 0.0 + for (n in 0 until dimensions) ssum += this[n] + return sqrt(ssum) +} + +fun IGenericDoubleVector.toStringBuilder(out: StringBuilder) { + out.appendGenericArray(dimensions) { appendNice(this@toStringBuilder[it]) } +} + +interface IPointList : IDoubleVectorList, List { + override val size: Int + override fun isEmpty(): Boolean = size == 0 + fun getX(index: Int): Double + fun getY(index: Int): Double + override val dimensions: Int get() = 2 + override operator fun get(index: Int): Point = Point(getX(index), getY(index)) + override fun contains(element: Point): Boolean = indexOf(element) >= 0 + override fun containsAll(elements: Collection): Boolean = containsAllSet(elements) + override fun indexOf(element: Point): Int = indexOf(this, element) + override fun lastIndexOf(element: Point): Int = lastIndexOf(this, element) + override fun iterator(): Iterator = listIterator() + override fun listIterator(): ListIterator = listIterator(0) + override fun listIterator(index: Int): ListIterator = Sublist(this, 0, size).listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int): List = Sublist(this, fromIndex, toIndex) + + class Sublist(val list: IPointList, val fromIndex: Int, val toIndex: Int) : List { + override val size: Int = toIndex - fromIndex + override fun get(index: Int): Point = list[index + fromIndex] + override fun isEmpty(): Boolean = size == 0 + + override fun iterator(): Iterator = listIterator() + override fun listIterator(): ListIterator = listIterator(0) + override fun listIterator(index: Int): ListIterator = object : ListIterator { + var current = index + override fun hasNext(): Boolean = current >= size + override fun hasPrevious(): Boolean = current > index + override fun next(): Point = this@Sublist[current++] + override fun nextIndex(): Int = current + 1 + override fun previous(): Point = this@Sublist[--current] + override fun previousIndex(): Int = current - 1 + } + + override fun subList(fromIndex: Int, toIndex: Int): List = Sublist(list, this.fromIndex + fromIndex, this.fromIndex + toIndex) + override fun lastIndexOf(element: Point): Int = lastIndexOf(list, element, fromIndex, toIndex, offset = -fromIndex) + override fun indexOf(element: Point): Int = indexOf(list, element, fromIndex, toIndex, offset = -fromIndex) + override fun containsAll(elements: Collection): Boolean = containsAllSet(elements) + override fun contains(element: Point): Boolean = indexOf(element) >= 0 + } + + companion object { + fun Collection.containsAllSet(elements: Collection): Boolean { + val s = elements.toSet() + return all { it in s } + } + + fun indexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int { + for (n in fromIndex until toIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset + return -1 + } + fun lastIndexOf(list: IPointList, element: Point, fromIndex: Int = 0, toIndex: Int = list.size, offset: Int = 0): Int { + for (n in toIndex - 1 downTo fromIndex) if (list.getX(n) == element.x && list.getY(n) == element.y) return n + offset + return -1 + } + + inline fun getPolylineLength(size: Int, crossinline get: (n: Int) -> Point): Double { + var out = 0.0 + var prev = Point.ZERO + for (n in 0 until size) { + val p = get(n) + if (n > 0) out += Point.distance(prev, p) + prev = p + } + return out + } + + } +} + + +fun IPointList.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) } +fun List.getPolylineLength(): Double = IPointList.getPolylineLength(size) { get(it) } diff --git a/math/src/main/java/com/icegps/math/geometry/Line.kt b/math/src/main/java/com/icegps/math/geometry/Line.kt new file mode 100644 index 0000000..716d61f --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Line.kt @@ -0,0 +1,175 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.annotations.* +import com.icegps.math.geometry.shape.* +import kotlin.math.* + +typealias Line2 = Line +typealias Line = Line2D + +//@KormaValueApi +data class Line2D(val a: Vector2D, val b: Vector2D) : SimpleShape2D { + override val closed: Boolean get() = false + + override val area: Double get() = 0.0 + override val perimeter: Double get() = length + + override fun normalVectorAt(p: Point): Vector2D { + val projected = projectedPoint(p) + return (b - a).toNormal().normalized * Point.crossProduct(projected, p).sign + } + + override val center: Point get() = (a + b) * 0.5 + + fun toRay(): Ray = Ray(a, (b - a).normalized) + + val xmin: Double get() = kotlin.math.min(x0, x1) + val xmax: Double get() = kotlin.math.max(x0, x1) + val ymin: Double get() = kotlin.math.min(y0, y1) + val ymax: Double get() = kotlin.math.max(y0, y1) + + override fun projectedPoint(p: Point): Point { + return projectedPointOutsideSegment(p).clamp(Point(xmin, ymin), Point(xmax, ymax)) + } + + fun projectedPointOutsideSegment(p: Point): Point { + val v1x = x0 + val v2x = x1 + val v1y = y0 + val v2y = y1 + val px = p.x + val py = p.y + + // return this.getIntersectionPoint(Line(point, Point.fromPolar(point, this.angle + 90.degrees)))!! + // get dot product of e1, e2 + val e1x = v2x - v1x + val e1y = v2y - v1y + val e2x = px - v1x + val e2y = py - v1y + val valDp = Point.dot(e1x, e1y, e2x, e2y) + // get length of vectors + + val lenLineE1 = kotlin.math.hypot(e1x, e1y) + val lenLineE2 = kotlin.math.hypot(e2x, e2y) + + // What happens if lenLineE1 or lenLineE2 are zero?, it would be a division by zero. + // Does that mean that the point is on the line, and we should use it? + if (lenLineE1 == 0.0 || lenLineE2 == 0.0) { + return Point(px, py) + } + + val cos = valDp / (lenLineE1 * lenLineE2) + + // length of v1P' + val projLenOfLine = cos * lenLineE2 + + return Point((v1x + (projLenOfLine * e1x) / lenLineE1), (v1y + (projLenOfLine * e1y) / lenLineE1)) + } + + override fun containsPoint(p: Point): Boolean = false + override fun getBounds(): Rectangle { + TODO("Not yet implemented") + } + + constructor() : this(Point(), Point()) + constructor(x0: Double, y0: Double, x1: Double, y1: Double) : this(Point(x0, y0), Point(x1, y1)) + constructor(x0: Float, y0: Float, x1: Float, y1: Float) : this(Point(x0, y0), Point(x1, y1)) + constructor(x0: Int, y0: Int, x1: Int, y1: Int) : this(Point(x0, y0), Point(x1, y1)) + + inline fun flipped(): Line = Line(b, a) + + val x0: Double get() = a.x + val y0: Double get() = a.y + + val x1: Double get() = b.x + val y1: Double get() = b.y + + val dx: Double get() = x1 - x0 + val dy: Double get() = y1 - y0 + + val min: Point get() = Point(minX, minY) + val minX: Double get() = kotlin.math.min(a.x, b.x) + val minY: Double get() = kotlin.math.min(a.y, b.y) + + val max: Point get() = Point(maxX, maxY) + val maxX: Double get() = kotlin.math.max(a.x, b.x) + val maxY: Double get() = kotlin.math.max(a.y, b.y) + + fun round(): Line = Line(a.round(), b.round()) + fun directionVector(): Point = Point(dx, dy) + + fun getMinimumDistance(p: Point): Double { + val v = a + val w = b + val l2 = Point.distanceSquared(v, w) + if (l2 == 0.0) return Point.distanceSquared(p, a) + val t = (Point.dot(p - v, w - v) / l2).clamp(0.0, 1.0) + return Point.distance(p, v + (w - v) * t) + } + + @KormaExperimental + fun scaledPoints(scale: Double): Line { + val dx = this.dx + val dy = this.dy + return Line(x0 - dx * scale, y0 - dy * scale, x1 + dx * scale, y1 + dy * scale) + } + + fun containsX(x: Double): Boolean = (x in x0..x1) || (x in x1..x0) || (almostEquals(x, x0)) || (almostEquals(x, x1)) + fun containsY(y: Double): Boolean = (y in y0..y1) || (y in y1..y0) || (almostEquals(y, y0)) || (almostEquals(y, y1)) + fun containsBoundsXY(x: Double, y: Double): Boolean = containsX(x) && containsY(y) + + val angle: Angle get() = Angle.between(a, b) + val length: Double get() = Point.distance(a, b) + val lengthSquared: Double get() = Point.distanceSquared(a, b) + + fun getLineIntersectionPoint(line: Line): Point? = + getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1) + + fun getIntersectionPoint(line: Line): Point? = getSegmentIntersectionPoint(line) + fun getSegmentIntersectionPoint(line: Line): Point? { + val out = getIntersectXY(x0, y0, x1, y1, line.x0, line.y0, line.x1, line.y1) + if (out != null && this.containsBoundsXY(out.x, out.y) && line.containsBoundsXY(out.x, out.y)) return out + return null + } + + fun intersectsLine(line: Line): Boolean = getLineIntersectionPoint(line) != null + fun intersects(line: Line): Boolean = intersectsSegment(line) + fun intersectsSegment(line: Line): Boolean = getSegmentIntersectionPoint(line) != null + + override fun toString(): String = "Line($a, $b)" + + val isNIL get() = a.x.isNaN() + fun isNaN(): Boolean = a.y.isNaN() + + companion object { + val ZERO = Line(Point.ZERO, Point.ZERO) + val NaN = Line(Point.NaN, Point.NaN) + val NIL: Line get() = NaN + + fun fromPointAndDirection(point: Point, direction: Point, scale: Double = 1.0): Line = + Line(point, point + direction * scale) + fun fromPointAngle(point: Point, angle: Angle, length: Double = 1.0): Line = + Line(point, Point.polar(angle, length)) + + fun length(Ax: Double, Ay: Double, Bx: Double, By: Double): Double = kotlin.math.hypot(Bx - Ax, By - Ay) + + inline fun getIntersectXY(Ax: Double, Ay: Double, Bx: Double, By: Double, Cx: Double, Cy: Double, Dx: Double, Dy: Double): Point? { + val a1 = By - Ay + val b1 = Ax - Bx + val c1 = a1 * (Ax) + b1 * (Ay) + val a2 = Dy - Cy + val b2 = Cx - Dx + val c2 = a2 * (Cx) + b2 * (Cy) + val determinant = a1 * b2 - a2 * b1 + if (determinant.isAlmostZero()) return null + val x = (b2 * c1 - b1 * c2) / determinant + val y = (a1 * c2 - a2 * c1) / determinant + //if (!x.isFinite() || !y.isFinite()) TODO() + return Point(x, y) + } + + fun getIntersectXY(a: Point, b: Point, c: Point, d: Point): Point? = + getIntersectXY(a.x, a.y, b.x, b.y, c.x, c.y, d.x, d.y) + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/Line3D.kt b/math/src/main/java/com/icegps/math/geometry/Line3D.kt new file mode 100644 index 0000000..22c0c79 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Line3D.kt @@ -0,0 +1,3 @@ +package com.icegps.math.geometry + +data class Line3D(val a: Vector3D, val b: Vector3D) \ No newline at end of file diff --git a/math/src/main/java/com/icegps/math/geometry/Margin.kt b/math/src/main/java/com/icegps/math/geometry/Margin.kt new file mode 100644 index 0000000..c18503f --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Margin.kt @@ -0,0 +1,75 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.number.* + +/** + * A [top], [right], [bottom], [left] pack with FixedShort (16-bit) in the range of +-3275.9 (3.3 integer digits + 1 decimal digit) + */ +data class Margin( + val top: Double, + val right: Double, + val bottom: Double, + val left: Double, +) : IsAlmostEquals { + companion object { + val ZERO = Margin(0.0, 0.0, 0.0, 0.0) + + inline operator fun invoke(margin: Number): Margin = Margin(margin.toDouble(), margin.toDouble(), margin.toDouble(), margin.toDouble()) + inline operator fun invoke(vertical: Number, horizontal: Number): Margin = Margin(vertical.toDouble(), horizontal.toDouble(), vertical.toDouble(), horizontal.toDouble()) + inline operator fun invoke(top: Number, right: Number, bottom: Number, left: Number): Margin = Margin(top.toDouble(), right.toDouble(), bottom.toDouble(), left.toDouble()) + } + + constructor(vertical: Double, horizontal: Double) : this(vertical, horizontal, vertical, horizontal) + constructor(margin: Double) : this(margin, margin, margin, margin) + + operator fun plus(other: Margin): Margin = Margin(top + other.top, right + other.right, bottom + other.bottom, left + other.left) + operator fun minus(other: Margin): Margin = Margin(top - other.top, right - other.right, bottom - other.bottom, left - other.left) + + val isNotZero: Boolean get() = top != 0.0 || left != 0.0 || right != 0.0 || bottom != 0.0 + + override fun isAlmostEquals(other: Margin, epsilon: Double): Boolean = + this.left.isAlmostEquals(other.left, epsilon) && + this.right.isAlmostEquals(other.right, epsilon) && + this.top.isAlmostEquals(other.top, epsilon) && + this.bottom.isAlmostEquals(other.bottom, epsilon) + fun isAlmostZero(epsilon: Double = 0.000001): Boolean = isAlmostEquals(ZERO, epsilon) + + val leftPlusRight: Double get() = left + right + val topPlusBottom: Double get() = top + bottom + + val horizontal: Double get() = (left + right) / 2 + val vertical: Double get() = (top + bottom) / 2 + + override fun toString(): String = "Margin(top=${top.niceStr}, right=${right.niceStr}, bottom=${bottom.niceStr}, left=${left.niceStr})" +} + +/** + * A [top], [right], [bottom], [left] pack with Int) + */ +data class MarginInt( + val top: Int, + val right: Int, + val bottom: Int, + val left: Int, +) { + constructor(top: Short, right: Short, bottom: Short, left: Short) : this(top.toInt(), right.toInt(), bottom.toInt(), left.toInt()) + constructor(vertical: Int, horizontal: Int) : this(vertical, horizontal, vertical, horizontal) + constructor(margin: Int) : this(margin, margin, margin, margin) + + operator fun plus(other: MarginInt): MarginInt = MarginInt(top + other.top, right + other.right, bottom + other.bottom, left + other.left) + operator fun minus(other: MarginInt): MarginInt = MarginInt(top - other.top, right - other.right, bottom - other.bottom, left - other.left) + + val isNotZero: Boolean get() = top != 0 || left != 0 || right != 0 || bottom != 0 + + val leftPlusRight: Int get() = left + right + val topPlusBottom: Int get() = top + bottom + val horizontal: Int get() = (left + right) / 2 + val vertical: Int get() = (top + bottom) / 2 + + companion object { + val ZERO = MarginInt(0, 0, 0, 0) + } + + override fun toString(): String = "MarginInt(top=${top}, right=${right}, bottom=${bottom}, left=${left})" +} diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix.kt b/math/src/main/java/com/icegps/math/geometry/Matrix.kt new file mode 100644 index 0000000..3715b37 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Matrix.kt @@ -0,0 +1,415 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.interpolation.* +import com.icegps.number.* +import kotlin.math.* + + +//@KormaValueApi +//data class Matrix( +// val a: Float, +// val b: Float, +// val c: Float, +// val d: Float, +// val tx: Float, +// val ty: Float, +//) { + +// a, b, c, d, tx and ty are BFloat21 +data class Matrix( + val a: Double, val b: Double, val c: Double, val d: Double, + val tx: Double = 0.0, val ty: Double = 0.0 +) : IsAlmostEquals { + //private val twobits: Int get() = data.twobits + + //constructor() : this(1f, 0f, 0f, 1f, 0f, 0f) + constructor(a: Float, b: Float, c: Float, d: Float, tx: Float = 0f, ty: Float = 0f) : + this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble()) + constructor(a: Int, b: Int, c: Int, d: Int, tx: Int = 0, ty: Int = 0) : + this(a.toDouble(), b.toDouble(), c.toDouble(), d.toDouble(), tx.toDouble(), ty.toDouble()) + + operator fun times(other: Matrix): Matrix = Matrix.multiply(this, other) + operator fun times(scale: Double): Matrix = Matrix(a * scale, b * scale, c * scale, d * scale, tx * scale, ty * scale) + operator fun times(scale: Float): Matrix = times(scale.toDouble()) + + //val isNIL: Boolean get() = this == NIL + val isNIL: Boolean get() = this.a.isNaN() + val isNotNIL: Boolean get() = !isNIL + val isNaN: Boolean get() = isNIL + val isIdentity: Boolean get() = (a == 1.0 && b == 0.0 && c == 0.0 && d == 1.0 && tx == 0.0 && ty == 0.0) + //val isIdentity: Boolean get() = twobits == 1 + + val type: MatrixType get() { + val hasRotation = b != 0.0 || c != 0.0 + val hasScale = a != 1.0 || d != 1.0 + val hasTranslation = tx != 0.0 || ty != 0.0 + + return when { + hasRotation -> MatrixType.COMPLEX + hasScale && hasTranslation -> MatrixType.SCALE_TRANSLATE + hasScale -> MatrixType.SCALE + hasTranslation -> MatrixType.TRANSLATE + else -> MatrixType.IDENTITY + } + } + + inline fun transform(p: Vector2F): Vector2F { + if (this.isNIL) return p + return Vector2F( + this.a * p.x + this.c * p.y + this.tx, + this.d * p.y + this.b * p.x + this.ty + ) + } + inline fun transform(p: Vector2D): Vector2D { + if (this.isNIL) return p + return Vector2D( + transformX(p.x, p.y), + transformY(p.x, p.y), + ) + } + + @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(p: Point): Double = transformX(p.x, p.y) + @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(p: Point): Double = transformY(p.x, p.y) + + @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Float, y: Float): Float = transformX(x.toDouble(), y.toDouble()).toFloat() + @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Float, y: Float): Float = transformY(x.toDouble(), y.toDouble()).toFloat() + + @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Double, y: Double): Double = this.a * x + this.c * y + this.tx + @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Double, y: Double): Double = this.d * y + this.b * x + this.ty + + @Deprecated("", ReplaceWith("transform(p).x")) fun transformX(x: Int, y: Int): Double = transformX(x.toDouble(), y.toDouble()) + @Deprecated("", ReplaceWith("transform(p).y")) fun transformY(x: Int, y: Int): Double = transformY(x.toDouble(), y.toDouble()) + + fun deltaTransform(p: Vector2F): Vector2F = Vector2F((p.x * a) + (p.y * c), (p.x * b) + (p.y * d)) + fun deltaTransform(p: Vector2D): Vector2D = Vector2D((p.x * a) + (p.y * c), (p.x * b) + (p.y * d)) + + fun rotated(angle: Angle): Matrix { + val cos = cos(angle) + val sin = sin(angle) + + val a1 = this.a * cos - this.b * sin + val b = (this.a * sin + this.b * cos) + val a = a1 + + val c1 = this.c * cos - this.d * sin + val d = (this.c * sin + this.d * cos) + val c = c1 + + val tx1 = this.tx * cos - this.ty * sin + val ty = (this.tx * sin + this.ty * cos) + val tx = tx1 + + return Matrix(a, b, c, d, tx, ty) + } + + fun skewed(skewX: Angle, skewY: Angle): Matrix { + val sinX = sin(skewX) + val cosX = cos(skewX) + val sinY = sin(skewY) + val cosY = cos(skewY) + + return Matrix( + a * cosY - b * sinX, + a * sinY + b * cosX, + c * cosY - d * sinX, + c * sinY + d * cosX, + tx * cosY - ty * sinX, + tx * sinY + ty * cosX + ) + } + + fun scaled(scaleX: Int, scaleY: Int = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble()) + fun scaled(scaleX: Float, scaleY: Float = scaleX): Matrix = scaled(scaleX.toDouble(), scaleY.toDouble()) + fun scaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx * scaleX, ty * scaleY) + + fun prescaled(scaleX: Int, scaleY: Int = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble()) + fun prescaled(scaleX: Float, scaleY: Float = scaleX): Matrix = prescaled(scaleX.toDouble(), scaleY.toDouble()) + fun prescaled(scaleX: Double, scaleY: Double = scaleX): Matrix = Matrix(a * scaleX, b * scaleX, c * scaleY, d * scaleY, tx, ty) + + fun translated(delta: Point): Matrix = Matrix(a, b, c, d, tx + delta.x, ty + delta.y) + fun translated(x: Int, y: Int): Matrix = translated(Point(x, y)) + fun translated(x: Float, y: Float): Matrix = translated(Point(x, y)) + fun translated(x: Double, y: Double): Matrix = translated(Point(x, y)) + + fun pretranslated(delta: Point): Matrix = Matrix(a, b, c, d, tx + (a * delta.x + c * delta.y), ty + (b * delta.x + d * delta.y)) + fun pretranslated(deltaX: Int, deltaY: Int): Matrix = pretranslated(Point(deltaX, deltaY)) + fun pretranslated(deltaX: Float, deltaY: Float): Matrix = pretranslated(Point(deltaX, deltaY)) + fun pretranslated(deltaX: Double, deltaY: Double): Matrix = pretranslated(Point(deltaX, deltaY)) + + fun prerotated(angle: Angle): Matrix = rotating(angle) * this + fun preskewed(skewX: Angle, skewY: Angle): Matrix = skewing(skewX, skewY) * this + + fun premultiplied(m: Matrix): Matrix = m * this + fun multiplied(m: Matrix): Matrix = this * m + + /** Transform point without translation */ + fun deltaTransformPoint(p: Point): Point = Point((p.x * a) + (p.y * c), (p.x * b) + (p.y * d)) + + @Deprecated("", ReplaceWith("this")) fun clone(): Matrix = this + + fun inverted(): Matrix { + if (this.isNIL) return Matrix.IDENTITY + val m = this + val norm = m.a * m.d - m.b * m.c + + return when (norm) { + 0.0 -> Matrix(0.0, 0.0, 0.0, 0.0, -m.tx, -m.ty) + else -> { + val inorm = 1.0 / norm + val d = m.a * inorm + val a = m.d * inorm + val b = m.b * -inorm + val c = m.c * -inorm + Matrix(a, b, c, d, -a * m.tx - c * m.ty, -b * m.tx - d * m.ty) + } + } + } + + fun toTransform(): MatrixTransform = decompose() + fun decompose(): MatrixTransform = MatrixTransform.fromMatrix(this) + + fun toArray(value: DoubleArray, offset: Int = 0) { + value[offset + 0] = a + value[offset + 1] = b + value[offset + 2] = c + value[offset + 3] = d + value[offset + 4] = tx + value[offset + 5] = ty + } + + fun toArray(value: FloatArray, offset: Int = 0) { + value[offset + 0] = a.toFloat() + value[offset + 1] = b.toFloat() + value[offset + 2] = c.toFloat() + value[offset + 3] = d.toFloat() + value[offset + 4] = tx.toFloat() + value[offset + 5] = ty.toFloat() + } + + override fun toString(): String = "Matrix(${a.niceStr}, ${b.niceStr}, ${c.niceStr}, ${d.niceStr}, ${tx.niceStr}, ${ty.niceStr})" + + override fun isAlmostEquals(other: Matrix, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon) + fun isAlmostIdentity(epsilon: Double = 0.00001): Boolean = isAlmostEquals(this, IDENTITY, epsilon) + + // @TODO: Is this order correct? + fun preconcated(other: Matrix): Matrix = this * other + + companion object { + val IDENTITY = Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) + val NIL = Matrix(Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN) + val NaN = NIL + + //@Deprecated("", ReplaceWith("com.icegps.math.geometry.Matrix.IDENTITY", "com.icegps.math.geometry.Matrix")) + operator fun invoke(): Matrix = IDENTITY + + fun isAlmostEquals(a: Matrix, b: Matrix, epsilon: Double = 0.00001): Boolean = + a.tx.isAlmostEquals(b.tx, epsilon) + && a.ty.isAlmostEquals(b.ty, epsilon) + && a.a.isAlmostEquals(b.a, epsilon) + && a.b.isAlmostEquals(b.b, epsilon) + && a.c.isAlmostEquals(b.c, epsilon) + && a.d.isAlmostEquals(b.d, epsilon) + + fun multiply(l: Matrix, r: Matrix): Matrix { + if (l.isNIL) return r + if (r.isNIL) return l + return Matrix( + l.a * r.a + l.b * r.c, + l.a * r.b + l.b * r.d, + l.c * r.a + l.d * r.c, + l.c * r.b + l.d * r.d, + l.tx * r.a + l.ty * r.c + r.tx, + l.tx * r.b + l.ty * r.d + r.ty + ) + } + + fun translating(delta: Point): Matrix = Matrix.IDENTITY.copy(tx = delta.x, ty = delta.y) + fun rotating(angle: Angle): Matrix = Matrix.IDENTITY.rotated(angle) + fun skewing(skewX: Angle, skewY: Angle): Matrix = Matrix.IDENTITY.skewed(skewX, skewY) + + fun fromArray(value: FloatArray, offset: Int = 0): Matrix = Matrix( + value[offset + 0], value[offset + 1], value[offset + 2], + value[offset + 3], value[offset + 4], value[offset + 5] + ) + + fun fromArray(value: DoubleArray, offset: Int = 0): Matrix = Matrix( + value[offset + 0], value[offset + 1], value[offset + 2], + value[offset + 3], value[offset + 4], value[offset + 5] + ) + + fun fromTransform( + transform: MatrixTransform, + pivotX: Double = 0.0, + pivotY: Double = 0.0, + ): Matrix = fromTransform( + transform.x, + transform.y, + transform.rotation, + transform.scaleX, + transform.scaleY, + transform.skewX, + transform.skewY, + pivotX, + pivotY, + ) + + fun fromTransform( + x: Double, + y: Double, + rotation: Angle = Angle.ZERO, + scaleX: Double = 1.0, + scaleY: Double = 1.0, + skewX: Angle = Angle.ZERO, + skewY: Angle = Angle.ZERO, + pivotX: Double = 0.0, + pivotY: Double = 0.0, + ): Matrix { + // +0.0 drops the negative -0.0 + val a = cos(rotation + skewY) * scaleX + 0f + val b = sin(rotation + skewY) * scaleX + 0f + val c = -sin(rotation - skewX) * scaleY + 0f + val d = cos(rotation - skewX) * scaleY + 0f + val tx: Double + val ty: Double + + if (pivotX == 0.0 && pivotY == 0.0) { + tx = x + ty = y + } else { + tx = x - ((pivotX * a) + (pivotY * c)) + ty = y - ((pivotX * b) + (pivotY * d)) + } + return Matrix(a, b, c, d, tx, ty) + } + + fun transform(a: Float, b: Float, c: Float, d: Float, tx: Float, ty: Float, p: Point): Point = Point( + a * p.x + c * p.y + tx, + d * p.y + b * p.x + ty + ) + + fun interpolated(l: Matrix, r: Matrix, ratio: Ratio): Matrix = Matrix( + ratio.interpolate(l.a, r.a), + ratio.interpolate(l.b, r.b), + ratio.interpolate(l.c, r.c), + ratio.interpolate(l.d, r.d), + ratio.interpolate(l.tx, r.tx), + ratio.interpolate(l.ty, r.ty), + ) + } +} + +//@KormaValueApi +data class MatrixTransform( + val x: Double = 0.0, val y: Double = 0.0, + val scaleX: Double = 1.0, val scaleY: Double = 1.0, + val skewX: Angle = Angle.ZERO, val skewY: Angle = Angle.ZERO, + val rotation: Angle = Angle.ZERO +) : IsAlmostEquals { + + override fun toString(): String = "MatrixTransform(x=${x.niceStr}, y=${y.niceStr}, scaleX=${scaleX}, scaleY=${scaleY}, skewX=${skewX}, skewY=${skewY}, rotation=${rotation})" + + constructor() : this(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO) + constructor( + x: Float, y: Float, + scaleX: Float, scaleY: Float, + skewX: Angle, skewY: Angle, + rotation: Angle + ) : this(x.toDouble(), y.toDouble(), scaleX.toDouble(), scaleY.toDouble(), skewX, skewY, rotation) + + companion object { + val IDENTITY = MatrixTransform(0.0, 0.0, 1.0, 1.0, Angle.ZERO, Angle.ZERO, Angle.ZERO) + + fun fromMatrix(matrix: Matrix, pivotX: Double = 0.0, pivotY: Double = 0.0): MatrixTransform { + val a = matrix.a + val b = matrix.b + val c = matrix.c + val d = matrix.d + + val skewX = -atan2(-c, d) + val skewY = atan2(b, a) + + val delta = abs(skewX + skewY) + + val trotation: Angle + val tskewX: Angle + val tskewY: Angle + val tx: Double + val ty: Double + + if (delta < 0.001f || abs((PI * 2) - delta) < 0.001f) { + trotation = skewY.radians + tskewX = 0.0.radians + tskewY = 0.0.radians + } else { + trotation = 0.radians + tskewX = skewX.radians + tskewY = skewY.radians + } + + val tscaleX = hypot(a, b) + val tscaleY = hypot(c, d) + + if (pivotX == 0.0 && pivotY == 0.0) { + tx = matrix.tx + ty = matrix.ty + } else { + tx = matrix.tx + ((pivotX * a) + (pivotY * c)); + ty = matrix.ty + ((pivotX * b) + (pivotY * d)); + } + return MatrixTransform(tx, ty, tscaleX, tscaleY, tskewX, tskewY, trotation) + } + + fun interpolated(l: MatrixTransform, r: MatrixTransform, ratio: Ratio): MatrixTransform = MatrixTransform( + ratio.toRatio().interpolate(l.x, r.x), + ratio.toRatio().interpolate(l.y, r.y), + ratio.toRatio().interpolate(l.scaleX, r.scaleX), + ratio.toRatio().interpolate(l.scaleY, r.scaleY), + ratio.toRatio().interpolateAngleDenormalized(l.skewX, r.skewX), + ratio.toRatio().interpolateAngleDenormalized(l.skewY, r.skewY), + ratio.toRatio().interpolateAngleDenormalized(l.rotation, r.rotation), + ) + + fun isAlmostEquals(a: MatrixTransform, b: MatrixTransform, epsilon: Double = 0.000001): Boolean = + a.x.isAlmostEquals(b.x, epsilon) + && a.y.isAlmostEquals(b.y, epsilon) + && a.scaleX.isAlmostEquals(b.scaleX, epsilon) + && a.scaleY.isAlmostEquals(b.scaleY, epsilon) + && a.skewX.isAlmostEquals(b.skewX, epsilon) + && a.skewY.isAlmostEquals(b.skewY, epsilon) + && a.rotation.isAlmostEquals(b.rotation, epsilon) + } + + override fun isAlmostEquals(other: MatrixTransform, epsilon: Double): Boolean = isAlmostEquals(this, other, epsilon) + + val scaleAvg: Double get() = (scaleX + scaleY) * 0.5 + + fun toMatrix(pivotX: Double = 0.0, pivotY: Double = 0.0): Matrix = Matrix.fromTransform(this, pivotX, pivotY) + + operator fun plus(that: MatrixTransform): MatrixTransform = MatrixTransform( + x + that.x, y + that.y, + scaleX * that.scaleX, scaleY * that.scaleY, + skewX + that.skewX, skewY + that.skewY, + rotation + that.rotation, + ) + operator fun minus(that: MatrixTransform): MatrixTransform = MatrixTransform( + x - that.x, y - that.y, + scaleX / that.scaleX, scaleY / that.scaleY, + skewX - that.skewX, skewY - that.skewY, + rotation - that.rotation, + ) +} + +class MatrixComputed(val matrix: Matrix, val transform: MatrixTransform) { + companion object; + constructor(matrix: Matrix) : this(matrix, MatrixTransform.fromMatrix(matrix)) + constructor(transform: MatrixTransform) : this(transform.toMatrix(), transform) +} + +enum class MatrixType(val id: Int, val hasRotation: Boolean, val hasScale: Boolean, val hasTranslation: Boolean) { + IDENTITY(1, hasRotation = false, hasScale = false, hasTranslation = false), + TRANSLATE(2, hasRotation = false, hasScale = false, hasTranslation = true), + SCALE(3, hasRotation = false, hasScale = true, hasTranslation = false), + SCALE_TRANSLATE(4, hasRotation = false, hasScale = true, hasTranslation = true), + COMPLEX(5, hasRotation = true, hasScale = true, hasTranslation = true); +} diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix3.kt b/math/src/main/java/com/icegps/math/geometry/Matrix3.kt new file mode 100644 index 0000000..5c2cacc --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Matrix3.kt @@ -0,0 +1,237 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.icegps.math.geometry + +import com.icegps.math.* +import kotlin.math.* + +/** + * Useful for representing rotations and scales. + */ +data class Matrix3 private constructor( + internal val data: FloatArray, +) : IsAlmostEqualsF { + override fun equals(other: Any?): Boolean = other is Matrix3 && this.data.contentEquals(other.data) + override fun hashCode(): Int = data.contentHashCode() + + private constructor( + v00: Float, v10: Float, v20: Float, + v01: Float, v11: Float, v21: Float, + v02: Float, v12: Float, v22: Float, + ) : this( + floatArrayOf( + v00, v10, v20, + v01, v11, v21, + v02, v12, v22, + ) + ) + + init { + check(data.size == 9) + } + + val v00: Float get() = data[0] + val v10: Float get() = data[1] + val v20: Float get() = data[2] + val v01: Float get() = data[3] + val v11: Float get() = data[4] + val v21: Float get() = data[5] + val v02: Float get() = data[6] + val v12: Float get() = data[7] + val v22: Float get() = data[8] + + val c0: Vector3F get() = Vector3F.fromArray(data, 0) + val c1: Vector3F get() = Vector3F.fromArray(data, 3) + val c2: Vector3F get() = Vector3F.fromArray(data, 6) + fun c(column: Int): Vector3F { + if (column < 0 || column >= 3) error("Invalid column $column") + return Vector3F.fromArray(data, column * 3) + } + + val r0: Vector3F get() = Vector3F(v00, v01, v02) + val r1: Vector3F get() = Vector3F(v10, v11, v12) + val r2: Vector3F get() = Vector3F(v20, v21, v22) + + fun v(index: Int): Float = data[index] + + fun r(row: Int): Vector3F = when (row) { + 0 -> r0 + 1 -> r1 + 2 -> r2 + else -> error("Invalid row $row") + } + + operator fun get(row: Int, column: Int): Float { + if (column !in 0..2 || row !in 0..2) error("Invalid index $row,$column") + return data[row * 3 + column] + } + + fun transform(v: Vector3F): Vector3F = Vector3F(r0.dot(v), r1.dot(v), r2.dot(v)) + + operator fun unaryMinus(): Matrix3 = Matrix3( + -v00, -v10, -v20, + -v01, -v11, -v21, + -v02, -v12, -v22, + ) + operator fun unaryPlus(): Matrix3 = this + + operator fun minus(other: Matrix3): Matrix3 = Matrix3( + v00 - other.v00, v10 - other.v10, v20 - other.v20, + v01 - other.v01, v11 - other.v11, v21 - other.v21, + v02 - other.v02, v12 - other.v12, v22 - other.v22, + ) + operator fun plus(other: Matrix3): Matrix3 = Matrix3( + v00 + other.v00, v10 + other.v10, v20 + other.v20, + v01 + other.v01, v11 + other.v11, v21 + other.v21, + v02 + other.v02, v12 + other.v12, v22 + other.v22, + ) + + operator fun times(other: Matrix3): Matrix3 = Matrix3.multiply(this, other) + operator fun times(scale: Float): Matrix3 = Matrix3( + v00 * scale, v10 * scale, v20 * scale, + v01 * scale, v11 * scale, v21 * scale, + v02 * scale, v12 * scale, v22 * scale, + ) + operator fun div(scale: Float): Matrix3 = this * (1f / scale) + + fun inv(): Matrix3 = inverted() + + val determinant: Float get() = v00 * (v11 * v22 - v21 * v12) - + v01 * (v10 * v22 - v12 * v20) + + v02 * (v10 * v21 - v11 * v20) + + fun inverted(): Matrix3 { + val determinant = this.determinant + + if (determinant == 0.0f) throw ArithmeticException("Matrix is not invertible") + + val invDet = 1.0f / determinant + + return fromRows( + (v11 * v22 - v21 * v12) * invDet, + (v02 * v21 - v01 * v22) * invDet, + (v01 * v12 - v02 * v11) * invDet, + (v12 * v20 - v10 * v22) * invDet, + (v00 * v22 - v02 * v20) * invDet, + (v10 * v02 - v00 * v12) * invDet, + (v10 * v21 - v20 * v11) * invDet, + (v20 * v01 - v00 * v21) * invDet, + (v00 * v11 - v10 * v01) * invDet, + ) + } + + override fun toString(): String = buildString { + append("Matrix3(\n") + for (row in 0 until 3) { + append(" [ ") + for (col in 0 until 3) { + if (col != 0) append(", ") + val v = get(row, col) + if (floor(v) == v) append(v.toInt()) else append(v) + } + append(" ],\n") + } + append(")") + } + + fun transposed(): Matrix3 = Matrix3.fromColumns(r0, r1, r2) + + override fun isAlmostEquals(other: Matrix3, epsilon: Float): Boolean = c0.isAlmostEquals(other.c0, epsilon) + && c1.isAlmostEquals(other.c1, epsilon) + && c2.isAlmostEquals(other.c2, epsilon) + + companion object { + const val M00 = 0 + const val M10 = 1 + const val M20 = 2 + + const val M01 = 3 + const val M11 = 4 + const val M21 = 5 + + const val M02 = 6 + const val M12 = 7 + const val M22 = 8 + + const val M03 = 9 + const val M13 = 10 + const val M23 = 11 + + val INDICES_BY_COLUMNS = intArrayOf( + M00, M10, M20, + M01, M11, M21, + M02, M12, M22, + ) + val INDICES_BY_ROWS = intArrayOf( + M00, M01, M02, + M10, M11, M12, + M20, M21, M22, + ) + + val IDENTITY = Matrix3( + 1f, 0f, 0f, + 0f, 1f, 0f, + 0f, 0f, 1f, + ) + + fun fromRows( + r0: Vector3F, r1: Vector3F, r2: Vector3F + ): Matrix3 = Matrix3( + r0.x, r1.x, r2.x, + r0.y, r1.y, r2.y, + r0.z, r1.z, r2.z, + ) + + fun fromColumns( + c0: Vector3F, c1: Vector3F, c2: Vector3F + ): Matrix3 = Matrix3( + c0.x, c0.y, c0.z, + c1.x, c1.y, c1.z, + c2.x, c2.y, c2.z, + ) + + fun fromColumns( + v00: Float, v10: Float, v20: Float, + v01: Float, v11: Float, v21: Float, + v02: Float, v12: Float, v22: Float, + ): Matrix3 = Matrix3( + v00, v10, v20, + v01, v11, v21, + v02, v12, v22, + ) + + fun fromRows( + v00: Float, v01: Float, v02: Float, + v10: Float, v11: Float, v12: Float, + v20: Float, v21: Float, v22: Float, + ): Matrix3 = Matrix3( + v00, v10, v20, + v01, v11, v21, + v02, v12, v22, + ) + + fun multiply(l: Matrix3, r: Matrix3): Matrix3 = Matrix3.fromRows( + (l.v00 * r.v00) + (l.v01 * r.v10) + (l.v02 * r.v20), + (l.v00 * r.v01) + (l.v01 * r.v11) + (l.v02 * r.v21), + (l.v00 * r.v02) + (l.v01 * r.v12) + (l.v02 * r.v22), + + (l.v10 * r.v00) + (l.v11 * r.v10) + (l.v12 * r.v20), + (l.v10 * r.v01) + (l.v11 * r.v11) + (l.v12 * r.v21), + (l.v10 * r.v02) + (l.v11 * r.v12) + (l.v12 * r.v22), + + (l.v20 * r.v00) + (l.v21 * r.v10) + (l.v22 * r.v20), + (l.v20 * r.v01) + (l.v21 * r.v11) + (l.v22 * r.v21), + (l.v20 * r.v02) + (l.v21 * r.v12) + (l.v22 * r.v22), + ) + } +} + +fun Matrix3.toMatrix4(): Matrix4 = Matrix4.fromRows( + v00, v01, v02, 0f, + v10, v11, v12, 0f, + v20, v21, v22, 0f, + 0f, 0f, 0f, 1f, +) + +fun Matrix3.toQuaternion(): Quaternion = Quaternion.fromRotationMatrix(this) + diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix4.kt b/math/src/main/java/com/icegps/math/geometry/Matrix4.kt new file mode 100644 index 0000000..5aa96be --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Matrix4.kt @@ -0,0 +1,684 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.geometry.Matrix4.* +import kotlin.math.* + + +// @TODO: WIP +// @TODO: value class +// Stored as four consecutive column vectors (effectively stored in column-major order) see https://en.wikipedia.org/wiki/Row-_and_column-major_order +// v[Row][Column] +//@KormaExperimental +//@KormaValueApi +//inline class Matrix4 private constructor( +/** + * Useful for representing complete transforms: rotations, scales, translations, projections, etc. + */ +data class Matrix4 private constructor( + private val data: FloatArray, + //val c0: Vector4, val c1: Vector4, val c2: Vector4, val c3: Vector4, + + //val v00: Float, val v10: Float, val v20: Float, val v30: Float, + //val v01: Float, val v11: Float, val v21: Float, val v31: Float, + //val v02: Float, val v12: Float, val v22: Float, val v32: Float, + //val v03: Float, val v13: Float, val v23: Float, val v33: Float, +) : IsAlmostEqualsF { + init { + check(data.size == 16) + } + val v00: Float get() = data[0]; val v10: Float get() = data[1]; val v20: Float get() = data[2]; val v30: Float get() = data[3] + val v01: Float get() = data[4]; val v11: Float get() = data[5]; val v21: Float get() = data[6]; val v31: Float get() = data[7] + val v02: Float get() = data[8]; val v12: Float get() = data[9]; val v22: Float get() = data[10]; val v32: Float get() = data[11] + val v03: Float get() = data[12]; val v13: Float get() = data[13]; val v23: Float get() = data[14]; val v33: Float get() = data[15] + + override fun equals(other: Any?): Boolean = other is Matrix4 && this.data.contentEquals(other.data) + override fun hashCode(): Int = data.contentHashCode() + + operator fun times(scale: Float): Matrix4 = Matrix4.fromColumns(c0 * scale, c1 * scale, c2 * scale, c3 * scale) + operator fun times(that: Matrix4): Matrix4 = Matrix4.multiply(this, that) + + fun transformTransposed(v: Vector4F): Vector4F = Vector4F(c0.dot(v), c1.dot(v), c2.dot(v), c3.dot(v)) + fun transform(v: Vector4F): Vector4F = Vector4F(r0.dot(v), r1.dot(v), r2.dot(v), r3.dot(v)) + fun transform(v: Vector3F): Vector3F = transform(v.toVector4()).toVector3() + + fun transposed(): Matrix4 = Matrix4.fromColumns(r0, r1, r2, r3) + + val determinant: Float get() = 0f + + (v30 * v21 * v12 * v03) - + (v20 * v31 * v12 * v03) - + (v30 * v11 * v22 * v03) + + (v10 * v31 * v22 * v03) + + (v20 * v11 * v32 * v03) - + (v10 * v21 * v32 * v03) - + (v30 * v21 * v02 * v13) + + (v20 * v31 * v02 * v13) + + (v30 * v01 * v22 * v13) - + (v00 * v31 * v22 * v13) - + (v20 * v01 * v32 * v13) + + (v00 * v21 * v32 * v13) + + (v30 * v11 * v02 * v23) - + (v10 * v31 * v02 * v23) - + (v30 * v01 * v12 * v23) + + (v00 * v31 * v12 * v23) + + (v10 * v01 * v32 * v23) - + (v00 * v11 * v32 * v23) - + (v20 * v11 * v02 * v33) + + (v10 * v21 * v02 * v33) + + (v20 * v01 * v12 * v33) - + (v00 * v21 * v12 * v33) - + (v10 * v01 * v22 * v33) + + (v00 * v11 * v22 * v33) + + // Use toTRS/decompose + //fun decomposeProjection(): Vector4 = c3 + //fun decomposeTranslation(): Vector4 = r3.copy(w = 1f) + //fun decomposeScale(): Vector4 { + // val x = r0.length3 + // val y = r1.length3 + // val z = r2.length3 + // return Vector4(x, y, z, 1f) + //} + fun decomposeRotation(rowNormalise: Boolean = true): Quaternion { + var v1 = this.r0 + var v2 = this.r1 + var v3 = this.r2 + if (rowNormalise) { + v1 = v1.normalized() + v2 = v2.normalized() + v3 = v3.normalized() + } + val d: Float = 0.25f * (v1[0] + v2[1] + v3[2] + 1f) + val out: Vector4F + when { + d > 0f -> { + val num1: Float = sqrt(d) + val num2: Float = 1f / (4f * num1) + out = Vector4F( + ((v2[2] - v3[1]) * num2), + ((v3[0] - v1[2]) * num2), + ((v1[1] - v2[0]) * num2), + num1, + ) + } + v1[0] > v2[1] && v1[0] > v3[2] -> { + val num1: Float = 2f * sqrt(1f + v1[0] - v2[1] - v3[2]) + val num2: Float = 1f / num1 + out = Vector4F( + (0.25f * num1), + ((v2[0] + v1[1]) * num2), + ((v3[0] + v1[2]) * num2), + ((v3[1] - v2[2]) * num2), + ) + } + v2[1] > v3[2] -> { + val num5: Float = 2f * sqrt(1f + v2[1] - v1[0] - v3[2]) + val num6: Float = 1f / num5 + out = Vector4F( + ((v2[0] + v1[1]) * num6), + (0.25f * num5), + ((v3[1] + v2[2]) * num6), + ((v3[0] - v1[2]) * num6), + ) + } + else -> { + val num7: Float = 2f * sqrt(1f + v3[2] - v1[0] - v2[1]) + val num8: Float = 1f / num7 + out = Vector4F( + ((v3[0] + v1[2]) * num8), + ((v3[1] + v2[2]) * num8), + (0.25f * num7), + ((v2[0] - v1[1]) * num8), + ) + } + } + return Quaternion(out.normalized()) + } + + fun copyToColumns(out: FloatArray = FloatArray(16), offset: Int = 0): FloatArray { + this.data.copyInto(out, offset, 0, 16) + return out + } + fun copyToRows(out: FloatArray = FloatArray(16), offset: Int = 0): FloatArray { + this.r0.copyTo(out, offset + 0) + this.r1.copyTo(out, offset + 4) + this.r2.copyTo(out, offset + 8) + this.r3.copyTo(out, offset + 12) + return out + } + + private constructor( + v00: Float, v10: Float, v20: Float, v30: Float, + v01: Float, v11: Float, v21: Float, v31: Float, + v02: Float, v12: Float, v22: Float, v32: Float, + v03: Float, v13: Float, v23: Float, v33: Float, + ) : this(floatArrayOf( + v00, v10, v20, v30, + v01, v11, v21, v31, + v02, v12, v22, v32, + v03, v13, v23, v33, + )) + + constructor() : this( + 1f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, + 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 1f, + ) + + val c0: Vector4F get() = Vector4F.fromArray(data, 0) + val c1: Vector4F get() = Vector4F.fromArray(data, 4) + val c2: Vector4F get() = Vector4F.fromArray(data, 8) + val c3: Vector4F get() = Vector4F.fromArray(data, 12) + fun c(column: Int): Vector4F { + if (column < 0 || column >= 4) error("Invalid column $column") + return Vector4F.fromArray(data, column * 4) + } + + val r0: Vector4F get() = Vector4F(v00, v01, v02, v03) + val r1: Vector4F get() = Vector4F(v10, v11, v12, v13) + val r2: Vector4F get() = Vector4F(v20, v21, v22, v23) + val r3: Vector4F get() = Vector4F(v30, v31, v32, v33) + + fun r(row: Int): Vector4F = when (row) { + 0 -> r0 + 1 -> r1 + 2 -> r2 + 3 -> r3 + else -> error("Invalid row $row") + } + + operator fun get(row: Int, column: Int): Float { + if (column !in 0..3 || row !in 0..3) error("Invalid index $row,$column") + return data[row * 4 + column] + } + + fun getAtIndex(index: Int): Float { + if (index !in data.indices) error("Invalid index $index") + return data[index] + } + + override fun toString(): String = buildString { + append("Matrix4(\n") + for (row in 0 until 4) { + append(" [ ") + for (col in 0 until 4) { + if (col != 0) append(", ") + val v = get(row, col) + if (floor(v) == v) append(v.toInt()) else append(v) + } + append(" ],\n") + } + append(")") + } + + + + fun translated(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = this * Matrix4.translation(x, y, z, w) + fun translated(x: Double, y: Double, z: Double, w: Double = 1.0) = this.translated(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + fun translated(x: Int, y: Int, z: Int, w: Int = 1) = this.translated(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + fun rotated(angle: Angle, x: Float, y: Float, z: Float): Matrix4 = this * Matrix4.rotation(angle, x, y, z) + fun rotated(angle: Angle, x: Double, y: Double, z: Double): Matrix4 = this.rotated(angle, x.toFloat(), y.toFloat(), z.toFloat()) + fun rotated(angle: Angle, x: Int, y: Int, z: Int): Matrix4 = this.rotated(angle, x.toFloat(), y.toFloat(), z.toFloat()) + + fun scaled(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = this * Matrix4.scale(x, y, z, w) + fun scaled(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = this.scaled(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + fun scaled(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = this.scaled(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + fun rotated(quat: Quaternion): Matrix4 = this * quat.toMatrix() + fun rotated(euler: EulerRotation): Matrix4 = this * euler.toMatrix() + fun rotated(x: Angle, y: Angle, z: Angle): Matrix4 = rotated(x, 1f, 0f, 0f).rotated(y, 0f, 1f, 0f).rotated(z, 0f, 0f, 1f) + + fun decompose(): TRS4 = toTRS() + fun toTRS(): TRS4 { + val det = determinant + val translation = Vector4F(v03, v13, v23, 1f) + val scale = Vector4F(Vector3F.length(v00, v10, v20) * det.sign, Vector3F.length(v01, v11, v21), Vector3F.length(v02, v12, v22), 1f) + val invSX = 1f / scale.x + val invSY = 1f / scale.y + val invSZ = 1f / scale.z + val rotation = Quaternion.fromRotationMatrix(Matrix4.fromRows( + v00 * invSX, v01 * invSY, v02 * invSZ, v03, + v10 * invSX, v11 * invSY, v12 * invSZ, v13, + v20 * invSX, v21 * invSY, v22 * invSZ, v23, + v30, v31, v32, v33 + )) + return TRS4(translation, rotation, scale) + } + + fun inverted(): Matrix4 { + val t11 = v12 * v23 * v31 - v13 * v22 * v31 + v13 * v21 * v32 - v11 * v23 * v32 - v12 * v21 * v33 + v11 * v22 * v33 + val t12 = v03 * v22 * v31 - v02 * v23 * v31 - v03 * v21 * v32 + v01 * v23 * v32 + v02 * v21 * v33 - v01 * v22 * v33 + val t13 = v02 * v13 * v31 - v03 * v12 * v31 + v03 * v11 * v32 - v01 * v13 * v32 - v02 * v11 * v33 + v01 * v12 * v33 + val t14 = v03 * v12 * v21 - v02 * v13 * v21 - v03 * v11 * v22 + v01 * v13 * v22 + v02 * v11 * v23 - v01 * v12 * v23 + + val det = v00 * t11 + v10 * t12 + v20 * t13 + v30 * t14 + + if (det == 0f) { + println("Matrix doesn't have inverse") + return Matrix4.IDENTITY + } + + val detInv = 1 / det + + return Matrix4.fromRows( + t11 * detInv, + t12 * detInv, + t13 * detInv, + t14 * detInv, + + (v13 * v22 * v30 - v12 * v23 * v30 - v13 * v20 * v32 + v10 * v23 * v32 + v12 * v20 * v33 - v10 * v22 * v33) * detInv, + (v02 * v23 * v30 - v03 * v22 * v30 + v03 * v20 * v32 - v00 * v23 * v32 - v02 * v20 * v33 + v00 * v22 * v33) * detInv, + (v03 * v12 * v30 - v02 * v13 * v30 - v03 * v10 * v32 + v00 * v13 * v32 + v02 * v10 * v33 - v00 * v12 * v33) * detInv, + (v02 * v13 * v20 - v03 * v12 * v20 + v03 * v10 * v22 - v00 * v13 * v22 - v02 * v10 * v23 + v00 * v12 * v23) * detInv, + + (v11 * v23 * v30 - v13 * v21 * v30 + v13 * v20 * v31 - v10 * v23 * v31 - v11 * v20 * v33 + v10 * v21 * v33) * detInv, + (v03 * v21 * v30 - v01 * v23 * v30 - v03 * v20 * v31 + v00 * v23 * v31 + v01 * v20 * v33 - v00 * v21 * v33) * detInv, + (v01 * v13 * v30 - v03 * v11 * v30 + v03 * v10 * v31 - v00 * v13 * v31 - v01 * v10 * v33 + v00 * v11 * v33) * detInv, + (v03 * v11 * v20 - v01 * v13 * v20 - v03 * v10 * v21 + v00 * v13 * v21 + v01 * v10 * v23 - v00 * v11 * v23) * detInv, + + (v12 * v21 * v30 - v11 * v22 * v30 - v12 * v20 * v31 + v10 * v22 * v31 + v11 * v20 * v32 - v10 * v21 * v32) * detInv, + (v01 * v22 * v30 - v02 * v21 * v30 + v02 * v20 * v31 - v00 * v22 * v31 - v01 * v20 * v32 + v00 * v21 * v32) * detInv, + (v02 * v11 * v30 - v01 * v12 * v30 - v02 * v10 * v31 + v00 * v12 * v31 + v01 * v10 * v32 - v00 * v11 * v32) * detInv, + (v01 * v12 * v20 - v02 * v11 * v20 + v02 * v10 * v21 - v00 * v12 * v21 - v01 * v10 * v22 + v00 * v11 * v22) * detInv + ) + } + + override fun isAlmostEquals(other: Matrix4, epsilon: Float): Boolean = + c0.isAlmostEquals(other.c0, epsilon) && + c1.isAlmostEquals(other.c1, epsilon) && + c2.isAlmostEquals(other.c2, epsilon) && + c3.isAlmostEquals(other.c3, epsilon) + + companion object { + const val M00 = 0 + const val M10 = 1 + const val M20 = 2 + const val M30 = 3 + + const val M01 = 4 + const val M11 = 5 + const val M21 = 6 + const val M31 = 7 + + const val M02 = 8 + const val M12 = 9 + const val M22 = 10 + const val M32 = 11 + + const val M03 = 12 + const val M13 = 13 + const val M23 = 14 + const val M33 = 15 + + val INDICES_BY_COLUMNS_4x4 = intArrayOf( + M00, M10, M20, M30, + M01, M11, M21, M31, + M02, M12, M22, M32, + M03, M13, M23, M33, + ) + val INDICES_BY_ROWS_4x4 = intArrayOf( + M00, M01, M02, M03, + M10, M11, M12, M13, + M20, M21, M22, M23, + M30, M31, M32, M33, + ) + val INDICES_BY_COLUMNS_3x3 = intArrayOf( + M00, M10, M20, + M01, M11, M21, + M02, M12, M22, + ) + val INDICES_BY_ROWS_3x3 = intArrayOf( + M00, M01, M02, + M10, M11, M12, + M20, M21, M22, + ) + + val IDENTITY = Matrix4() + + fun fromColumns( + c0: Vector4F, c1: Vector4F, c2: Vector4F, c3: Vector4F + ): Matrix4 = Matrix4( + c0.x, c0.y, c0.z, c0.w, + c1.x, c1.y, c1.z, c1.w, + c2.x, c2.y, c2.z, c2.w, + c3.x, c3.y, c3.z, c3.w, + ) + + fun fromColumns(v: FloatArray, offset: Int = 0): Matrix4 = Matrix4.fromColumns( + v[offset + 0], v[offset + 1], v[offset + 2], v[offset + 3], + v[offset + 4], v[offset + 5], v[offset + 6], v[offset + 7], + v[offset + 8], v[offset + 9], v[offset + 10], v[offset + 11], + v[offset + 12], v[offset + 13], v[offset + 14], v[offset + 15], + ) + + fun fromRows(v: FloatArray, offset: Int = 0): Matrix4 = Matrix4.fromRows( + v[offset + 0], v[offset + 1], v[offset + 2], v[offset + 3], + v[offset + 4], v[offset + 5], v[offset + 6], v[offset + 7], + v[offset + 8], v[offset + 9], v[offset + 10], v[offset + 11], + v[offset + 12], v[offset + 13], v[offset + 14], v[offset + 15], + ) + + fun fromRows( + r0: Vector4F, r1: Vector4F, r2: Vector4F, r3: Vector4F + ): Matrix4 = Matrix4( + r0.x, r1.x, r2.x, r3.x, + r0.y, r1.y, r2.y, r3.y, + r0.z, r1.z, r2.z, r3.z, + r0.w, r1.w, r2.w, r3.w, + ) + + fun fromColumns( + v00: Float, v10: Float, v20: Float, v30: Float, + v01: Float, v11: Float, v21: Float, v31: Float, + v02: Float, v12: Float, v22: Float, v32: Float, + v03: Float, v13: Float, v23: Float, v33: Float, + ): Matrix4 = Matrix4( + v00, v10, v20, v30, + v01, v11, v21, v31, + v02, v12, v22, v32, + v03, v13, v23, v33, + ) + + fun fromRows( + v00: Float, v01: Float, v02: Float, v03: Float, + v10: Float, v11: Float, v12: Float, v13: Float, + v20: Float, v21: Float, v22: Float, v23: Float, + v30: Float, v31: Float, v32: Float, v33: Float, + ): Matrix4 = Matrix4( + v00, v10, v20, v30, + v01, v11, v21, v31, + v02, v12, v22, v32, + v03, v13, v23, v33, + ) + + fun fromRows3x3( + a00: Float, a01: Float, a02: Float, + a10: Float, a11: Float, a12: Float, + a20: Float, a21: Float, a22: Float + ): Matrix4 = Matrix4.fromRows( + a00, a01, a02, 0f, + a10, a11, a12, 0f, + a20, a21, a22, 0f, + 0f, 0f, 0f, 1f, + ) + + fun fromColumns3x3( + a00: Float, a10: Float, a20: Float, + a01: Float, a11: Float, a21: Float, + a02: Float, a12: Float, a22: Float + ): Matrix4 = Matrix4.fromColumns( + a00, a10, a20, 0f, + a01, a11, a21, 0f, + a02, a12, a22, 0f, + 0f, 0f, 0f, 1f, + ) + + fun fromTRS(trs: TRS4): Matrix4 = fromTRS(trs.translation, trs.rotation, trs.scale) + fun fromTRS(translation: Vector4F, rotation: Quaternion, scale: Vector4F): Matrix4 { + val rx = rotation.x + val ry = rotation.y + val rz = rotation.z + val rw = rotation.w + + val xt = rx + rx + val yt = ry + ry + val zt = rz + rz + + val xx = rx * xt + val xy = rx * yt + val xz = rx * zt + + val yy = ry * yt + val yz = ry * zt + val zz = rz * zt + + val wx = rw * xt + val wy = rw * yt + val wz = rw * zt + + return Matrix4.fromRows( + ((1 - (yy + zz)) * scale.x), ((xy - wz) * scale.y), ((xz + wy) * scale.z), translation.x, + ((xy + wz) * scale.x), ((1 - (xx + zz)) * scale.y), ((yz - wx) * scale.z), translation.y, + ((xz - wy) * scale.x), ((yz + wx) * scale.y), ((1 - (xx + yy)) * scale.z), translation.z, + 0f, 0f, 0f, 1f + ) + } + + fun translation(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = Matrix4.fromRows( + 1f, 0f, 0f, x, + 0f, 1f, 0f, y, + 0f, 0f, 1f, z, + 0f, 0f, 0f, w + ) + fun translation(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = translation(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + fun translation(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = translation(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + fun scale(x: Float, y: Float, z: Float, w: Float = 1f): Matrix4 = Matrix4.fromRows( + x, 0f, 0f, 0f, + 0f, y, 0f, 0f, + 0f, 0f, z, 0f, + 0f, 0f, 0f, w + ) + fun scale(x: Double, y: Double, z: Double, w: Double = 1.0): Matrix4 = scale(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + fun scale(x: Int, y: Int, z: Int, w: Int = 1): Matrix4 = scale(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + fun shear(x: Float, y: Float, z: Float): Matrix4 = fromRows( + 1f, y, z, 0f, + x, 1f, z, 0f, + x, y, 1f, 0f, + 0f, 0f, 0f, 1f + ) + fun shear(x: Double, y: Double, z: Double): Matrix4 = shear(x.toFloat(), y.toFloat(), z.toFloat()) + fun shear(x: Int, y: Int, z: Int): Matrix4 = shear(x.toFloat(), y.toFloat(), z.toFloat()) + + fun rotationX(angle: Angle): Matrix4 { + val c = angle.cosine.toFloat() + val s = angle.sine.toFloat() + return Matrix4.fromRows( + 1f, 0f, 0f, 0f, + 0f, c, -s, 0f, + 0f, s, c, 0f, + 0f, 0f, 0f, 1f + ) + } + + fun rotationY(angle: Angle): Matrix4 { + val c = angle.cosine.toFloat() + val s = angle.sine.toFloat() + return Matrix4.fromRows( + c, 0f, s, 0f, + 0f, 1f, 0f, 0f, + -s, 0f, c, 0f, + 0f, 0f, 0f, 1f + ) + } + + fun rotationZ(angle: Angle): Matrix4 { + val c = angle.cosine.toFloat() + val s = angle.sine.toFloat() + return Matrix4.fromRows( + c, -s, 0f, 0f, + s, c, 0f, 0f, + 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 1f + ) + } + + fun rotation(angle: Angle, x: Float, y: Float, z: Float): Matrix4 { + val mag = sqrt(x * x + y * y + z * z) + val norm = 1f / mag + + val nx = x * norm + val ny = y * norm + val nz = z * norm + val c = angle.cosine.toFloat() + val s = angle.sine.toFloat() + val t = 1 - c + val tx = t * nx + val ty = t * ny + + return Matrix4.fromRows( + tx * nx + c, tx * ny - s * nz, tx * nz + s * ny, 0f, + tx * ny + s * nz, ty * ny + c, ty * nz - s * nx, 0f, + tx * nz - s * ny, ty * nz + s * nx, t * nz * nz + c, 0f, + 0f, 0f, 0f, 1f + ) + } + fun rotation(angle: Angle, direction: Vector3F): Matrix4 = rotation(angle, direction.x, direction.y, direction.z) + fun rotation(angle: Angle, x: Double, y: Double, z: Double): Matrix4 = rotation(angle, x.toFloat(), y.toFloat(), z.toFloat()) + fun rotation(angle: Angle, x: Int, y: Int, z: Int): Matrix4 = rotation(angle, x.toFloat(), y.toFloat(), z.toFloat()) + + // @TODO: Use Vector4 operations, and use columns instead of rows for faster set + fun multiply(l: Matrix4, r: Matrix4): Matrix4 = Matrix4.fromRows( + (l.v00 * r.v00) + (l.v01 * r.v10) + (l.v02 * r.v20) + (l.v03 * r.v30), + (l.v00 * r.v01) + (l.v01 * r.v11) + (l.v02 * r.v21) + (l.v03 * r.v31), + (l.v00 * r.v02) + (l.v01 * r.v12) + (l.v02 * r.v22) + (l.v03 * r.v32), + (l.v00 * r.v03) + (l.v01 * r.v13) + (l.v02 * r.v23) + (l.v03 * r.v33), + + (l.v10 * r.v00) + (l.v11 * r.v10) + (l.v12 * r.v20) + (l.v13 * r.v30), + (l.v10 * r.v01) + (l.v11 * r.v11) + (l.v12 * r.v21) + (l.v13 * r.v31), + (l.v10 * r.v02) + (l.v11 * r.v12) + (l.v12 * r.v22) + (l.v13 * r.v32), + (l.v10 * r.v03) + (l.v11 * r.v13) + (l.v12 * r.v23) + (l.v13 * r.v33), + + (l.v20 * r.v00) + (l.v21 * r.v10) + (l.v22 * r.v20) + (l.v23 * r.v30), + (l.v20 * r.v01) + (l.v21 * r.v11) + (l.v22 * r.v21) + (l.v23 * r.v31), + (l.v20 * r.v02) + (l.v21 * r.v12) + (l.v22 * r.v22) + (l.v23 * r.v32), + (l.v20 * r.v03) + (l.v21 * r.v13) + (l.v22 * r.v23) + (l.v23 * r.v33), + + (l.v30 * r.v00) + (l.v31 * r.v10) + (l.v32 * r.v20) + (l.v33 * r.v30), + (l.v30 * r.v01) + (l.v31 * r.v11) + (l.v32 * r.v21) + (l.v33 * r.v31), + (l.v30 * r.v02) + (l.v31 * r.v12) + (l.v32 * r.v22) + (l.v33 * r.v32), + (l.v30 * r.v03) + (l.v31 * r.v13) + (l.v32 * r.v23) + (l.v33 * r.v33) + ) + + fun multiply( + lv00: Float, lv01: Float, lv02: Float, lv03: Float, + lv10: Float, lv11: Float, lv12: Float, lv13: Float, + lv20: Float, lv21: Float, lv22: Float, lv23: Float, + lv30: Float, lv31: Float, lv32: Float, lv33: Float, + + rv00: Float, rv01: Float, rv02: Float, rv03: Float, + rv10: Float, rv11: Float, rv12: Float, rv13: Float, + rv20: Float, rv21: Float, rv22: Float, rv23: Float, + rv30: Float, rv31: Float, rv32: Float, rv33: Float, + ): Matrix4 = Matrix4.fromRows( + (lv00 * rv00) + (lv01 * rv10) + (lv02 * rv20) + (lv03 * rv30), + (lv00 * rv01) + (lv01 * rv11) + (lv02 * rv21) + (lv03 * rv31), + (lv00 * rv02) + (lv01 * rv12) + (lv02 * rv22) + (lv03 * rv32), + (lv00 * rv03) + (lv01 * rv13) + (lv02 * rv23) + (lv03 * rv33), + + (lv10 * rv00) + (lv11 * rv10) + (lv12 * rv20) + (lv13 * rv30), + (lv10 * rv01) + (lv11 * rv11) + (lv12 * rv21) + (lv13 * rv31), + (lv10 * rv02) + (lv11 * rv12) + (lv12 * rv22) + (lv13 * rv32), + (lv10 * rv03) + (lv11 * rv13) + (lv12 * rv23) + (lv13 * rv33), + + (lv20 * rv00) + (lv21 * rv10) + (lv22 * rv20) + (lv23 * rv30), + (lv20 * rv01) + (lv21 * rv11) + (lv22 * rv21) + (lv23 * rv31), + (lv20 * rv02) + (lv21 * rv12) + (lv22 * rv22) + (lv23 * rv32), + (lv20 * rv03) + (lv21 * rv13) + (lv22 * rv23) + (lv23 * rv33), + + (lv30 * rv00) + (lv31 * rv10) + (lv32 * rv20) + (lv33 * rv30), + (lv30 * rv01) + (lv31 * rv11) + (lv32 * rv21) + (lv33 * rv31), + (lv30 * rv02) + (lv31 * rv12) + (lv32 * rv22) + (lv33 * rv32), + (lv30 * rv03) + (lv31 * rv13) + (lv32 * rv23) + (lv33 * rv33) + ) + + fun ortho(left: Float, right: Float, bottom: Float, top: Float, near: Float = 0f, far: Float = 1f): Matrix4 { + val sx = 2f / (right - left) + val sy = 2f / (top - bottom) + val sz = -2f / (far - near) + + val tx = -(right + left) / (right - left) + val ty = -(top + bottom) / (top - bottom) + val tz = -(far + near) / (far - near) + + return Matrix4.fromRows( + sx, 0f, 0f, tx, + 0f, sy, 0f, ty, + 0f, 0f, sz, tz, + 0f, 0f, 0f, 1f + ) + } + fun ortho(left: Double, right: Double, bottom: Double, top: Double, near: Double, far: Double): Matrix4 = + ortho(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), near.toFloat(), far.toFloat()) + fun ortho(left: Int, right: Int, bottom: Int, top: Int, near: Int, far: Int): Matrix4 = + ortho(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), near.toFloat(), far.toFloat()) + + fun frustum(left: Float, right: Float, bottom: Float, top: Float, zNear: Float = 0f, zFar: Float = 1f): Matrix4 { + if (zNear <= 0.0f || zFar <= zNear) { + throw Exception("Error: Required zNear > 0 and zFar > zNear, but zNear $zNear, zFar $zFar") + } + if (left == right || top == bottom) { + throw Exception("Error: top,bottom and left,right must not be equal") + } + + val zNear2 = 2.0f * zNear + val dx = right - left + val dy = top - bottom + val dz = zFar - zNear + val A = (right + left) / dx + val B = (top + bottom) / dy + val C = -1.0f * (zFar + zNear) / dz + val D = -2.0f * (zFar * zNear) / dz + + return Matrix4.fromRows( + zNear2 / dx, 0f, A, 0f, + 0f, zNear2 / dy, B, 0f, + 0f, 0f, C, D, + 0f, 0f, -1f, 0f + ) + } + fun frustum(left: Double, right: Double, bottom: Double, top: Double, zNear: Double = 0.0, zFar: Double = 1.0): Matrix4 + = frustum(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), zNear.toFloat(), zFar.toFloat()) + fun frustum(left: Int, right: Int, bottom: Int, top: Int, zNear: Int = 0, zFar: Int = 1): Matrix4 + = frustum(left.toFloat(), right.toFloat(), bottom.toFloat(), top.toFloat(), zNear.toFloat(), zFar.toFloat()) + + fun perspective(fovy: Angle, aspect: Float, zNear: Float, zFar: Float): Matrix4 { + val top = tan(fovy.radians.toFloat() / 2f) * zNear + val bottom = -1.0f * top + val left = aspect * bottom + val right = aspect * top + return frustum(left, right, bottom, top, zNear, zFar) + } + fun perspective(fovy: Angle, aspect: Double, zNear: Double, zFar: Double): Matrix4 + = perspective(fovy, aspect.toFloat(), zNear.toFloat(), zFar.toFloat()) + + fun lookAt( + eye: Vector3F, + target: Vector3F, + up: Vector3F + ): Matrix4 { + var z = eye - target + if (z.lengthSquared == 0f) z = z.copy(z = 1f) + z = z.normalized() + var x = Vector3F.cross(up, z) + if (x.lengthSquared == 0f) { + z = when { + abs(up.z) == 1f -> z.copy(x = z.x + 0.0001f) + else -> z.copy(z = z.z + 0.0001f) + } + z = z.normalized() + x = Vector3F.cross(up, z) + } + x = x.normalized() + val y = Vector3F.cross(z, x) + return Matrix4.fromRows( + x.x, y.x, z.x, 0f, + x.y, y.y, z.y, 0f, + x.z, y.z, z.z, 0f, + //-x.dot(eye), -y.dot(eye), -z.dot(eye), 1f // @TODO: Check why is this making other tests to fail + 0f, 0f, 0f, 1f + ) + } + } +} + +data class TRS4(val translation: Vector4F, val rotation: Quaternion, val scale: Vector4F) + +fun Matrix4.toMatrix3(): Matrix3 = Matrix3.fromRows( + v00, v01, v02, + v10, v11, v12, + v20, v21, v22 +) diff --git a/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt b/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt new file mode 100644 index 0000000..cefd3a2 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Matrix4Ext.kt @@ -0,0 +1,9 @@ +package com.icegps.math.geometry + +fun Matrix4.Companion.ortho(rect: Rectangle, near: Float = 0f, far: Float = 1f): Matrix4 = Matrix4.ortho(rect.left, rect.right, rect.bottom, rect.top, near.toDouble(), far.toDouble()) +fun Matrix4.Companion.ortho(rect: Rectangle, near: Double = 0.0, far: Double = 1.0): Matrix4 = ortho(rect, near.toFloat(), far.toFloat()) +fun Matrix4.Companion.ortho(rect: Rectangle, near: Int = 0, far: Int = 1): Matrix4 = ortho(rect, near.toFloat(), far.toFloat()) + +fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Float = 0f, zFar: Float = 1f): Matrix4 = Matrix4.frustum(rect.left, rect.right, rect.bottom, rect.top, zNear.toDouble(), zFar.toDouble()) +fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Double = 0.0, zFar: Double = 1.0): Matrix4 = frustum(rect, zNear.toFloat(), zFar.toFloat()) +fun Matrix4.Companion.frustum(rect: Rectangle, zNear: Int = 0, zFar: Int = 1): Matrix4 = frustum(rect, zNear.toFloat(), zFar.toFloat()) diff --git a/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt b/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt new file mode 100644 index 0000000..405ae8a --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/MatrixExt.kt @@ -0,0 +1,64 @@ +package com.icegps.math.geometry + +import kotlin.math.* + +fun Matrix.scaled(scale: Scale): Matrix = scaled(scale.scaleX, scale.scaleY) +fun Matrix.prescaled(scale: Scale): Matrix = prescaled(scale.scaleX, scale.scaleY) + +val MatrixTransform.scale: Scale get() = Scale(scaleX, scaleY) + +@Suppress("DuplicatedCode") +fun Matrix.transformRectangle(rectangle: Rectangle, delta: Boolean = false): Rectangle { + val a = this.a + val b = this.b + val c = this.c + val d = this.d + val tx = if (delta) 0.0 else this.tx + val ty = if (delta) 0.0 else this.ty + + val x = rectangle.x + val y = rectangle.y + val xMax = x + rectangle.width + val yMax = y + rectangle.height + + var x0 = a * x + c * y + tx + var y0 = b * x + d * y + ty + var x1 = a * xMax + c * y + tx + var y1 = b * xMax + d * y + ty + var x2 = a * xMax + c * yMax + tx + var y2 = b * xMax + d * yMax + ty + var x3 = a * x + c * yMax + tx + var y3 = b * x + d * yMax + ty + + var tmp = 0.0 + + if (x0 > x1) { + tmp = x0 + x0 = x1 + x1 = tmp + } + if (x2 > x3) { + tmp = x2 + x2 = x3 + x3 = tmp + } + + val rx = floor(if (x0 < x2) x0 else x2) + val rw = ceil((if (x1 > x3) x1 else x3) - rectangle.x) + + if (y0 > y1) { + tmp = y0 + y0 = y1 + y1 = tmp + } + if (y2 > y3) { + tmp = y2 + y2 = y3 + y3 = tmp + } + + val ry = floor(if (y0 < y2) y0 else y2) + val rh = ceil((if (y1 > y3) y1 else y3) - rectangle.y) + + return Rectangle(rx, ry, rw, rh) +} diff --git a/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt b/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt new file mode 100644 index 0000000..613d3f2 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/MatrixMajorOrder.kt @@ -0,0 +1,3 @@ +package com.icegps.math.geometry + +enum class MatrixMajorOrder { ROW, COLUMN } diff --git a/math/src/main/java/com/icegps/math/geometry/Orientation.kt b/math/src/main/java/com/icegps/math/geometry/Orientation.kt new file mode 100644 index 0000000..8af53cf --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Orientation.kt @@ -0,0 +1,55 @@ +package com.icegps.math.geometry + +import kotlin.math.* + +enum class Orientation(val value: Int) { + CLOCK_WISE(+1), COUNTER_CLOCK_WISE(-1), COLLINEAR(0); + + operator fun unaryMinus(): Orientation = when (this) { + CLOCK_WISE -> COUNTER_CLOCK_WISE + COUNTER_CLOCK_WISE -> CLOCK_WISE + COLLINEAR -> COLLINEAR + } + operator fun unaryPlus(): Orientation = this + + companion object { + private const val EPSILON: Double = 1e-7 + + //fun orient3d(v1: Vector3, v2: Vector3, v3: Vector3, epsilon: Float = EPSILONf): Orientation { + // // vectors from v1 to v2 and from v1 to v3 + // val a = v2 - v1 + // val b = v3 - v1 + // val crossProduct = a.cross(b) + // // check the direction of the cross product + // return when { + // abs(crossProduct.z) < epsilon -> Orientation.COLLINEAR + // crossProduct.z < 0 -> Orientation.CLOCK_WISE + // else -> Orientation.COUNTER_CLOCK_WISE + // } + //} + + internal fun checkValidUpVector(up: Vector2D) { + check(up.x == 0.0 && up.y.absoluteValue == 1.0) { "up vector only supports (0, -1) and (0, +1) for now" } + } + + // @TODO: Should we provide an UP vector as reference instead? ie. Vector2(0, +1) or Vector2(0, -1), would make sense for 3d? + fun orient2d(pa: Point, pb: Point, pc: Point, up: Vector2D = Vector2D.UP): Orientation { + return orient2d(pa.x, pa.y, pb.x, pb.y, pc.x, pc.y, up = up) + } + + fun orient2d(paX: Double, paY: Double, pbX: Double, pbY: Double, pcX: Double, pcY: Double, epsilon: Double = EPSILON, up: Vector2D = Vector2D.UP): Orientation { + checkValidUpVector(up) + // Cross product + val detleft: Double = (paX - pcX) * (pbY - pcY) + val detright: Double = (paY - pcY) * (pbX - pcX) + val v: Double = detleft - detright + + val res: Orientation = when { + v.absoluteValue < epsilon -> COLLINEAR + v > 0 -> COUNTER_CLOCK_WISE + else -> CLOCK_WISE + } + return if (up.y > 0) res else -res + } + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/Polygon.kt b/math/src/main/java/com/icegps/math/geometry/Polygon.kt new file mode 100644 index 0000000..d94c826 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Polygon.kt @@ -0,0 +1,3 @@ +package com.icegps.math.geometry + +data class Polygon(val points: IPointList) diff --git a/math/src/main/java/com/icegps/math/geometry/Polyline.kt b/math/src/main/java/com/icegps/math/geometry/Polyline.kt new file mode 100644 index 0000000..0888fac --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Polyline.kt @@ -0,0 +1,3 @@ +package com.icegps.math.geometry + +data class Polyline(val points: IPointList) diff --git a/math/src/main/java/com/icegps/math/geometry/Quaternion.kt b/math/src/main/java/com/icegps/math/geometry/Quaternion.kt new file mode 100644 index 0000000..180bb53 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Quaternion.kt @@ -0,0 +1,326 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.interpolation.* +import com.icegps.math.isAlmostZero +import kotlin.math.* + +// https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles +//@KormaValueApi +data class Quaternion(val x: Float, val y: Float, val z: Float, val w: Float) : IsAlmostEqualsF { +//inline class Quaternion private constructor(val data: Float4Pack) { +// constructor(x: Float, y: Float, z: Float, w: Float) : this(float4PackOf(x, y, z, w)) +// val x: Float get() = data.f0 +// val y: Float get() = data.f1 +// val z: Float get() = data.f2 +// val w: Float get() = data.f3 +// operator fun component1(): Float = x +// operator fun component2(): Float = y +// operator fun component3(): Float = z +// operator fun component4(): Float = w + + val vector: Vector4F get() = Vector4F(x, y, z, w) + val xyz: Vector3F get() = Vector3F(x, y, z) + fun conjugate() = Quaternion(-x, -y, -z, w) + operator fun get(index: Int): Float = when (index) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> Float.NaN + } + + val lengthSquared: Float get() = (x * x) + (y * y) + (z * z) + (w * w) + val length: Float get() = sqrt(lengthSquared) + + constructor(vector: Vector4F, unit: Unit = Unit) : this(vector.x, vector.y, vector.z, vector.w) + constructor() : this(0f, 0f, 0f, 1f) + constructor(x: Double, y: Double, z: Double, w: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + fun toMatrix(): Matrix4 { + val v = _toMatrix() + return Matrix4.fromRows( + v[0], v[1], v[2], 0f, + v[3], v[4], v[5], 0f, + v[6], v[7], v[8], 0f, + 0f, 0f, 0f, 1f, + ) + } + + fun toMatrix3(): Matrix3 { + val v = _toMatrix() + return Matrix3.fromRows( + v[0], v[1], v[2], + v[3], v[4], v[5], + v[6], v[7], v[8], + ) + } + + private fun _toMatrix(): FloatArray { + val xx = x * x + val xy = x * y + val xz = x * z + val xw = x * w + val yy = y * y + val yz = y * z + val yw = y * w + val zz = z * z + val zw = z * w + + return floatArrayOf( + 1 - 2 * (yy + zz), 2 * (xy - zw), 2 * (xz + yw), + 2 * (xy + zw), 1 - 2 * (xx + zz), 2 * (yz - xw), + 2 * (xz - yw), 2 * (yz + xw), 1 - 2 * (xx + yy), + ) + } + + @Deprecated("Use toMatrix instead") + fun toMatrixInverted(): Matrix4 = Matrix4.multiply( + // Left + w, z, -y, x, + -z, w, x, y, + y, -x, w, z, + -x, -y, -z, w, + // Right + w, z, -y, -x, + -z, w, x, -y, + y, -x, w, -z, + x, y, z, w, + ) + + operator fun unaryMinus(): Quaternion = Quaternion(-x, -y, -z, -w) + operator fun plus(other: Quaternion): Quaternion = Quaternion(x + other.x, y + other.y, z + other.z, w + other.w) + operator fun minus(other: Quaternion): Quaternion = Quaternion(x - other.x, y - other.y, z - other.z, w - other.w) + + fun scaled(scale: Float): Quaternion = Quaternion.interpolated(Quaternion.IDENTITY, this, scale) + fun scaled(scale: Double): Quaternion = scaled(scale.toFloat()) + fun scaled(scale: Int): Quaternion = scaled(scale.toFloat()) + + operator fun times(scale: Float): Quaternion = Quaternion(x * scale, y * scale, z * scale, w * scale) + operator fun times(scale: Double): Quaternion = times(scale.toFloat()) + operator fun times(other: Quaternion): Quaternion { + val left = this + val right = other + return Quaternion(Vector4F( + (left.xyz * right.w) + (right.xyz * left.w) + Vector3F.cross(left.xyz, right.xyz), + left.w * right.w - left.xyz.dot(right.xyz) + )) + } + + fun normalized(): Quaternion { + val length = 1f / Vector4F(x, y, z, w).length + return Quaternion(x / length, y / length, z / length, w / length) + } + + /** Also known as conjugate */ + fun inverted(): Quaternion { + val q = this + val lengthSquared = q.lengthSquared + if (lengthSquared.isAlmostZero()) error("Zero quaternion doesn't have invesrse") + val num = 1f / lengthSquared + return Quaternion(q.x * -num, q.y * -num, q.z * -num, q.w * num) + } + + fun transform(v: Vector3F): Vector3F { + // Create a pure quaternion from the vector + val q = this + val p = Quaternion(v.x, v.y, v.z, 0f) + // Multiply q by p, then by the conjugate of q + val resultQuaternion = q * p * q.conjugate() + // Return the vector part of the resulting quaternion + return Vector3F(resultQuaternion.x, resultQuaternion.y, resultQuaternion.z) + } + + fun toEuler(config: EulerRotation.Config = EulerRotation.Config.DEFAULT): EulerRotation = EulerRotation.fromQuaternion(this, config) + override fun isAlmostEquals(other: Quaternion, epsilon: Float): Boolean = + this.x.isAlmostEquals(other.x, epsilon) + && this.y.isAlmostEquals(other.y, epsilon) + && this.z.isAlmostEquals(other.z, epsilon) + && this.w.isAlmostEquals(other.w, epsilon) + + fun interpolated(other: Quaternion, t: Float): Quaternion = interpolated(this, other, t) + fun interpolated(other: Quaternion, t: Ratio): Quaternion = interpolated(this, other, t.toFloat()) + fun angleTo(other: Quaternion): Angle = angleBetween(this, other) + + companion object { + val IDENTITY = Quaternion() + + fun dotProduct(l: Quaternion, r: Quaternion): Float = l.x * r.x + l.y * r.y + l.z * r.z + l.w * r.w + + fun angleBetween(a: Quaternion, b: Quaternion): Angle { + val dot = dotProduct(a, b) + return Angle.arcCosine(2 * (dot * dot) - 1) + } + + inline fun func(callback: (Int) -> Float) = Quaternion(callback(0), callback(1), callback(2), callback(3)) + inline fun func(l: Quaternion, r: Quaternion, func: (l: Float, r: Float) -> Float) = Quaternion( + func(l.x, r.x), + func(l.y, r.y), + func(l.z, r.z), + func(l.w, r.w) + ) + fun slerp(left: Quaternion, right: Quaternion, t: Float): Quaternion { + var tleft = left.normalized() + var tright = right.normalized() + + var dot = Quaternion.dotProduct(tleft, right) + + if (dot < 0.0f) { + tright = -tright + dot = -dot + } + + if (dot > 0.99995f) return func(tleft, tright) { l, r -> l + t * (r - l) } + + val angle0 = acos(dot) + val angle1 = angle0 * t + + val s1 = sin(angle1) / sin(angle0) + val s0 = cos(angle1) - dot * s1 + + return func(tleft, tright) { l, r -> ((s0 * l) + (s1 * r)) } + } + + fun nlerp(left: Quaternion, right: Quaternion, t: Double): Quaternion { + val sign = if (Quaternion.dotProduct(left, right) < 0) -1 else +1 + return func { ((1f - t) * left[it] + t * right[it] * sign).toFloat() }.normalized() + } + + fun interpolated(left: Quaternion, right: Quaternion, t: Float): Quaternion = slerp(left, right, t) + + fun fromVectors(from: Vector3F, to: Vector3F): Quaternion { + // Normalize input vectors + val start = from.normalized() + val dest = to.normalized() + + val dot = start.dot(dest) + + // If vectors are opposite + when { + dot < -0.9999999f -> { + val tmp = Vector3F(start.y, -start.x, 0f).normalized() + return Quaternion(tmp.x, tmp.y, tmp.z, 0f) + } + + dot > 0.9999999f -> { + // If vectors are same + return Quaternion() + } + + else -> { + val s = kotlin.math.sqrt((1 + dot) * 2) + val invs = 1 / s + + val c = start.cross(dest) + + return Quaternion( + c.x * invs, + c.y * invs, + c.z * invs, + s * 0.5f, + ).normalized() + } + } + } + + fun fromAxisAngle(axis: Vector3F, angle: Angle): Quaternion { + val naxis = axis.normalized() + val angle2 = angle / 2 + val s = sin(angle2) + return Quaternion( + naxis.x * s, + naxis.y * s, + naxis.z * s, + cos(angle2) + ) + } + + // @TODO: Check + fun lookRotation(forward: Vector3F, up: Vector3F = Vector3F.UP): Quaternion { + //if (up == Vector3.UP) return fromVectors(Vector3.FORWARD, forward.normalized()) + val z = forward.normalized() + val x = (up.normalized() cross z).normalized() + + //println("x=$x, z=$z") + if (x.lengthSquared.isAlmostZero()) { + // COLLINEAR + return Quaternion.fromVectors(Vector3F.FORWARD, z) + } + + val y = z cross x + return fromRotationMatrix(Matrix3.fromColumns(x, y, z)) + } + + fun fromRotationMatrix(m: Matrix4): Quaternion = fromRotationMatrix( + m.v00, m.v10, m.v20, + m.v01, m.v11, m.v21, + m.v02, m.v12, m.v22, + ) + + fun fromRotationMatrix(m: Matrix3): Quaternion = fromRotationMatrix( + m.v00, m.v10, m.v20, + m.v01, m.v11, m.v21, + m.v02, m.v12, m.v22, + ) + + fun fromRotationMatrix( + v00: Float, v10: Float, v20: Float, + v01: Float, v11: Float, v21: Float, + v02: Float, v12: Float, v22: Float, + ): Quaternion { + val t = v00 + v11 + v22 + //println("t=$t, v00=$v00, v11=$v11, v22=$v22") + return when { + t >= 0 -> { + val s = .5f / sqrt(t + 1f) + //println("[0]") + Quaternion(((v21 - v12) * s), ((v02 - v20) * s), ((v10 - v01) * s), (0.25f / s)) + } + v00 > v11 && v00 > v22 -> { + val s = 2f * sqrt(1f + v00 - v11 - v22) + //println("[1]") + Quaternion((0.25f * s), ((v01 + v10) / s), ((v02 + v20) / s), ((v21 - v12) / s)) + } + v11 > v22 -> { + val s = 2f * sqrt(1f + v11 - v00 - v22) + //println("[2]") + Quaternion(((v01 + v10) / s), (.25f * s), ((v12 + v21) / s), ((v02 - v20) / s)) + } + else -> { + val s = 2f * sqrt(1f + v22 - v00 - v11) + //println("[3]") + Quaternion(((v02 + v20) / s), ((v12 + v21) / s), (.25f * s), ((v10 - v01) / s)) + } + } + } + + fun fromEuler(e: EulerRotation): Quaternion = e.toQuaternion() + fun fromEuler(roll: Angle, pitch: Angle, yaw: Angle): Quaternion = EulerRotation(roll, pitch, yaw).toQuaternion() + + fun toEuler(x: Float, y: Float, z: Float, w: Float, config: EulerRotation.Config = EulerRotation.Config.DEFAULT): EulerRotation { + return EulerRotation.Companion.fromQuaternion(x, y, z, w, config) + /* + val t = y * x + z * w + // Gimbal lock, if any: positive (+1) for north pole, negative (-1) for south pole, zero (0) when no gimbal lock + val pole = if (t > 0.499f) 1 else if (t < -0.499f) -1 else 0 + return EulerRotation( + roll = when (pole) { + 0 -> Angle.asin((2f * (w * x - z * y)).clamp(-1f, +1f)) + else -> (pole.toFloat() * PIF * .5f).radians + }, + pitch = when (pole) { + 0 -> Angle.atan2(2f * (y * w + x * z), 1f - 2f * (y * y + x * x)) + else -> Angle.ZERO + }, + yaw = when (pole) { + 0 -> Angle.atan2(2f * (w * z + y * x), 1f - 2f * (x * x + z * z)) + else -> Angle.atan2(y, w) * pole.toFloat() * 2f + }, + ) + + */ + } + } +} + +fun Angle.Companion.between(a: Quaternion, b: Quaternion): Angle = Quaternion.angleBetween(a, b) diff --git a/math/src/main/java/com/icegps/math/geometry/Ray.kt b/math/src/main/java/com/icegps/math/geometry/Ray.kt new file mode 100644 index 0000000..81b2587 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Ray.kt @@ -0,0 +1,90 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.annotations.* + +typealias Ray = Ray2D +typealias Ray2 = Ray + +/** Represents an infinite [Ray] starting at [point] in the specified [direction] with an [angle] */ +//inline class Ray(val data: Float4Pack) { +data class Ray2D +/** Constructs a [Ray] starting from [point] in the specified [direction] */ +private constructor( + /** Starting point */ + val point: Point, + /** Normalized direction of the ray starting at [point] */ + val direction: Vector2D, +) : IsAlmostEquals { + companion object { + /** Creates a ray starting in [start] and passing by [end] */ + fun fromTwoPoints(start: Point, end: Point): Ray = Ray(start, end - start, Unit) + } + + //val point: Point get() = Point(data.f0, data.f1) + //val direction: Vector2 get() = Vector2(data.f2, data.f3) + /** Angle between two points */ + val angle: Angle get() = direction.angle + + /** Constructs a [Ray] starting from [point] in the specified [direction] */ + constructor(point: Point, direction: Vector2D, unit: Unit = Unit) : this(point, direction.normalized) + /** Constructs a [Ray] starting from [point] in the specified [angle] */ + constructor(point: Point, angle: Angle) : this(point, Vector2D.polar(angle), Unit) + + //private constructor(point: Point, normalizedDirection: Vector2, unit: Unit) : this(point.x, point.y, normalizedDirection.x, normalizedDirection.y) + + /** Checks if [this] and [other]are equals with an [epsilon] difference */ + override fun isAlmostEquals(other: Ray, epsilon: Double): Boolean = + this.point.isAlmostEquals(other.point, epsilon) && this.direction.isAlmostEquals(other.direction, epsilon) + + /** Checks if [this] and [other]are equals with an [epsilon] tolerance */ + fun transformed(m: Matrix): Ray = Ray(m.transform(point), m.deltaTransform(direction).normalized) + + /** Converts this [Ray] into a [Line] of a specific [length] starting by [point] */ + fun toLine(length: Double = 100000.0): Line = Line(point, point + direction * length) + + override fun toString(): String = "Ray($point, $angle)" +} + +typealias Ray3 = Ray3F + +data class Ray3F(val pos: Vector3F, val dir: Vector3F) {//: Shape3D { + //override val center: Vector3 get() = pos + //override val volume: Float = 0f +} + +@KormaMutableApi +fun Ray3F.intersectRayAABox1(box: AABB3D) : Boolean { + val ray = this + // r.dir is unit direction vector of ray + val dirfrac = ray.dir.inv() + // lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner + // r.org is origin of ray + val t1 = (box.min.x - ray.pos.x) * dirfrac.x + val t2 = (box.max.x - ray.pos.x) * dirfrac.x + val t3 = (box.min.y - ray.pos.y) * dirfrac.y + val t4 = (box.max.y - ray.pos.y) * dirfrac.y + val t5 = (box.min.z - ray.pos.z) * dirfrac.z + val t6 = (box.max.z - ray.pos.z) * dirfrac.z + + val tmin = + kotlin.math.max(kotlin.math.max(kotlin.math.min(t1, t2), kotlin.math.min(t3, t4)), kotlin.math.min(t5, t6)) + val tmax = + kotlin.math.min(kotlin.math.min(kotlin.math.max(t1, t2), kotlin.math.max(t3, t4)), kotlin.math.max(t5, t6)) + + // if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us + if (tmax < 0) { + val t = tmax + return false + } + + // if tmin > tmax, ray doesn't intersect AABB + if (tmin > tmax) { + val t = tmax + return false + } + + val t = tmin + return true + +} diff --git a/math/src/main/java/com/icegps/math/geometry/RectCorners.kt b/math/src/main/java/com/icegps/math/geometry/RectCorners.kt new file mode 100644 index 0000000..e4ca9bb --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/RectCorners.kt @@ -0,0 +1,28 @@ +package com.icegps.math.geometry + +data class RectCorners( + val topLeft: Double, + val topRight: Double, + val bottomRight: Double, + val bottomLeft: Double, +) { + operator fun unaryMinus(): RectCorners = this * (-1.0) + operator fun unaryPlus(): RectCorners = this + operator fun plus(that: RectCorners): RectCorners = RectCorners(this.topLeft + that.topLeft, this.topRight + that.topRight, this.bottomLeft + that.bottomLeft, this.bottomRight + that.bottomRight) + operator fun minus(that: RectCorners): RectCorners = RectCorners(this.topLeft - that.topLeft, this.topRight - that.topRight, this.bottomLeft - that.bottomLeft, this.bottomRight - that.bottomRight) + operator fun times(scale: Double): RectCorners = RectCorners(topLeft * scale, topRight * scale, bottomRight * scale, bottomLeft * scale) + operator fun div(scale: Double): RectCorners = this * (1.0 / scale) + + companion object { + val EMPTY = RectCorners(0) + val ZERO = RectCorners(0) + val ONE = RectCorners(1.0) + val MINUS_ONE = RectCorners(-1.0) + val NaN = RectCorners(Double.NaN) + + inline operator fun invoke(corner: Number): RectCorners = RectCorners(corner.toDouble(), corner.toDouble(), corner.toDouble(), corner.toDouble()) + inline operator fun invoke(topLeftBottomRight: Number, topRightAndBottomLeft: Number): RectCorners = RectCorners(topLeftBottomRight.toDouble(), topRightAndBottomLeft.toDouble(), topLeftBottomRight.toDouble(), topRightAndBottomLeft.toDouble()) + inline operator fun invoke(topLeft: Number, topRightAndBottomLeft: Number, bottomRight: Number): RectCorners = RectCorners(topLeft.toDouble(), topRightAndBottomLeft.toDouble(), bottomRight.toDouble(), topRightAndBottomLeft.toDouble()) + inline operator fun invoke(topLeft: Number, topRight: Number, bottomRight: Number, bottomLeft: Number): RectCorners = RectCorners(topLeft.toDouble(), topRight.toDouble(), bottomRight.toDouble(), bottomLeft.toDouble()) + } +} diff --git a/math/src/main/java/com/icegps/math/geometry/Rectangle.kt b/math/src/main/java/com/icegps/math/geometry/Rectangle.kt new file mode 100644 index 0000000..260ccf0 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Rectangle.kt @@ -0,0 +1,291 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.geometry.shape.* +import com.icegps.math.interpolation.* +import com.icegps.number.* +import kotlin.math.* + +typealias RectangleD = Rectangle + +//@KormaValueApi +//inline class Rectangle(val data: Float4Pack) : Shape2D, Interpolable { +//inline class Rectangle(val data: Float4) : Shape2D { +data class Rectangle(val x: Double, val y: Double, val width: Double, val height: Double) : SimpleShape2D, IsAlmostEquals { + val int: RectangleInt get() = toInt() + + //operator fun component1(): Float = x + //operator fun component2(): Float = y + //operator fun component3(): Float = width + //operator fun component4(): Float = height + //val x: Float get() = data.f0 + //val y: Float get() = data.f1 + //val width: Float get() = data.f2 + //val height: Float get() = data.f3 + //fun copy(x: Float = this.x, y: Float = this.y, width: Float = this.width, height: Float = this.height): Rectangle = Rectangle(x, y, width, height) + + @Deprecated("", ReplaceWith("this")) fun clone(): Rectangle = this + @Deprecated("", ReplaceWith("this")) val immutable: Rectangle get() = this + + val position: Point get() = Point(x, y) + val size: Size get() = Size(width, height) + + val isZero: Boolean get() = this == ZERO + val isInfinite: Boolean get() = this == INFINITE + //val isNaN: Boolean get() = this == NaN + val isNaN: Boolean get() = this.x.isNaN() + val isNIL: Boolean get() = isNaN + val isNotNIL: Boolean get() = !isNIL + + override fun isAlmostEquals(other: Rectangle, epsilon: Double): Boolean = + this.x.isAlmostEquals(other.x, epsilon) && + this.y.isAlmostEquals(other.y, epsilon) && + this.width.isAlmostEquals(other.width, epsilon) && + this.height.isAlmostEquals(other.height, epsilon) + + fun toStringBounds(): String = "Rectangle([${left.niceStr},${top.niceStr}]-[${right.niceStr},${bottom.niceStr}])" + fun toStringSize(): String = "Rectangle([${left.niceStr},${top.niceStr}],[${width.niceStr},${height.niceStr}])" + fun toStringCompat(): String = "Rectangle(x=${left.niceStr}, y=${top.niceStr}, w=${width.niceStr}, h=${height.niceStr})" + + //override fun interpolateWith(ratio: Ratio, other: Rectangle): Rectangle = interpolated(this, other, ratio) + + override fun toString(): String = when { + isNIL -> "null" + else -> "Rectangle(x=${x.niceStr}, y=${y.niceStr}, width=${width.niceStr}, height=${height.niceStr})" + } + + companion object { + val ZERO = Rectangle(0, 0, 0, 0) + val INFINITE = Rectangle(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) + val NaN = Rectangle(Float.NaN, Float.NaN, 0f, 0f) + val NIL get() = NaN + + operator fun invoke(): Rectangle = ZERO + operator fun invoke(p: Point, s: Size): Rectangle = Rectangle(p.x, p.y, s.width, s.height) + operator fun invoke(x: Int, y: Int, width: Int, height: Int): Rectangle = Rectangle(Point(x, y), Size(width, height)) + operator fun invoke(x: Float, y: Float, width: Float, height: Float): Rectangle = Rectangle(Point(x, y), Size(width, height)) + operator fun invoke(x: Double, y: Double, width: Double, height: Double): Rectangle = Rectangle(Point(x, y), Size(width, height)) + inline operator fun invoke(x: Number, y: Number, width: Number, height: Number): Rectangle = Rectangle(Point(x, y), Size(width, height)) + + fun fromBounds(left: Double, top: Double, right: Double, bottom: Double): Rectangle = Rectangle(left, top, right - left, bottom - top) + fun fromBounds(left: Int, top: Int, right: Int, bottom: Int): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble()) + fun fromBounds(left: Float, top: Float, right: Float, bottom: Float): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble()) + fun fromBounds(point1: Point, point2: Point): Rectangle = Rectangle(point1, (point2 - point1).toSize()) + inline fun fromBounds(left: Number, top: Number, right: Number, bottom: Number): Rectangle = fromBounds(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble()) + + fun isContainedIn(a: Rectangle, b: Rectangle): Boolean = a.x >= b.x && a.y >= b.y && a.x + a.width <= b.x + b.width && a.y + a.height <= b.y + b.height + + fun interpolated(a: Rectangle, b: Rectangle, ratio: Ratio): Rectangle = Rectangle.fromBounds( + ratio.interpolate(a.left, b.left), + ratio.interpolate(a.top, b.top), + ratio.interpolate(a.right, b.right), + ratio.interpolate(a.bottom, b.bottom), + ) + } + + operator fun times(scale: Double): Rectangle = Rectangle(x * scale, y * scale, width * scale, height * scale) + operator fun times(scale: Float): Rectangle = times(scale.toDouble()) + operator fun times(scale: Int): Rectangle = times(scale.toDouble()) + + operator fun div(scale: Double): Rectangle = Rectangle(x / scale, y / scale, width / scale, height / scale) + operator fun div(scale: Float): Rectangle = div(scale.toDouble()) + operator fun div(scale: Int): Rectangle = div(scale.toDouble()) + + operator fun contains(that: Point): Boolean = contains(that.x, that.y) + operator fun contains(that: Vector2F): Boolean = contains(that.x, that.y) + operator fun contains(that: Vector2I): Boolean = contains(that.x, that.y) + fun contains(x: Double, y: Double): Boolean = (x >= left && x < right) && (y >= top && y < bottom) + fun contains(x: Float, y: Float): Boolean = contains(x.toDouble(), y.toDouble()) + fun contains(x: Int, y: Int): Boolean = contains(x.toDouble(), y.toDouble()) + + override val area: Double get() = width * height + override val perimeter: Double get() = (width + height) * 2 + override val closed: Boolean = true + + override fun containsPoint(p: Point): Boolean = (p.x >= left && p.x < right) && (p.y >= top && p.y < bottom) + override fun getBounds(): Rectangle = this + + override fun distance(p: Point): Double { + val p = p - center + val b = Vector2D(width * 0.5, height * 0.5) + val d = p.absoluteValue - b + return max(d, Vector2D.ZERO).length + min(max(d.x, d.y), 0.0) + } + + override fun normalVectorAt(p: Point): Vector2D { + val pp = projectedPoint(p) + val x = when (pp.x) { + left -> -1.0 + right -> +1.0 + else -> 0.0 + } + val y = when (pp.y) { + top -> -1.0 + bottom -> +1.0 + else -> 0.0 + } + return Point(x, y).normalized + } + + override fun projectedPoint(p: Point): Point { + val p0 = Line(topLeft, topRight).projectedPoint(p) + val p1 = Line(topRight, bottomRight).projectedPoint(p) + val p2 = Line(bottomRight, bottomLeft).projectedPoint(p) + val p3 = Line(bottomLeft, topLeft).projectedPoint(p) + val d0 = (p0 - p).lengthSquared + val d1 = (p1 - p).lengthSquared + val d2 = (p2 - p).lengthSquared + val d3 = (p3 - p).lengthSquared + val dmin = com.icegps.math.min(d0, d1, d2, d3) + return when (dmin) { + d0 -> p0 + d1 -> p1 + d2 -> p2 + d3 -> p3 + else -> p0 + } + + //val px = p.x.clamp(left, right) + //val py = p.y.clamp(top, bottom) + //val distTop = (py - top).absoluteValue + //val distBottom = (py - bottom).absoluteValue + //val minDistY = min(distTop, distBottom) + //val distLeft = (px - left).absoluteValue + //val distRight = (px - right).absoluteValue + //val minDistX = min(distLeft, distRight) + //if (minDistX < minDistY) { + // return Point(if (distLeft < distRight) left else right, py) + //} else { + // return Point(px, if (distTop < distBottom) top else bottom) + //} + } + + val isEmpty: Boolean get() = width == 0.0 && height == 0.0 + val isNotEmpty: Boolean get() = !isEmpty + + val left: Double get() = x + val top: Double get() = y + val right: Double get() = x + width + val bottom: Double get() = y + height + + val topLeft: Point get() = Point(left, top) + val topRight: Point get() = Point(right, top) + val bottomLeft: Point get() = Point(left, bottom) + val bottomRight: Point get() = Point(right, bottom) + + val centerX: Double get() = (right + left) * 0.5 + val centerY: Double get() = (bottom + top) * 0.5 + override val center: Point get() = Point(centerX, centerY) + + fun without(padding: Margin): Rectangle = fromBounds( + left + padding.left, + top + padding.top, + right - padding.right, + bottom - padding.bottom + ) + + fun with(margin: Margin): Rectangle = fromBounds( + left - margin.left, + top - margin.top, + right + margin.right, + bottom + margin.bottom + ) + + infix fun intersects(that: Rectangle): Boolean = intersectsX(that) && intersectsY(that) + infix fun intersectsX(that: Rectangle): Boolean = that.left <= this.right && that.right >= this.left + infix fun intersectsY(that: Rectangle): Boolean = that.top <= this.bottom && that.bottom >= this.top + + infix fun intersectionOrNull(that: Rectangle): Rectangle? = if (this intersects that) Rectangle( + max(this.left, that.left), max(this.top, that.top), + min(this.right, that.right), min(this.bottom, that.bottom) + ) else null + + infix fun intersection(that: Rectangle): Rectangle = if (this intersects that) Rectangle( + max(this.left, that.left), max(this.top, that.top), + min(this.right, that.right), min(this.bottom, that.bottom) + ) else Rectangle.NIL + + fun toInt(): RectangleInt = RectangleInt(x.toInt(), y.toInt(), width.toInt(), height.toInt()) + fun toIntRound(): RectangleInt = RectangleInt(x.toIntRound(), y.toIntRound(), width.toIntRound(), height.toIntRound()) + fun toIntCeil(): RectangleInt = RectangleInt(x.toIntCeil(), y.toIntCeil(), width.toIntCeil(), height.toIntCeil()) + fun toIntFloor(): RectangleInt = RectangleInt(x.toIntFloor(), y.toIntFloor(), width.toIntFloor(), height.toIntFloor()) + + fun getAnchoredPoint(anchor: Anchor): Point = Point(left + width * anchor.sx, top + height * anchor.sy) + + fun expanded(border: MarginInt): Rectangle = + fromBounds(left - border.left, top - border.top, right + border.right, bottom + border.bottom) + + fun copyBounds(left: Double = this.left, top: Double = this.top, right: Double = this.right, bottom: Double = this.bottom): Rectangle = + Rectangle.fromBounds(left, top, right, bottom) + + fun translated(delta: Point): Rectangle = copy(x = this.x + delta.x, y = this.y + delta.y) + + fun transformed(m: Matrix): Rectangle { + val tl = m.transform(topLeft) + val tr = m.transform(topRight) + val bl = m.transform(bottomLeft) + val br = m.transform(bottomRight) + val min = Point.minComponents(tl, tr, bl, br) + val max = Point.maxComponents(tl, tr, bl, br) + return Rectangle.fromBounds(min, max) + } + + fun normalized(): Rectangle = + Rectangle.fromBounds(Point.minComponents(topLeft, bottomRight), Point.maxComponents(topLeft, bottomRight)) + + fun roundDecimalPlaces(places: Int): Rectangle = Rectangle( + x.roundDecimalPlaces(places), + y.roundDecimalPlaces(places), + width.roundDecimalPlaces(places), + height.roundDecimalPlaces(places) + ) + + fun rounded(): Rectangle = Rectangle(round(x), round(y), round(width), round(height)) + fun floored(): Rectangle = Rectangle(floor(x), floor(y), floor(width), floor(height)) + fun ceiled(): Rectangle = Rectangle(ceil(x), ceil(y), ceil(width), ceil(height)) +} + + +fun Iterable.bounds(): Rectangle { + var first = true + var left = 0.0 + var right = 0.0 + var top = 0.0 + var bottom = 0.0 + for (r in this) { + if (first) { + left = r.left + right = r.right + top = r.top + bottom = r.bottom + first = false + } else { + left = min(left, r.left) + right = max(right, r.right) + top = min(top, r.top) + bottom = max(bottom, r.bottom) + } + } + return Rectangle.fromBounds(left, top, right, bottom) +} + +/** + * Circle that touches or contains all the corners ([Rectangle.topLeft], [Rectangle.topRight], [Rectangle.bottomLeft], [Rectangle.bottomRight]) of the rectangle. + */ +fun Rectangle.outerCircle(): Circle { + val centerX = centerX + val centerY = centerY + return Circle(center, Point.distance(centerX, centerY, right, top)) +} + +fun Rectangle.place(item: Size, anchor: Anchor, scale: ScaleMode): Rectangle { + val outSize = scale(item, this.size) + val p = (this.size - outSize) * anchor + return Rectangle(p, outSize) +} + +//fun RectangleInt.place(item: SizeInt, anchor: Anchor, scale: ScaleMode): RectangleInt { +// val outSize = scale(item, this.size) +// val p = (this.size - outSize) * anchor +// return RectangleInt(p, outSize) +//} diff --git a/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt b/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt new file mode 100644 index 0000000..03d0774 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/RectangleInt.kt @@ -0,0 +1,89 @@ +package com.icegps.math.geometry + +typealias RectangleI = RectangleInt + +//@KormaValueApi +data class RectangleInt( + val x: Int, val y: Int, + val width: Int, val height: Int, +) { + constructor() : this(0, 0, 0, 0) + + val position: Vector2I get() = Vector2I(x, y) + + val area: Int get() = width * height + val isEmpty: Boolean get() = width == 0 && height == 0 + val isNotEmpty: Boolean get() = !isEmpty + + val left: Int get() = x + val top: Int get() = y + val right: Int get() = x + width + val bottom: Int get() = y + height + + val topLeft: Vector2I get() = Vector2I(left, top) + val topRight: Vector2I get() = Vector2I(right, top) + val bottomLeft: Vector2I get() = Vector2I(left, bottom) + val bottomRight: Vector2I get() = Vector2I(right, bottom) + + val centerX: Int get() = ((right + left) * 0.5f).toInt() + val centerY: Int get() = ((bottom + top) * 0.5f).toInt() + val center: Vector2I get() = Vector2I(centerX, centerY) + + operator fun times(scale: Double): RectangleInt = RectangleInt( + (x * scale).toInt(), (y * scale).toInt(), + (width * scale).toInt(), (height * scale).toInt() + ) + operator fun times(scale: Float): RectangleInt = this * scale.toDouble() + operator fun times(scale: Int): RectangleInt = this * scale.toDouble() + + operator fun div(scale: Float): RectangleInt = RectangleInt( + (x / scale).toInt(), (y / scale).toInt(), + (width / scale).toInt(), (height / scale).toInt() + ) + + operator fun div(scale: Double): RectangleInt = this / scale.toFloat() + operator fun div(scale: Int): RectangleInt = this / scale.toFloat() + + operator fun contains(that: Point): Boolean = contains(that.x, that.y) + operator fun contains(that: Vector2I): Boolean = contains(that.x, that.y) + fun contains(x: Float, y: Float): Boolean = (x >= left && x < right) && (y >= top && y < bottom) + fun contains(x: Double, y: Double): Boolean = contains(x.toFloat(), y.toFloat()) + fun contains(x: Int, y: Int): Boolean = contains(x.toFloat(), y.toFloat()) + + fun sliceWithBounds(left: Int, top: Int, right: Int, bottom: Int, clamped: Boolean = true): RectangleInt { + val left = if (!clamped) left else left.coerceIn(0, this.width) + val right = if (!clamped) right else right.coerceIn(0, this.width) + val top = if (!clamped) top else top.coerceIn(0, this.height) + val bottom = if (!clamped) bottom else bottom.coerceIn(0, this.height) + return fromBounds(this.x + left, this.y + top, this.x + right, this.y + bottom) + } + + fun sliceWithSize(x: Int, y: Int, width: Int, height: Int, clamped: Boolean = true): RectangleInt = + sliceWithBounds(x, y, x + width, y + height, clamped) + + override fun toString(): String = "Rectangle(x=${x}, y=${y}, width=${width}, height=${height})" + + companion object { + fun union(a: RectangleInt, b: RectangleInt): RectangleInt = fromBounds( + kotlin.math.min(a.left, b.left), + kotlin.math.min(a.top, b.top), + kotlin.math.max(a.right, b.right), + kotlin.math.max(a.bottom, b.bottom) + ) + + fun fromBounds(topLeft: Vector2I, bottomRight: Vector2I): RectangleInt { + val size = (bottomRight - topLeft) + return RectangleInt(topLeft.x, topLeft.y, size.x, size.y) + } + fun fromBounds(left: Int, top: Int, right: Int, bottom: Int): RectangleInt = fromBounds(Vector2I(left, top), Vector2I(right, bottom)) + + operator fun invoke(position: PointInt, size: SizeInt): RectangleInt = RectangleInt(position.x, position.y, size.width, size.height) + } + + val float: Rectangle get() = Rectangle(x, y, width, height) + val size: SizeInt get() = SizeInt(width, height) + fun toFloat(): Rectangle = Rectangle(position.toDouble(), size.toDouble()) + fun expanded(border: MarginInt): RectangleInt = + RectangleInt.fromBounds(left - border.left, top - border.top, right + border.right, bottom + border.bottom) + +} diff --git a/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt b/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt new file mode 100644 index 0000000..0ccb586 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/RoundRectangle.kt @@ -0,0 +1,18 @@ +package com.icegps.math.geometry + +import com.icegps.math.interpolation.* + +data class RoundRectangle(val rect: Rectangle, val corners: RectCorners) { + companion object { + private fun areaQuarter(radius: Double): Double = Arc_length(radius, Angle.QUARTER) + private fun areaComplementaryQuarter(radius: Double): Double = (radius * radius) - areaQuarter(radius) + private fun Arc_length(radius: Double, angle: Angle): Double = PI2 * radius * angle.ratio + } + + val area: Double get() = rect.area - ( + areaComplementaryQuarter(corners.topLeft) + + areaComplementaryQuarter(corners.topRight) + + areaComplementaryQuarter(corners.bottomLeft) + + areaComplementaryQuarter(corners.bottomRight) + ) +} diff --git a/math/src/main/java/com/icegps/math/geometry/Scale.kt b/math/src/main/java/com/icegps/math/geometry/Scale.kt new file mode 100644 index 0000000..16fa1a3 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Scale.kt @@ -0,0 +1,52 @@ +package com.icegps.math.geometry + +//@KormaValueApi +//inline class Scale internal constructor(internal val raw: Float2Pack) { +data class Scale(val scaleX: Double, val scaleY: Double) { + companion object { + val IDENTITY = Scale(1f, 1f) + } + + //val scaleX: Float get() = raw.f0 + //val scaleY: Float get() = raw.f1 + val scaleAvg: Double get() = scaleX * .5 + scaleY * .5 + + @Deprecated("", ReplaceWith("scaleAvg")) + val avg: Double get() = scaleAvg + + constructor() : this(1f, 1f) + constructor(scale: Float) : this(scale, scale) + constructor(scale: Double) : this(scale, scale) + constructor(scale: Int) : this(scale.toDouble()) + //constructor(scaleX: Float, scaleY: Float) : this(float2PackOf(scaleX, scaleY)) + constructor(scaleX: Float, scaleY: Float) : this(scaleX.toDouble(), scaleY.toDouble()) + constructor(scaleX: Int, scaleY: Int) : this(scaleX.toDouble(), scaleY.toDouble()) + + operator fun unaryMinus(): Scale = Scale(-scaleX, -scaleY) + operator fun unaryPlus(): Scale = this + + operator fun plus(other: Scale): Scale = Scale(scaleX + other.scaleX, scaleY + other.scaleY) + operator fun minus(other: Scale): Scale = Scale(scaleX - other.scaleX, scaleY - other.scaleY) + + operator fun times(other: Scale): Scale = Scale(scaleX * other.scaleX, scaleY * other.scaleY) + operator fun times(other: Float): Scale = Scale(scaleX * other, scaleY * other) + operator fun div(other: Scale): Scale = Scale(scaleX / other.scaleX, scaleY / other.scaleY) + operator fun div(other: Float): Scale = Scale(scaleX / other, scaleY / other) + operator fun rem(other: Scale): Scale = Scale(scaleX % other.scaleX, scaleY % other.scaleY) + operator fun rem(other: Float): Scale = Scale(scaleX % other, scaleY % scaleY) +} + +operator fun Vector2D.times(other: Scale): Vector2D = Vector2D(x * other.scaleX, y * other.scaleY) +operator fun Vector2D.div(other: Scale): Vector2D = Vector2D(x / other.scaleX, y / other.scaleY) +operator fun Vector2D.rem(other: Scale): Vector2D = Vector2D(x % other.scaleX, y % other.scaleY) + +operator fun Vector2F.times(other: Scale): Vector2F = Vector2F(x * other.scaleX, y * other.scaleY) +operator fun Vector2F.div(other: Scale): Vector2F = Vector2F(x / other.scaleX, y / other.scaleY) +operator fun Vector2F.rem(other: Scale): Vector2F = Vector2F(x % other.scaleX, y % other.scaleY) + +fun Vector2F.toScale(): Scale = Scale(x, y) +fun Vector2D.toScale(): Scale = Scale(x, y) + +fun Scale.toPoint(): Point = Point(scaleX, scaleY) +fun Scale.toVector2(): Vector2D = Vector2D(scaleX, scaleY) +fun Scale.toVector2F(): Vector2F = Vector2F(scaleX, scaleY) diff --git a/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt b/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt new file mode 100644 index 0000000..28ae855 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/ScaleMode.kt @@ -0,0 +1,40 @@ +package com.icegps.math.geometry + +class ScaleMode( + val name: String? = null, + val transform: (item: Size, container: Size) -> Size +) { + override fun toString(): String = "ScaleMode($name)" + + operator fun invoke(item: Size, container: Size): Size = transform(item, container) + operator fun invoke(item: SizeInt, container: SizeInt): SizeInt = transform(item.toFloat(), container.toFloat()).toInt() + + companion object { + val COVER: ScaleMode = ScaleMode("COVER") { i, c -> i * (c / i).toVector2().maxComponent() } + val SHOW_ALL: ScaleMode = ScaleMode("SHOW_ALL") { i, c -> i * (c / i).toVector2().minComponent() } + val FIT: ScaleMode get() = SHOW_ALL + val FILL: ScaleMode get() = EXACT + val EXACT: ScaleMode = ScaleMode("EXACT") { i, c -> c } + val NO_SCALE: ScaleMode = ScaleMode("NO_SCALE") { i, c -> i } + } +} + +fun Rectangle.applyScaleMode( + container: Rectangle, mode: ScaleMode, anchor: Anchor +): Rectangle = this.size.applyScaleMode(container, mode, anchor) + +fun SizeInt.applyScaleMode(container: RectangleInt, mode: ScaleMode, anchor: Anchor): RectangleInt = this.toFloat().applyScaleMode(container.toFloat(), mode, anchor).toInt() +fun SizeInt.applyScaleMode(container: SizeInt, mode: ScaleMode): SizeInt = mode(this, container) +fun SizeInt.fitTo(container: SizeInt): SizeInt = applyScaleMode(container, ScaleMode.SHOW_ALL) + +fun Size.applyScaleMode(container: Rectangle, mode: ScaleMode, anchor: Anchor): Rectangle { + val outSize = this.applyScaleMode(container.size, mode) + return Rectangle( + (container.x + anchor.sx * (container.width - outSize.width)), + (container.y + anchor.sy * (container.height - outSize.height)), + outSize.width, + outSize.height + ) +} +fun Size.applyScaleMode(container: Size, mode: ScaleMode): Size = mode(this, container) +fun Size.fitTo(container: Size): Size = applyScaleMode(container, ScaleMode.SHOW_ALL) diff --git a/math/src/main/java/com/icegps/math/geometry/Size.kt b/math/src/main/java/com/icegps/math/geometry/Size.kt new file mode 100644 index 0000000..56a1dc8 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Size.kt @@ -0,0 +1,150 @@ +package com.icegps.math.geometry + +import com.icegps.number.* +import kotlin.math.* + +typealias Size = Size2D +typealias Size3 = Size2F + +data class Size2F(val width: Float, val height: Float) +data class Size3F(val width: Float, val height: Float, val depth: Float) +data class Size3D(val width: Double, val height: Double, val depth: Double) + +/** + * A class representing a size with a [width] and a [height] as Float. + */ +data class Size2D(val width: Double, val height: Double) {//: Sizeable { + companion object { + inline operator fun invoke(width: Number, height: Number): Size2D = Size2D(width.toDouble(), height.toDouble()) + val ZERO = Size(0.0, 0.0) + fun square(value: Int): Size = Size(value, value) + fun square(value: Double): Size = Size(value, value) + } + + fun isEmpty(): Boolean = width == 0.0 || height == 0.0 + + fun avgComponent(): Double = width * 0.5 + height * 0.5 + fun minComponent(): Double = min(width, height) + fun maxComponent(): Double = max(width, height) + + val area: Double get() = width * height + val perimeter: Double get() = width * 2 + height * 2 + + //(val width: Double, val height: Double) { + constructor() : this(0.0, 0.0) + constructor(width: Float, height: Float) : this(width.toDouble(), height.toDouble()) + constructor(width: Int, height: Int) : this(width.toDouble(), height.toDouble()) + + operator fun unaryMinus(): Size = Size(-width, -height) + operator fun unaryPlus(): Size = this + + operator fun minus(other: Size): Size = Size(width - other.width, height - other.height) + operator fun plus(other: Size): Size = Size(width + other.width, height + other.height) + operator fun times(scale: Scale): Size = Size(width * scale.scaleX, height * scale.scaleY) + operator fun times(scale: Vector2F): Size = Size(width * scale.x, height * scale.y) + operator fun times(s: Float): Size = Size(width * s, height * s) + operator fun times(s: Double): Size = times(s.toFloat()) + operator fun times(s: Int): Size = times(s.toFloat()) + operator fun div(other: Size): Scale = Scale(width / other.width, height / other.height) + operator fun div(s: Float): Size = Size(width / s, height / s) + operator fun div(s: Double): Size = div(s.toFloat()) + operator fun div(s: Int): Size = div(s.toFloat()) + + //override val size: Size get() = this + + override fun toString(): String = "Size(width=${width.niceStr}, height=${height.niceStr})" +} + +operator fun Vector2D.plus(other: Size): Vector2D = Vector2D(x + other.width, y + other.height) +operator fun Vector2D.minus(other: Size): Vector2D = Vector2D(x - other.width, y - other.height) +operator fun Vector2D.times(other: Size): Vector2D = Vector2D(x * other.width, y * other.height) +operator fun Vector2D.div(other: Size): Vector2D = Vector2D(x / other.width, y / other.height) +operator fun Vector2D.rem(other: Size): Vector2D = Vector2D(x % other.width, y % other.height) + +operator fun Vector2F.plus(other: Size): Vector2F = Vector2F(x + other.width, y + other.height) +operator fun Vector2F.minus(other: Size): Vector2F = Vector2F(x - other.width, y - other.height) +operator fun Vector2F.times(other: Size): Vector2F = Vector2F(x * other.width, y * other.height) +operator fun Vector2F.div(other: Size): Vector2F = Vector2F(x / other.width, y / other.height) +operator fun Vector2F.rem(other: Size): Vector2F = Vector2F(x % other.width, y % other.height) + +fun Point.toSize(): Size = Size(x, y) + +fun Size.toInt(): SizeInt = SizeInt(width.toInt(), height.toInt()) +fun Size.toPoint(): Point = Point(width, height) +fun Size.toVector(): Vector2D = Vector2D(width, height) +fun Size.toVector2D(): Vector2D = Vector2D(width, height) +fun Size.toVector2F(): Vector2F = Vector2F(width, height) + +interface Sizeable { + val size: Size + + companion object { + operator fun invoke(size: Size): Sizeable = object : Sizeable { + override val size: Size get() = size + } + } +} + +interface SizeableInt { + val size: SizeInt + companion object { + operator fun invoke(size: SizeInt): SizeableInt = object : SizeableInt { + override val size: SizeInt get() = size + } + operator fun invoke(width: Int, height: Int): SizeableInt = invoke(SizeInt(width, height)) + } +} + +typealias SizeI = SizeInt + +data class SizeInt(val width: Int, val height: Int) { + constructor() : this(0, 0) + + fun avgComponent(): Int = (width + height) / 2 + fun minComponent(): Int = kotlin.math.min(width, height) + fun maxComponent(): Int = kotlin.math.max(width, height) + + val area: Int get() = width * height + val perimeter: Int get() = width * 2 + height * 2 + + operator fun unaryMinus(): SizeInt = SizeInt(-width, -height) + operator fun unaryPlus(): SizeInt = this + + operator fun minus(other: SizeInt): SizeInt = SizeInt(width - other.width, height - other.height) + operator fun plus(other: SizeInt): SizeInt = SizeInt(width + other.width, height + other.height) + operator fun times(s: Float): SizeInt = SizeInt((width * s).toInt(), (height * s).toInt()) + operator fun times(s: Double): SizeInt = times(s.toFloat()) + operator fun times(s: Int): SizeInt = times(s.toFloat()) + operator fun times(scale: Vector2F): SizeInt = SizeInt((width * scale.x).toInt(), (height * scale.y).toInt()) + operator fun times(scale: Scale): SizeInt = SizeInt((width * scale.scaleX).toInt(), (height * scale.scaleY).toInt()) + + operator fun div(other: SizeInt): SizeInt = SizeInt(width / other.width, height / other.height) + operator fun div(s: Float): SizeInt = SizeInt((width / s).toInt(), (height / s).toInt()) + operator fun div(s: Double): SizeInt = div(s.toFloat()) + operator fun div(s: Int): SizeInt = div(s.toFloat()) + + override fun toString(): String = "${width}x${height}" +} + +fun Vector2I.toSize(): SizeInt = SizeInt(x, y) +fun SizeInt.toFloat(): Size = Size(width.toFloat(), height.toFloat()) +fun SizeInt.toDouble(): Size = Size(width.toDouble(), height.toDouble()) +fun SizeInt.toVector(): Vector2I = Vector2I(width, height) + +operator fun Vector2D.plus(other: SizeInt): Vector2D = Vector2D(x + other.width, y + other.height) +operator fun Vector2D.minus(other: SizeInt): Vector2D = Vector2D(x - other.width, y - other.height) +operator fun Vector2D.times(other: SizeInt): Vector2D = Vector2D(x * other.width, y * other.height) +operator fun Vector2D.div(other: SizeInt): Vector2D = Vector2D(x / other.width, y / other.height) +operator fun Vector2D.rem(other: SizeInt): Vector2D = Vector2D(x % other.width, y % other.height) + +operator fun Vector2F.plus(other: SizeInt): Vector2F = Vector2F(x + other.width, y + other.height) +operator fun Vector2F.minus(other: SizeInt): Vector2F = Vector2F(x - other.width, y - other.height) +operator fun Vector2F.times(other: SizeInt): Vector2F = Vector2F(x * other.width, y * other.height) +operator fun Vector2F.div(other: SizeInt): Vector2F = Vector2F(x / other.width, y / other.height) +operator fun Vector2F.rem(other: SizeInt): Vector2F = Vector2F(x % other.width, y % other.height) + +operator fun Vector2I.plus(other: SizeInt): Vector2I = Vector2I(x + other.width, y + other.height) +operator fun Vector2I.minus(other: SizeInt): Vector2I = Vector2I(x - other.width, y - other.height) +operator fun Vector2I.times(other: SizeInt): Vector2I = Vector2I(x * other.width, y * other.height) +operator fun Vector2I.div(other: SizeInt): Vector2I = Vector2I(x / other.width, y / other.height) +operator fun Vector2I.rem(other: SizeInt): Vector2I = Vector2I(x % other.width, y % other.height) diff --git a/math/src/main/java/com/icegps/math/geometry/Spacing.kt b/math/src/main/java/com/icegps/math/geometry/Spacing.kt new file mode 100644 index 0000000..a21164e --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Spacing.kt @@ -0,0 +1,28 @@ +package com.icegps.math.geometry + +import com.icegps.number.* + +data class Spacing( + val vertical: Double, + val horizontal: Double +) { + operator fun unaryMinus(): Spacing = Spacing(-vertical, -horizontal) + operator fun unaryPlus(): Spacing = this + operator fun plus(other: Spacing): Spacing = Spacing(vertical + other.vertical, horizontal + other.horizontal) + operator fun minus(other: Spacing): Spacing = Spacing(vertical - other.vertical, horizontal - other.horizontal) + operator fun times(scale: Double): Spacing = Spacing(vertical * scale, horizontal * scale) + operator fun div(scale: Double): Spacing = this * (1.0 / scale) + operator fun rem(scale: Double): Spacing = Spacing(vertical % scale, horizontal % scale) + operator fun rem(scale: Spacing): Spacing = Spacing(vertical % scale.vertical, horizontal % scale.horizontal) + + companion object { + val ZERO = Spacing(0.0, 0.0) + + inline operator fun invoke(spacing: Number): Spacing = Spacing(spacing.toDouble(), spacing.toDouble()) + inline operator fun invoke(vertical: Number, horizontal: Number): Spacing = Spacing(vertical.toDouble(), horizontal.toDouble()) + } + + constructor(spacing: Double) : this(spacing, spacing) + + override fun toString(): String = "Spacing(vertical=${vertical.niceStr}, horizontal=${horizontal.niceStr})" +} diff --git a/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt b/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt new file mode 100644 index 0000000..fa7646b --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/Sphere3D.kt @@ -0,0 +1,13 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.math.geometry.shape.* + +//inline class Sphere3D private constructor(private val data: Float4) : Shape3D { +data class Sphere3D(override val center: Vector3F, val radius: Float) : SimpleShape3D { + //constructor(center: Vector3, radius: Float) : this(Float4(center.x, center.y, center.z, radius)) + //override val center: Vector3 get() = Vector3(data.x, data.y, data.z) + //val radius: Float get() = data.w + + override val volume: Float get() = ((4f / 3f) * PIF) * (radius * radius * radius) +} diff --git a/math/src/main/java/com/icegps/math/geometry/VectorExt.kt b/math/src/main/java/com/icegps/math/geometry/VectorExt.kt new file mode 100644 index 0000000..a917707 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/VectorExt.kt @@ -0,0 +1,47 @@ +package com.icegps.math.geometry + +import com.icegps.math.interpolation.* + +inline fun Vector2F.deltaTransformed(m: Matrix): Vector2F = m.deltaTransform(this) +inline fun Vector2F.transformed(m: Matrix): Vector2F = m.transform(this) +fun Vector2F.transformX(m: Matrix): Float = m.transform(this).x +fun Vector2F.transformY(m: Matrix): Float = m.transform(this).y +inline fun Vector2F.transformedNullable(m: Matrix?): Vector2F = if (m != null && m.isNotNIL) m.transform(this) else this +fun Vector2F.transformNullableX(m: Matrix?): Float = if (m != null && m.isNotNIL) m.transform(this).x else x +fun Vector2F.transformNullableY(m: Matrix?): Float = if (m != null && m.isNotNIL) m.transform(this).y else y + +inline fun Vector2D.deltaTransformed(m: Matrix): Vector2D = m.deltaTransform(this) +inline fun Vector2D.transformed(m: Matrix): Vector2D = m.transform(this) +fun Vector2D.transformX(m: Matrix): Double = m.transform(this).x +fun Vector2D.transformY(m: Matrix): Double = m.transform(this).y +inline fun Vector2D.transformedNullable(m: Matrix?): Vector2D = if (m != null && m.isNotNIL) m.transform(this) else this +fun Vector2D.transformNullableX(m: Matrix?): Double = if (m != null && m.isNotNIL) m.transform(this).x else x +fun Vector2D.transformNullableY(m: Matrix?): Double = if (m != null && m.isNotNIL) m.transform(this).y else y + +fun List.bounds(): Rectangle = BoundsBuilder(size) { this + get(it) }.bounds +fun Iterable.bounds(): Rectangle { + var bb = BoundsBuilder() + for (p in this) bb += p + return bb.bounds +} + + +//inline operator fun Vector2F.plus(that: Size): Vector2F = Vector2F(x + that.width, y + that.height) +//inline operator fun Vector2F.minus(that: Size): Vector2F = Vector2F(x - that.width, y - that.height) +//inline operator fun Vector2F.times(that: Size): Vector2F = Vector2F(x * that.width, y * that.height) +//inline operator fun Vector2F.times(that: Scale): Vector2F = Vector2F(x * that.scaleX, y * that.scaleY) +//inline operator fun Vector2F.div(that: Size): Vector2F = Vector2F(x / that.width, y / that.height) +//inline operator fun Vector2F.rem(that: Size): Vector2F = Vector2F(x % that.width, y % that.height) + +@Deprecated("", ReplaceWith("ratio.interpolate(this, other)", "com.icegps.math.interpolation.interpolate")) +fun Vector2F.interpolateWith(ratio: Ratio, other: Vector2F): Vector2F = ratio.interpolate(this, other) + +// inline operator fun Vector2D.plus(that: Size): Vector2D = Vector2D(x + that.width, y + that.height) +// inline operator fun Vector2D.minus(that: Size): Vector2D = Vector2D(x - that.width, y - that.height) +// inline operator fun Vector2D.times(that: Size): Vector2D = Vector2D(x * that.width, y * that.height) +// inline operator fun Vector2D.times(that: Scale): Vector2D = Vector2D(x * that.scaleX, y * that.scaleY) +// inline operator fun Vector2D.div(that: Size): Vector2D = Vector2D(x / that.width, y / that.height) +// inline operator fun Vector2D.rem(that: Size): Vector2D = Vector2D(x % that.width, y % that.height) + +@Deprecated("", ReplaceWith("ratio.interpolate(this, other)", "com.icegps.math.interpolation.interpolate")) +fun Vector2D.interpolateWith(ratio: Ratio, other: Vector2D): Vector2D = ratio.interpolate(this, other) diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt b/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt new file mode 100644 index 0000000..c898163 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/VectorsDouble.kt @@ -0,0 +1,343 @@ +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.number.* +import kotlin.math.* + +typealias Point = Vector2D +typealias Point2 = Vector2D +typealias Point3 = Vector3D + +data class Vector3D(val x: Double, val y: Double, val z: Double) { + + constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble()) + constructor(x: Int, y: Int, z: Int) : this(x.toDouble(), y.toDouble(), z.toDouble()) + + constructor() : this(0.0, 0.0, 0.0) + + inline operator fun unaryMinus(): Vector3D = Vector3D(-x, -y, -z) + inline operator fun unaryPlus(): Vector3D = this + + inline operator fun plus(that: Vector3D): Vector3D = Vector3D(x + that.x, y + that.y, z + that.z) + inline operator fun minus(that: Vector3D): Vector3D = Vector3D(x - that.x, y - that.y, z - that.z) + inline operator fun times(that: Vector3D): Vector3D = Vector3D(x * that.x, y * that.y, z * that.z) + inline operator fun div(that: Vector3D): Vector3D = Vector3D(x / that.x, y / that.y, z / that.z) + inline operator fun rem(that: Vector3D): Vector3D = Vector3D(x % that.x, y % that.y, z % that.z) + + inline operator fun times(scale: Double): Vector3D = Vector3D(x * scale, y * scale, z * scale) + inline operator fun times(scale: Float): Vector3D = this * scale.toDouble() + inline operator fun times(scale: Int): Vector3D = this * scale.toDouble() + + inline operator fun div(scale: Double): Vector3D = Vector3D(x / scale, y / scale, z / scale) + inline operator fun div(scale: Float): Vector3D = this / scale.toDouble() + inline operator fun div(scale: Int): Vector3D = this / scale.toDouble() + + inline operator fun rem(scale: Double): Vector3D = Vector3D(x % scale, y % scale, z % scale) + inline operator fun rem(scale: Float): Vector3D = this % scale.toDouble() + inline operator fun rem(scale: Int): Vector3D = this % scale.toDouble() + + fun distanceTo(x: Double, y: Double, z: Double): Double = hypot(hypot(x - this.x, y - this.y), z - this.z) + fun distanceTo(x: Float, y: Float, z: Float): Double = distanceTo(x.toDouble(), y.toDouble(), z.toDouble()) + fun distanceTo(x: Int, y: Int, z: Int): Double = this.distanceTo(x.toDouble(), y.toDouble(), z.toDouble()) + fun distanceTo(that: Vector3D): Double = distanceTo(that.x, that.y, that.z) + + val length: Double get() = sqrt(x * x + y * y + z * z) + + fun normalized(): Vector3D { + val len = length + return if (len == 0.0) Vector3D(0.0, 0.0, 0.0) else this * (1.0 / len) + } + + infix fun cross(that: Vector3D) = Vector3D( + y * that.z - z * that.y, + z * that.x - x * that.z, + x * that.y - y * that.x + ) + + infix fun dot(that: Vector3D): Double = x * that.x + y * that.y + z * that.z + + companion object { + val FORWARD: Vector3D = Vector3D(0.0, 1.0, 0.0) // +Y 指向北 + val BACK: Vector3D = Vector3D(0.0, -1.0, 0.0) // -Y 指向南 + val RIGHT: Vector3D = Vector3D(1.0, 0.0, 0.0) // +X 指向东 + val LEFT: Vector3D = Vector3D(-1.0, 0.0, 0.0) // -X 指向西 + val UP: Vector3D = Vector3D(0.0, 0.0, 1.0) // +Z 指向天 + val DOWN: Vector3D = Vector3D(0.0, 0.0, -1.0) // -Z 指向地 + } +} + +fun Vector3D.toVector2D(): Vector2D = Vector2D(x, y) + +data class Vector4D(val x: Double, val y: Double, val z: Double, val w: Double) + +fun Vector3F.toDouble(): Vector3D = Vector3D(x.toDouble(), y.toDouble(), z.toDouble()) +fun Vector3D.toFloat(): Vector3F = Vector3F(x, y, z) + +data class Vector2D(val x: Double, val y: Double) : IsAlmostEquals { + //constructor(x: Float, y: Float) : this(float2PackOf(x, y)) + constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble()) + constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble()) + + constructor(x: Double, y: Int) : this(x.toDouble(), y.toDouble()) + constructor(x: Int, y: Double) : this(x.toDouble(), y.toDouble()) + + constructor(x: Float, y: Int) : this(x.toDouble(), y.toDouble()) + constructor(x: Int, y: Float) : this(x.toDouble(), y.toDouble()) + + //constructor(p: Vector2) : this(p.raw) + constructor() : this(0.0, 0.0) + //constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble()) + //constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble()) + + fun copy(x: Float = this.x.toFloat(), y: Float = this.y.toFloat()): Vector2D = Vector2D(x, y) + + inline operator fun unaryMinus(): Vector2D = Vector2D(-x, -y) + inline operator fun unaryPlus(): Vector2D = this + + inline operator fun plus(that: Vector2D): Vector2D = Vector2D(x + that.x, y + that.y) + inline operator fun minus(that: Vector2D): Vector2D = Vector2D(x - that.x, y - that.y) + inline operator fun times(that: Vector2D): Vector2D = Vector2D(x * that.x, y * that.y) + inline operator fun div(that: Vector2D): Vector2D = Vector2D(x / that.x, y / that.y) + inline operator fun rem(that: Vector2D): Vector2D = Vector2D(x % that.x, y % that.y) + + inline operator fun times(scale: Double): Vector2D = Vector2D(x * scale, y * scale) + inline operator fun times(scale: Float): Vector2D = this * scale.toDouble() + inline operator fun times(scale: Int): Vector2D = this * scale.toDouble() + + inline operator fun div(scale: Double): Vector2D = Vector2D(x / scale, y / scale) + inline operator fun div(scale: Float): Vector2D = this / scale.toDouble() + inline operator fun div(scale: Int): Vector2D = this / scale.toDouble() + + inline operator fun rem(scale: Double): Vector2D = Vector2D(x % scale, y % scale) + inline operator fun rem(scale: Float): Vector2D = this % scale.toDouble() + inline operator fun rem(scale: Int): Vector2D = this % scale.toDouble() + + fun avgComponent(): Double = x * 0.5 + y * 0.5 + fun minComponent(): Double = min(x, y) + fun maxComponent(): Double = max(x, y) + + fun distanceTo(x: Double, y: Double): Double = hypot(x - this.x, y - this.y) + fun distanceTo(x: Float, y: Float): Double = distanceTo(x.toDouble(), y.toDouble()) + fun distanceTo(x: Int, y: Int): Double = this.distanceTo(x.toDouble(), y.toDouble()) + fun distanceTo(that: Vector2D): Double = distanceTo(that.x, that.y) + + infix fun cross(that: Vector2D): Double = crossProduct(this, that) + infix fun dot(that: Vector2D): Double = ((this.x * that.x) + (this.y * that.y)) + + fun angleTo(other: Vector2D, up: Vector2D = UP): Angle = Angle.between(this.x, this.y, other.x, other.y, up) + val angle: Angle get() = angle() + fun angle(up: Vector2D = UP): Angle = Angle.between(0.0, 0.0, this.x, this.y, up) + + operator fun get(component: Int): Double = when (component) { + 0 -> x; 1 -> y + else -> throw IndexOutOfBoundsException("Point doesn't have $component component") + } + val length: Double get() = hypot(x, y) + val lengthSquared: Double get() { + val x = x + val y = y + return x*x + y*y + } + val magnitude: Double get() = hypot(x, y) + val normalized: Vector2D get() = this * (1f / magnitude) + val unit: Vector2D get() = this / length + + /** Normal vector. Rotates the vector/point -90 degrees (not normalizing it) */ + fun toNormal(): Vector2D = Vector2D(-this.y, this.x) + + + val int: Vector2I get() = Vector2I(x.toInt(), y.toInt()) + val intRound: Vector2I get() = Vector2I(x.roundToInt(), y.roundToInt()) + + fun roundDecimalPlaces(places: Int): Vector2D = Vector2D(x.roundDecimalPlaces(places), y.roundDecimalPlaces(places)) + fun round(): Vector2D = Vector2D(round(x), round(y)) + fun ceil(): Vector2D = Vector2D(ceil(x), ceil(y)) + fun floor(): Vector2D = Vector2D(floor(x), floor(y)) + + //fun copy(x: Double = this.x, y: Double = this.y): Vector2 = Vector2D(x, y) + + override fun isAlmostEquals(other: Vector2D, epsilon: Double): Boolean = + this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon) + + val niceStr: String get() = "(${x.niceStr}, ${y.niceStr})" + fun niceStr(decimalPlaces: Int): String = "(${x.niceStr(decimalPlaces)}, ${y.niceStr(decimalPlaces)})" + override fun toString(): String = niceStr + + fun reflected(normal: Vector2D): Vector2D { + val d = this + val n = normal + return d - 2.0 * (d dot n) * n + } + + /** Vector2 with inverted (1f / v) components to this */ + fun inv(): Vector2D = Vector2D(1.0 / x, 1.0 / y) + + fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() + + val absoluteValue: Vector2D get() = Vector2D(abs(x), abs(y)) + + companion object { + val ZERO = Vector2D(0.0, 0.0) + val NaN = Vector2D(Double.NaN, Double.NaN) + + /** Mathematically typical LEFT, matching screen coordinates (-1, 0) */ + val LEFT = Vector2D(-1.0, 0.0) + /** Mathematically typical RIGHT, matching screen coordinates (+1, 0) */ + val RIGHT = Vector2D(+1.0, 0.0) + + /** Mathematically typical UP (0, +1) */ + val UP = Vector2D(0.0, +1.0) + /** UP using screen coordinates as reference (0, -1) */ + val UP_SCREEN = Vector2D(0.0, -1.0) + + /** Mathematically typical DOWN (0, -1) */ + val DOWN = Vector2D(0.0, -1.0) + /** DOWN using screen coordinates as reference (0, +1) */ + val DOWN_SCREEN = Vector2D(0.0, +1.0) + + + inline operator fun invoke(x: Number, y: Number): Vector2D = Vector2D(x.toDouble(), y.toDouble()) + //inline operator fun invoke(x: Float, y: Float): Vector2D = Vector2D(x.toDouble(), y.toDouble()) + + //fun fromRaw(raw: Float2Pack) = Vector2D(raw) + + /** Constructs a point from polar coordinates determined by an [angle] and a [length]. Angle 0 is pointing to the right, and the direction is counter-clock-wise for up=UP and clock-wise for up=UP_SCREEN */ + inline fun polar(x: Float, y: Float, angle: Angle, length: Float = 1f, up: Vector2D = UP): Vector2D = Vector2D(x + angle.cosine(up) * length, y + angle.sine(up) * length) + inline fun polar(x: Double, y: Double, angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = Vector2D(x + angle.cosine(up) * length, y + angle.sine(up) * length) + inline fun polar(base: Vector2D, angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = polar(base.x, base.y, angle, length, up) + inline fun polar(angle: Angle, length: Double = 1.0, up: Vector2D = UP): Vector2D = polar(0.0, 0.0, angle, length, up) + + inline fun middle(a: Vector2D, b: Vector2D): Vector2D = (a + b) * 0.5 + + fun angle(ax: Double, ay: Double, bx: Double, by: Double, up: Vector2D = UP): Angle = Angle.between(ax, ay, bx, by, up) + fun angle(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double, up: Vector2D = UP): Angle = Angle.between(x1 - x2, y1 - y2, x1 - x3, y1 - y3, up) + + fun angle(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.between(a, b, up) + fun angle(p1: Vector2D, p2: Vector2D, p3: Vector2D, up: Vector2D = UP): Angle = Angle.between(p1 - p2, p1 - p3, up) + + fun angleArc(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.fromRadians(acos((a dot b) / (a.length * b.length))).adjustFromUp(up) + fun angleFull(a: Vector2D, b: Vector2D, up: Vector2D = UP): Angle = Angle.between(a, b, up) + + fun distance(a: Double, b: Double): Double = abs(a - b) + fun distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = hypot(x1 - x2, y1 - y2) + fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Double = hypot(x1 - x2, y1 - y2).toDouble() + fun distance(x1: Int, y1: Int, x2: Int, y2: Int): Double = hypot(x1.toDouble() - x2.toDouble(), y1.toDouble() - y2.toDouble()) + fun distance(a: Vector2D, b: Vector2D): Double = distance(a.x, a.y, b.x, b.y) + fun distance(a: Vector2I, b: Vector2I): Double = distance(a.x, a.y, b.x, b.y) + + fun distanceSquared(a: Vector2D, b: Vector2D): Double = distanceSquared(a.x, a.y, b.x, b.y) + fun distanceSquared(a: Vector2I, b: Vector2I): Int = distanceSquared(a.x, a.y, b.x, b.y) + fun distanceSquared(x1: Double, y1: Double, x2: Double, y2: Double): Double = square(x1 - x2) + square(y1 - y2) + fun distanceSquared(x1: Float, y1: Float, x2: Float, y2: Float): Float = square(x1 - x2) + square(y1 - y2) + fun distanceSquared(x1: Int, y1: Int, x2: Int, y2: Int): Int = square(x1 - x2) + square(y1 - y2) + + @Deprecated("Likely searching for orientation") + inline fun direction(a: Vector2D, b: Vector2D): Vector2D = b - a + + fun compare(l: Vector2D, r: Vector2D): Int = compare(l.x, l.y, r.x, r.y) + fun compare(lx: Float, ly: Float, rx: Float, ry: Float): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret } + fun compare(lx: Double, ly: Double, rx: Double, ry: Double): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret } + + private fun square(x: Double): Double = x * x + private fun square(x: Float): Float = x * x + private fun square(x: Int): Int = x * x + + fun dot(aX: Double, aY: Double, bX: Double, bY: Double): Double = (aX * bX) + (aY * bY) + fun dot(aX: Float, aY: Float, bX: Float, bY: Float): Float = (aX * bX) + (aY * bY) + fun dot(a: Vector2D, b: Vector2D): Double = dot(a.x, a.y, b.x, b.y) + + fun isCollinear(p1: Point, p2: Point, p3: Point): Boolean = + isCollinear(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y) + + fun isCollinear(p1x: Double, p1y: Double, p2x: Double, p2y: Double, p3x: Double, p3y: Double): Boolean { + val area2 = (p1x * (p2y - p3y) + p2x * (p3y - p1y) + p3x * (p1y - p2y)) // 2x triangle area + //println("($p1x, $p1y), ($p2x, $p2y), ($p3x, $p3y) :: area=$area2") + return area2.isAlmostZero() + + //val div1 = (p2x - p1x) / (p2y - p1y) + //val div2 = (p1x - p3x) / (p1y - p3y) + //val result = (div1 - div2).absoluteValue + //println("result=$result, div1=$div1, div2=$div2, xa=$p1x, ya=$p1y, x=$p2x, y=$p2y, xb=$p3x, yb=$p3y") + //if (div1.isInfinite() != div2.isInfinite()) return false + //return result.isAlmostZero() || result.isInfinite() + } + + fun isCollinear(xa: Float, ya: Float, x: Float, y: Float, xb: Float, yb: Float): Boolean = isCollinear( + xa.toDouble(), ya.toDouble(), + x.toDouble(), y.toDouble(), + xb.toDouble(), yb.toDouble(), + ) + + fun isCollinear(xa: Int, ya: Int, x: Int, y: Int, xb: Int, yb: Int): Boolean = isCollinear( + xa.toDouble(), ya.toDouble(), + x.toDouble(), y.toDouble(), + xb.toDouble(), yb.toDouble(), + ) + + // https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/ + /** < 0 left, > 0 right, 0 collinear */ + fun orientation(p1: Vector2D, p2: Vector2D, p3: Vector2D, up: Vector2D = UP): Double = orientation(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, up) + fun orientation(ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float, up: Vector2D = UP): Float { + Orientation.checkValidUpVector(up) + val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay) + return if (up.y > 0f) res else -res + } + fun orientation(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double, up: Vector2D = UP): Double { + Orientation.checkValidUpVector(up) + val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay) + return if (up.y > 0f) res else -res + } + + fun crossProduct(ax: Float, ay: Float, bx: Float, by: Float): Float = (ax * by) - (bx * ay) + fun crossProduct(ax: Double, ay: Double, bx: Double, by: Double): Double = (ax * by) - (bx * ay) + fun crossProduct(p1: Vector2D, p2: Vector2D): Double = crossProduct(p1.x, p1.y, p2.x, p2.y) + + fun minComponents(p1: Vector2D, p2: Vector2D): Vector2D = Vector2D(min(p1.x, p2.x), min(p1.y, p2.y)) + fun minComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D): Vector2D = Vector2D( + minOf(p1.x, p2.x, p3.x), + minOf(p1.y, p2.y, p3.y) + ) + fun minComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D, p4: Vector2D): Vector2D = Vector2D( + minOf( + p1.x, + p2.x, + p3.x, + p4.x + ), minOf(p1.y, p2.y, p3.y, p4.y) + ) + fun maxComponents(p1: Vector2D, p2: Vector2D): Vector2D = Vector2D(max(p1.x, p2.x), max(p1.y, p2.y)) + fun maxComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D): Vector2D = Vector2D( + maxOf(p1.x, p2.x, p3.x), + maxOf(p1.y, p2.y, p3.y) + ) + fun maxComponents(p1: Vector2D, p2: Vector2D, p3: Vector2D, p4: Vector2D): Vector2D = Vector2D( + maxOf( + p1.x, + p2.x, + p3.x, + p4.x + ), maxOf(p1.y, p2.y, p3.y, p4.y) + ) + } +} + +operator fun Int.times(v: Vector2D): Vector2D = v * this +operator fun Float.times(v: Vector2D): Vector2D = v * this +operator fun Double.times(v: Vector2D): Vector2D = v * this + +fun Vector2D.toFloat(): Vector2F = Vector2F(x, y) +fun Vector2F.toDouble(): Vector2D = Vector2D(x, y) + +fun abs(a: Vector2D): Vector2D = a.absoluteValue +fun min(a: Vector2D, b: Vector2D): Vector2D = Vector2D(min(a.x, b.x), min(a.y, b.y)) +fun max(a: Vector2D, b: Vector2D): Vector2D = Vector2D(max(a.x, b.x), max(a.y, b.y)) +fun Vector2D.clamp(min: Float, max: Float): Vector2D = clamp(min.toDouble(), max.toDouble()) +fun Vector2D.clamp(min: Double, max: Double): Vector2D = Vector2D(x.clamp(min, max), y.clamp(min, max)) +fun Vector2D.clamp(min: Vector2D, max: Vector2D): Vector2D = Vector2D(x.clamp(min.x, max.x), y.clamp(min.y, max.y)) + +fun Vector2D.toInt(): Vector2I = Vector2I(x.toInt(), y.toInt()) +fun Vector2D.toIntCeil(): Vector2I = Vector2I(x.toIntCeil(), y.toIntCeil()) +fun Vector2D.toIntRound(): Vector2I = Vector2I(x.toIntRound(), y.toIntRound()) +fun Vector2D.toIntFloor(): Vector2I = Vector2I(x.toIntFloor(), y.toIntFloor()) + +fun Vector3D.toCylindrical(): CylindricalVector = CylindricalVector.fromCartesian(this) diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt b/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt new file mode 100644 index 0000000..f6a726a --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/VectorsFloat.kt @@ -0,0 +1,523 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package com.icegps.math.geometry + +import com.icegps.math.* +import com.icegps.number.* +import kotlin.math.* + +typealias Vector2 = Vector2F +typealias Vector3 = Vector3F +typealias Vector4 = Vector4F + +fun vec(x: Float, y: Float): Vector2F = Vector2F(x, y) +fun vec2(x: Float, y: Float): Vector2F = Vector2F(x, y) +fun vec(x: Float, y: Float, z: Float): Vector3F = Vector3F(x, y, z) +fun vec3(x: Float, y: Float, z: Float): Vector3F = Vector3F(x, y, z) +fun vec(x: Float, y: Float, z: Float, w: Float): Vector4F = Vector4F(x, y, z, w) +fun vec4(x: Float, y: Float, z: Float, w: Float = 1f): Vector4F = Vector4F(x, y, z, w) + +////////////////////////////// +// VALUE CLASSES +////////////////////////////// + +//@Deprecated("", ReplaceWith("p", "com.icegps.math.geometry.Point")) fun Point(p: Vector2F): Vector2F = p +//@Deprecated("", ReplaceWith("p", "com.icegps.math.geometry.Vector2")) fun Vector2(p: Vector2F): Vector2F = p + +data class Vector2F(val x: Float, val y: Float) { + constructor(x: Double, y: Double) : this(x.toFloat(), y.toFloat()) + constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat()) + + constructor(x: Double, y: Int) : this(x.toFloat(), y.toFloat()) + constructor(x: Int, y: Double) : this(x.toFloat(), y.toFloat()) + + constructor(x: Float, y: Int) : this(x.toFloat(), y.toFloat()) + constructor(x: Int, y: Float) : this(x.toFloat(), y.toFloat()) + + //constructor(p: Vector2) : this(p.raw) + constructor() : this(0f, 0f) + //constructor(x: Int, y: Int) : this(x.toDouble(), y.toDouble()) + //constructor(x: Float, y: Float) : this(x.toDouble(), y.toDouble()) + + fun copy(x: Double = this.x.toDouble(), y: Double = this.y.toDouble()): Vector2F = Vector2F(x, y) + + inline operator fun unaryMinus(): Vector2F = Vector2F(-x, -y) + inline operator fun unaryPlus(): Vector2F = this + + inline operator fun plus(that: Vector2F): Vector2F = Vector2F(x + that.x, y + that.y) + inline operator fun minus(that: Vector2F): Vector2F = Vector2F(x - that.x, y - that.y) + inline operator fun times(that: Vector2F): Vector2F = Vector2F(x * that.x, y * that.y) + inline operator fun div(that: Vector2F): Vector2F = Vector2F(x / that.x, y / that.y) + inline operator fun rem(that: Vector2F): Vector2F = Vector2F(x % that.x, y % that.y) + + inline operator fun times(scale: Float): Vector2F = Vector2F(x * scale, y * scale) + inline operator fun times(scale: Double): Vector2F = this * scale.toFloat() + inline operator fun times(scale: Int): Vector2F = this * scale.toDouble() + + inline operator fun div(scale: Float): Vector2F = Vector2F(x / scale, y / scale) + inline operator fun div(scale: Double): Vector2F = this / scale.toFloat() + inline operator fun div(scale: Int): Vector2F = this / scale.toDouble() + + inline operator fun rem(scale: Float): Vector2F = Vector2F(x % scale, y % scale) + inline operator fun rem(scale: Double): Vector2F = this % scale.toFloat() + inline operator fun rem(scale: Int): Vector2F = this % scale.toDouble() + + fun avgComponent(): Float = x * 0.5f + y * 0.5f + fun minComponent(): Float = min(x, y) + fun maxComponent(): Float = max(x, y) + + fun distanceTo(x: Float, y: Float): Float = hypot(x - this.x, y - this.y) + fun distanceTo(x: Double, y: Double): Float = this.distanceTo(x.toFloat(), y.toFloat()) + fun distanceTo(x: Int, y: Int): Float = this.distanceTo(x.toDouble(), y.toDouble()) + fun distanceTo(that: Vector2F): Float = distanceTo(that.x, that.y) + + infix fun cross(that: Vector2F): Float = crossProduct(this, that) + infix fun dot(that: Vector2F): Float = ((this.x * that.x) + (this.y * that.y)) + + fun angleTo(other: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(this.x, this.y, other.x, other.y, up) + val angle: Angle get() = angle() + fun angle(up: Vector2D = Vector2D.UP): Angle = Angle.between(0f, 0f, this.x, this.y, up) + + operator fun get(component: Int) = when (component) { + 0 -> x; 1 -> y + else -> throw IndexOutOfBoundsException("Point doesn't have $component component") + } + val length: Float get() = hypot(x, y) + val lengthSquared: Float get() { + val x = x + val y = y + return x*x + y*y + } + val magnitude: Float get() = hypot(x, y) + val normalized: Vector2F get() = this * (1f / magnitude) + val unit: Vector2F get() = this / length + + /** Normal vector. Rotates the vector/point -90 degrees (not normalizing it) */ + fun toNormal(): Vector2F = Vector2F(-this.y, this.x) + + + val int: Vector2I get() = Vector2I(x.toInt(), y.toInt()) + val intRound: Vector2I get() = Vector2I(x.roundToInt(), y.roundToInt()) + + fun roundDecimalPlaces(places: Int): Vector2F = Vector2F(x.roundDecimalPlaces(places), y.roundDecimalPlaces(places)) + fun round(): Vector2F = Vector2F(round(x), round(y)) + fun ceil(): Vector2F = Vector2F(ceil(x), ceil(y)) + fun floor(): Vector2F = Vector2F(floor(x), floor(y)) + + //fun copy(x: Double = this.x, y: Double = this.y): Vector2 = Point(x, y) + + fun isAlmostEquals(other: Vector2F, epsilon: Float = 0.00001f): Boolean = + this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon) + + val niceStr: String get() = "(${x.niceStr}, ${y.niceStr})" + fun niceStr(decimalPlaces: Int): String = "(${x.niceStr(decimalPlaces)}, ${y.niceStr(decimalPlaces)})" + override fun toString(): String = niceStr + + fun reflected(normal: Vector2F): Vector2F { + val d = this + val n = normal + return d - 2f * (d dot n) * n + } + + /** Vector2 with inverted (1f / v) components to this */ + fun inv(): Vector2F = Vector2F(1f / x, 1f / y) + + fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() + + val absoluteValue: Vector2F get() = Vector2F(abs(x), abs(y)) + + companion object { + val ZERO = Vector2F(0f, 0f) + val NaN = Vector2F(Float.NaN, Float.NaN) + + /** Mathematically typical LEFT, matching screen coordinates (-1, 0) */ + val LEFT = Vector2F(-1f, 0f) + /** Mathematically typical RIGHT, matching screen coordinates (+1, 0) */ + val RIGHT = Vector2F(+1f, 0f) + + /** Mathematically typical UP (0, +1) */ + val UP = Vector2F(0f, +1f) + /** UP using 2D screen coordinates as reference (0, -1) */ + val UP_SCREEN = Vector2F(0f, -1f) + + /** Mathematically typical DOWN (0, -1) */ + val DOWN = Vector2F(0f, -1f) + /** DOWN using 2D screen coordinates as reference (0, +1) */ + val DOWN_SCREEN = Vector2F(0f, +1f) + + + //inline operator fun invoke(x: Int, y: Int): Vector2 = Point(x.toDouble(), y.toDouble()) + //inline operator fun invoke(x: Float, y: Float): Vector2 = Point(x.toDouble(), y.toDouble()) + + //fun fromRaw(raw: Float2Pack) = Point(raw) + + /** Constructs a point from polar coordinates determined by an [angle] and a [length]. Angle 0 is pointing to the right, and the direction is counter-clock-wise for up=UP and clock-wise for up=UP_SCREEN */ + inline fun polar(x: Float, y: Float, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = Vector2F(x + angle.cosine(up) * length, y + angle.sine(up) * length) + inline fun polar(x: Double, y: Double, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = Vector2F(x + angle.cosine(up) * length, y + angle.sine(up) * length) + inline fun polar(base: Vector2F, angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = polar(base.x, base.y, angle, length, up) + inline fun polar(angle: Angle, length: Float = 1f, up: Vector2D = Vector2D.UP): Vector2F = polar(0.0, 0.0, angle, length, up) + + inline fun middle(a: Vector2F, b: Vector2F): Vector2F = (a + b) * 0.5 + + fun angle(ax: Double, ay: Double, bx: Double, by: Double, up: Vector2D = Vector2D.UP): Angle = Angle.between(ax, ay, bx, by, up) + fun angle(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double, up: Vector2D = Vector2D.UP): Angle = Angle.between(x1 - x2, y1 - y2, x1 - x3, y1 - y3, up) + + fun angle(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(a, b, up) + fun angle(p1: Vector2F, p2: Vector2F, p3: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(p1 - p2, p1 - p3, up) + + fun angleArc(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.fromRadians(acos((a dot b) / (a.length * b.length))).adjustFromUp(up) + fun angleFull(a: Vector2F, b: Vector2F, up: Vector2D = Vector2D.UP): Angle = Angle.between(a, b, up) + + fun distance(a: Double, b: Double): Double = abs(a - b) + fun distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = hypot(x1 - x2, y1 - y2) + fun distance(x1: Float, y1: Float, x2: Float, y2: Float): Float = hypot(x1 - x2, y1 - y2) + fun distance(x1: Int, y1: Int, x2: Int, y2: Int): Float = distance(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat()) + fun distance(a: Vector2F, b: Vector2F): Float = distance(a.x, a.y, b.x, b.y) + fun distance(a: Vector2I, b: Vector2I): Float = distance(a.x, a.y, b.x, b.y) + + fun distanceSquared(a: Vector2F, b: Vector2F): Float = distanceSquared(a.x, a.y, b.x, b.y) + fun distanceSquared(a: Vector2I, b: Vector2I): Int = distanceSquared(a.x, a.y, b.x, b.y) + fun distanceSquared(x1: Double, y1: Double, x2: Double, y2: Double): Double = square(x1 - x2) + square(y1 - y2) + fun distanceSquared(x1: Float, y1: Float, x2: Float, y2: Float): Float = square(x1 - x2) + square(y1 - y2) + fun distanceSquared(x1: Int, y1: Int, x2: Int, y2: Int): Int = square(x1 - x2) + square(y1 - y2) + + @Deprecated("Likely searching for orientation") + inline fun direction(a: Vector2F, b: Vector2F): Vector2F = b - a + + fun compare(l: Vector2F, r: Vector2F): Int = compare(l.x, l.y, r.x, r.y) + fun compare(lx: Float, ly: Float, rx: Float, ry: Float): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret } + fun compare(lx: Double, ly: Double, rx: Double, ry: Double): Int = ly.compareTo(ry).let { ret -> if (ret == 0) lx.compareTo(rx) else ret } + + private fun square(x: Double): Double = x * x + private fun square(x: Float): Float = x * x + private fun square(x: Int): Int = x * x + + fun dot(aX: Double, aY: Double, bX: Double, bY: Double): Double = (aX * bX) + (aY * bY) + fun dot(aX: Float, aY: Float, bX: Float, bY: Float): Float = (aX * bX) + (aY * bY) + fun dot(a: Vector2F, b: Vector2F): Float = dot(a.x, a.y, b.x, b.y) + + fun isCollinear(p1: Point, p2: Point, p3: Point): Boolean = + isCollinear(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y) + + fun isCollinear(p1x: Float, p1y: Float, p2x: Float, p2y: Float, p3x: Float, p3y: Float): Boolean { + val area2 = (p1x * (p2y - p3y) + p2x * (p3y - p1y) + p3x * (p1y - p2y)) // 2x triangle area + //println("($p1x, $p1y), ($p2x, $p2y), ($p3x, $p3y) :: area=$area2") + return area2.isAlmostZero() + + //val div1 = (p2x - p1x) / (p2y - p1y) + //val div2 = (p1x - p3x) / (p1y - p3y) + //val result = (div1 - div2).absoluteValue + //println("result=$result, div1=$div1, div2=$div2, xa=$p1x, ya=$p1y, x=$p2x, y=$p2y, xb=$p3x, yb=$p3y") + //if (div1.isInfinite() != div2.isInfinite()) return false + //return result.isAlmostZero() || result.isInfinite() + } + + fun isCollinear(xa: Double, ya: Double, x: Double, y: Double, xb: Double, yb: Double): Boolean = isCollinear( + xa.toFloat(), ya.toFloat(), + x.toFloat(), y.toFloat(), + xb.toFloat(), yb.toFloat(), + ) + + fun isCollinear(xa: Int, ya: Int, x: Int, y: Int, xb: Int, yb: Int): Boolean = isCollinear( + xa.toFloat(), ya.toFloat(), + x.toFloat(), y.toFloat(), + xb.toFloat(), yb.toFloat(), + ) + + // https://algorithmtutor.com/Computational-Geometry/Determining-if-two-consecutive-segments-turn-left-or-right/ + /** < 0 left, > 0 right, 0 collinear */ + fun orientation(p1: Vector2F, p2: Vector2F, p3: Vector2F, up: Vector2D = Vector2D.UP): Float = orientation(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, up) + fun orientation(ax: Float, ay: Float, bx: Float, by: Float, cx: Float, cy: Float, up: Vector2D = Vector2D.UP): Float { + Orientation.checkValidUpVector(up) + val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay) + return if (up.y > 0f) res else -res + } + fun orientation(ax: Double, ay: Double, bx: Double, by: Double, cx: Double, cy: Double, up: Vector2D = Vector2D.UP): Double { + Orientation.checkValidUpVector(up) + val res = crossProduct(cx - ax, cy - ay, bx - ax, by - ay) + return if (up.y > 0f) res else -res + } + + fun crossProduct(ax: Float, ay: Float, bx: Float, by: Float): Float = (ax * by) - (bx * ay) + fun crossProduct(ax: Double, ay: Double, bx: Double, by: Double): Double = (ax * by) - (bx * ay) + fun crossProduct(p1: Vector2F, p2: Vector2F): Float = crossProduct(p1.x, p1.y, p2.x, p2.y) + + fun minComponents(p1: Vector2F, p2: Vector2F): Vector2F = Vector2F(min(p1.x, p2.x), min(p1.y, p2.y)) + fun minComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F): Vector2F = Vector2F( + minOf(p1.x, p2.x, p3.x), + minOf(p1.y, p2.y, p3.y) + ) + fun minComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F, p4: Vector2F): Vector2F = Vector2F( + minOf( + p1.x, + p2.x, + p3.x, + p4.x + ), minOf(p1.y, p2.y, p3.y, p4.y) + ) + fun maxComponents(p1: Vector2F, p2: Vector2F): Vector2F = Vector2F(max(p1.x, p2.x), max(p1.y, p2.y)) + fun maxComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F): Vector2F = Vector2F( + maxOf(p1.x, p2.x, p3.x), + maxOf(p1.y, p2.y, p3.y) + ) + fun maxComponents(p1: Vector2F, p2: Vector2F, p3: Vector2F, p4: Vector2F): Vector2F = Vector2F( + maxOf( + p1.x, + p2.x, + p3.x, + p4.x + ), maxOf(p1.y, p2.y, p3.y, p4.y) + ) + } +} + +operator fun Int.times(v: Vector2F): Vector2F = v * this +operator fun Float.times(v: Vector2F): Vector2F = v * this +operator fun Double.times(v: Vector2F): Vector2F = v * this + +fun abs(a: Vector2F): Vector2F = a.absoluteValue +fun min(a: Vector2F, b: Vector2F): Vector2F = Vector2F(min(a.x, b.x), min(a.y, b.y)) +fun max(a: Vector2F, b: Vector2F): Vector2F = Vector2F(max(a.x, b.x), max(a.y, b.y)) +fun Vector2F.clamp(min: Float, max: Float): Vector2F = Vector2F(x.clamp(min, max), y.clamp(min, max)) +fun Vector2F.clamp(min: Double, max: Double): Vector2F = clamp(min.toFloat(), max.toFloat()) +fun Vector2F.clamp(min: Vector2F, max: Vector2F): Vector2F = Vector2F(x.clamp(min.x, max.x), y.clamp(min.y, max.y)) + +fun Vector2F.toInt(): Vector2I = Vector2I(x.toInt(), y.toInt()) +fun Vector2F.toIntCeil(): Vector2I = Vector2I(x.toIntCeil(), y.toIntCeil()) +fun Vector2F.toIntRound(): Vector2I = Vector2I(x.toIntRound(), y.toIntRound()) +fun Vector2F.toIntFloor(): Vector2I = Vector2I(x.toIntFloor(), y.toIntFloor()) + + +data class Vector3F(val x: Float, val y: Float, val z: Float) : IsAlmostEqualsF { + companion object { + val NaN = Vector3F(Float.NaN, Float.NaN, Float.NaN) + + val ZERO = Vector3F(0f, 0f, 0f) + val ONE = Vector3F(1f, 1f, 1f) + + val FORWARD = Vector3F(0f, 0f, 1f) + val BACK = Vector3F(0f, 0f, -1f) + val LEFT = Vector3F(-1f, 0f, 0f) + val RIGHT = Vector3F(1f, 0f, 0f) + val UP = Vector3F(0f, 1f, 0f) + val DOWN = Vector3F(0f, -1f, 0f) + + operator fun invoke(): Vector3F = ZERO + + fun cross(a: Vector3F, b: Vector3F): Vector3F = Vector3F( + ((a.y * b.z) - (a.z * b.y)), + ((a.z * b.x) - (a.x * b.z)), + ((a.x * b.y) - (a.y * b.x)), + ) + + fun length(x: Float, y: Float, z: Float): Float = sqrt(lengthSq(x, y, z)) + fun lengthSq(x: Float, y: Float, z: Float): Float = x * x + y * y + z * z + + fun fromArray(array: FloatArray, offset: Int): Vector3F = + Vector3F(array[offset + 0], array[offset + 1], array[offset + 2]) + + inline fun func(func: (index: Int) -> Float): Vector3F = Vector3F(func(0), func(1), func(2)) + } + + //constructor(x: Float, y: Float, z: Float) : this(float4PackOf(x, y, z, 0f)) + constructor(x: Int, y: Int, z: Int) : this(x.toFloat(), y.toFloat(), z.toFloat()) + constructor(x: Double, y: Double, z: Double) : this(x.toFloat(), y.toFloat(), z.toFloat()) + + fun distanceTo(other: Vector3F): Float { + val dx = this.x - other.x + val dy = this.y - other.y + val dz = this.z - other.z + return sqrt(dx * dx + dy * dy + dz * dz) + } + + val lengthSquared: Float get() = (x * x) + (y * y) + (z * z) + val length: Float get() = sqrt(lengthSquared) + fun normalized(): Vector3F { + val length = this.length + //if (length.isAlmostZero()) return Vector3.ZERO + if (length == 0f) return Vector3F.ZERO + return this / length + } + + // https://math.stackexchange.com/questions/13261/how-to-get-a-reflection-vector + // 𝑟=𝑑−2(𝑑⋅𝑛)𝑛 + fun reflected(surfaceNormal: Vector3F): Vector3F { + val d = this + val n = surfaceNormal + return d - 2f * (d dot n) * n + } + + operator fun get(index: Int): Float = when (index) { + 0 -> x + 1 -> y + 2 -> z + else -> throw IndexOutOfBoundsException() + } + + operator fun unaryPlus(): Vector3F = this + operator fun unaryMinus(): Vector3F = Vector3F(-this.x, -this.y, -this.z) + + operator fun plus(v: Vector3F): Vector3F = Vector3F(this.x + v.x, this.y + v.y, this.z + v.z) + operator fun minus(v: Vector3F): Vector3F = Vector3F(this.x - v.x, this.y - v.y, this.z - v.z) + + operator fun times(v: Vector3F): Vector3F = Vector3F(this.x * v.x, this.y * v.y, this.z * v.z) + operator fun div(v: Vector3F): Vector3F = Vector3F(this.x / v.x, this.y / v.y, this.z / v.z) + operator fun rem(v: Vector3F): Vector3F = Vector3F(this.x % v.x, this.y % v.y, this.z % v.z) + + operator fun times(v: Float): Vector3F = Vector3F(this.x * v, this.y * v, this.z * v) + operator fun div(v: Float): Vector3F = Vector3F(this.x / v, this.y / v, this.z / v) + operator fun rem(v: Float): Vector3F = Vector3F(this.x % v, this.y % v, this.z % v) + + operator fun times(v: Int): Vector3F = this * v.toFloat() + operator fun div(v: Int): Vector3F = this / v.toFloat() + operator fun rem(v: Int): Vector3F = this % v.toFloat() + + operator fun times(v: Double): Vector3F = this * v.toFloat() + operator fun div(v: Double): Vector3F = this / v.toFloat() + operator fun rem(v: Double): Vector3F = this % v.toFloat() + + infix fun dot(v: Vector3F): Float = (x * v.x) + (y * v.y) + (z * v.z) + infix fun cross(v: Vector3F): Vector3F = cross(this, v) + + /** Vector3 with inverted (1f / v) components to this */ + fun inv(): Vector3F = Vector3F(1f / x, 1f / y, 1f / z) + + fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() && this.z.isNaN() + val absoluteValue: Vector3F get() = Vector3F(abs(x), abs(y), abs(z)) + + override fun toString(): String = "Vector3(${x.niceStr}, ${y.niceStr}, ${z.niceStr})" + + fun toVector4(w: Float = 1f): Vector4F = Vector4F(x, y, z, w) + override fun isAlmostEquals(other: Vector3F, epsilon: Float): Boolean = + this.x.isAlmostEquals(other.x, epsilon) && + this.y.isAlmostEquals(other.y, epsilon) && + this.z.isAlmostEquals(other.z, epsilon) +} + +operator fun Int.times(v: Vector3F): Vector3F = v * this +operator fun Float.times(v: Vector3F): Vector3F = v * this +operator fun Double.times(v: Vector3F): Vector3F = v * this + +fun abs(a: Vector3F): Vector3F = a.absoluteValue +fun min(a: Vector3F, b: Vector3F): Vector3F = Vector3F(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)) +fun max(a: Vector3F, b: Vector3F): Vector3F = Vector3F(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)) +fun Vector3F.clamp(min: Float, max: Float): Vector3F = Vector3F(x.clamp(min, max), y.clamp(min, max), z.clamp(min, max)) +fun Vector3F.clamp(min: Double, max: Double): Vector3F = clamp(min.toFloat(), max.toFloat()) +fun Vector3F.clamp(min: Vector3F, max: Vector3F): Vector3F = Vector3F(x.clamp(min.x, max.x), y.clamp(min.y, max.y), z.clamp(min.z, max.z)) + +data class Vector4F(val x: Float, val y: Float, val z: Float, val w: Float) { + companion object { + val ZERO = Vector4F(0f, 0f, 0f, 0f) + val ONE = Vector4F(1f, 1f, 1f, 1f) + + operator fun invoke(): Vector4F = Vector4F.ZERO + + fun fromArray(array: FloatArray, offset: Int = 0): Vector4F = Vector4F(array[offset + 0], array[offset + 1], array[offset + 2], array[offset + 3]) + + fun length(x: Float, y: Float, z: Float, w: Float): Float = sqrt(lengthSq(x, y, z, w)) + fun lengthSq(x: Float, y: Float, z: Float, w: Float): Float = x * x + y * y + z * z + w * w + + inline fun func(func: (index: Int) -> Float): Vector4F = Vector4F(func(0), func(1), func(2), func(3)) + } + + constructor(xyz: Vector3F, w: Float) : this(xyz.x, xyz.y, xyz.z, w) + //constructor(x: Float, y: Float, z: Float, w: Float) : this(float4PackOf(x, y, z, w)) + constructor(x: Int, y: Int, z: Int, w: Int) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + constructor(x: Double, y: Double, z: Double, w: Double) : this(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) + + val xyz: Vector3F get() = Vector3F(x, y, z) + + val length3Squared: Float get() = (x * x) + (y * y) + (z * z) + /** Only taking into accoount x, y, z */ + val length3: Float get() = sqrt(length3Squared) + + val lengthSquared: Float get() = (x * x) + (y * y) + (z * z) + (w * w) + val length: Float get() = sqrt(lengthSquared) + + fun normalized(): Vector4F { + val length = this.length + if (length == 0f) return Vector4F.ZERO + return this / length + } + + operator fun get(index: Int): Float = when (index) { + 0 -> x + 1 -> y + 2 -> z + 3 -> w + else -> throw IndexOutOfBoundsException() + } + + operator fun unaryPlus(): Vector4F = this + operator fun unaryMinus(): Vector4F = Vector4F(-x, -y, -z, -w) + + operator fun plus(v: Vector4F): Vector4F = Vector4F(x + v.x, y + v.y, z + v.z, w + v.w) + operator fun minus(v: Vector4F): Vector4F = Vector4F(x - v.x, y - v.y, z - v.z, w - v.w) + + operator fun times(v: Vector4F): Vector4F = Vector4F(x * v.x, y * v.y, z * v.z, w * v.w) + operator fun div(v: Vector4F): Vector4F = Vector4F(x / v.x, y / v.y, z / v.z, w / v.w) + operator fun rem(v: Vector4F): Vector4F = Vector4F(x % v.x, y % v.y, z % v.z, w % v.w) + + operator fun times(v: Float): Vector4F = Vector4F(x * v, y * v, z * v, w * v) + operator fun div(v: Float): Vector4F = Vector4F(x / v, y / v, z / v, w / v) + operator fun rem(v: Float): Vector4F = Vector4F(x % v, y % v, z % v, w % v) + + infix fun dot(v: Vector4F): Float = (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w) + //infix fun cross(v: Vector4): Vector4 = cross(this, v) + + fun copyTo(out: FloatArray, offset: Int = 0): FloatArray { + out[offset + 0] = x + out[offset + 1] = y + out[offset + 2] = z + out[offset + 3] = w + return out + } + + /** Vector4 with inverted (1f / v) components to this */ + fun inv(): Vector4F = Vector4F(1f / x, 1f / y, 1f / z, 1f / w) + + fun isNaN(): Boolean = this.x.isNaN() && this.y.isNaN() && this.z.isNaN() && this.w.isNaN() + val absoluteValue: Vector4F get() = Vector4F(abs(x), abs(y), abs(z), abs(w)) + + override fun toString(): String = "Vector4(${x.niceStr}, ${y.niceStr}, ${z.niceStr}, ${w.niceStr})" + + // @TODO: Should we scale Vector3 by w? + fun toVector3(): Vector3F = Vector3F(x, y, z) + fun isAlmostEquals(other: Vector4F, epsilon: Float = 0.00001f): Boolean = + this.x.isAlmostEquals(other.x, epsilon) && this.y.isAlmostEquals(other.y, epsilon) && this.z.isAlmostEquals(other.z, epsilon) && this.w.isAlmostEquals(other.w, epsilon) +} + +fun abs(a: Vector4F): Vector4F = a.absoluteValue +fun min(a: Vector4F, b: Vector4F): Vector4F = Vector4F(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z), min(a.w, b.w)) +fun max(a: Vector4F, b: Vector4F): Vector4F = Vector4F(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z), max(a.w, b.w)) +fun Vector4F.clamp(min: Float, max: Float): Vector4F = Vector4F(x.clamp(min, max), y.clamp(min, max), z.clamp(min, max), w.clamp(min, max)) +fun Vector4F.clamp(min: Double, max: Double): Vector4F = clamp(min.toFloat(), max.toFloat()) +fun Vector4F.clamp(min: Vector4F, max: Vector4F): Vector4F = Vector4F(x.clamp(min.x, max.x), y.clamp(min.y, max.y), z.clamp(min.z, max.z), w.clamp(min.w, max.w)) + +data class CylindricalVector( + val radius: Double = 1.0, + val angle: Angle = Angle.ZERO, + val y: Double = 0.0, +) { + fun toVector3(): Vector3F = toCartesian(this).toFloat() + + companion object { + fun fromCartesian(v: Vector3F): CylindricalVector = fromCartesian(v.x, v.y, v.z) + fun fromCartesian(v: Vector3D): CylindricalVector = fromCartesian(v.x, v.y, v.z) + inline fun fromCartesian(x: Number, y: Number, z: Number): CylindricalVector = fromCartesian(x.toDouble(), y.toDouble(), z.toDouble()) + fun fromCartesian(x: Double, y: Double, z: Double): CylindricalVector = CylindricalVector( + radius = sqrt(x * x + z * z), + angle = Angle.atan2(x, z), + y = y, + ) + + fun toCartesian(c: CylindricalVector): Vector3D = toCartesian(c.radius, c.angle, c.y) + fun toCartesian(radius: Double, angle: Angle, y: Double): Vector3D = Vector3D( + x = radius * sin(angle), + y = y, + z = radius * cos(angle), + ) + } +} + +fun Vector3F.toCylindrical(): CylindricalVector = CylindricalVector.fromCartesian(this) diff --git a/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt b/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt new file mode 100644 index 0000000..0a94709 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/VectorsInt.kt @@ -0,0 +1,41 @@ +package com.icegps.math.geometry + +typealias PointInt = Vector2I + +data class Vector3I(val x: Int, val y: Int, val z: Int) +data class Vector4I(val x: Int, val y: Int, val z: Int, val w: Int) + +//@KormaValueApi +data class Vector2I(val x: Int, val y: Int) { + //operator fun component1(): Int = x + //operator fun component2(): Int = y + //fun copy(x: Int = this.x, y: Int = this.y): Vector2Int = Vector2Int(x, y) + +//inline class Vector2Int(internal val raw: Int2Pack) { + + companion object { + val ZERO = Vector2I(0, 0) + + fun compare(lx: Int, ly: Int, rx: Int, ry: Int): Int { + val ret = ly.compareTo(ry) + return if (ret == 0) lx.compareTo(rx) else ret + } + } + + //val x: Int get() = raw.i0 + //val y: Int get() = raw.i1 + + constructor() : this(0, 0) + //constructor(x: Int, y: Int) : this(int2PackOf(x, y)) + + operator fun plus(that: Vector2I): Vector2I = Vector2I(this.x + that.x, this.y + that.y) + operator fun minus(that: Vector2I): Vector2I = Vector2I(this.x - that.x, this.y - that.y) + operator fun times(that: Vector2I): Vector2I = Vector2I(this.x * that.x, this.y * that.y) + operator fun div(that: Vector2I): Vector2I = Vector2I(this.x / that.x, this.y / that.y) + operator fun rem(that: Vector2I): Vector2I = Vector2I(this.x % that.x, this.y % that.y) + + override fun toString(): String = "($x, $y)" +} + +fun Vector2I.toFloat(): Vector2F = Vector2F(x, y) +fun Vector2I.toDouble(): Vector2D = Vector2D(x, y) diff --git a/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt new file mode 100644 index 0000000..0152bd9 --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape2D.kt @@ -0,0 +1,15 @@ +package com.icegps.math.geometry.shape + +import com.icegps.math.geometry.* + +interface SimpleShape2D { + val closed: Boolean + val area: Double + val perimeter: Double + val center: Point + fun distance(p: Point): Double = projectedPoint(p).distanceTo(p) + fun normalVectorAt(p: Point): Vector2D = (p - projectedPoint(p)).normalized + fun projectedPoint(p: Point): Point + fun containsPoint(p: Point): Boolean + fun getBounds(): Rectangle +} \ No newline at end of file diff --git a/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt new file mode 100644 index 0000000..735824e --- /dev/null +++ b/math/src/main/java/com/icegps/math/geometry/shape/SimpleShape3D.kt @@ -0,0 +1,8 @@ +package com.icegps.math.geometry.shape + +import com.icegps.math.geometry.* + +interface SimpleShape3D { + val center: Vector3F + val volume: Float +} diff --git a/math/src/main/java/com/icegps/math/interpolation/Easing.kt b/math/src/main/java/com/icegps/math/interpolation/Easing.kt new file mode 100644 index 0000000..17194c6 --- /dev/null +++ b/math/src/main/java/com/icegps/math/interpolation/Easing.kt @@ -0,0 +1,44 @@ +package com.icegps.math.interpolation + +@Suppress("unused") +fun interface Easing { + operator fun invoke(it: Float): Float + operator fun invoke(it: Double): Double = invoke(it.toFloat()).toDouble() + operator fun invoke(it: Ratio): Ratio = Ratio(invoke(it.toFloat()).toDouble()) + + companion object { + operator fun invoke(name: () -> String, block: (Float) -> Float): Easing { + return object : Easing { + override fun invoke(it: Float): Float = block(it) + override fun toString(): String = name() + } + } + + fun steps(steps: Int, easing: Easing): Easing = Easing({ "steps($steps, $easing)" }) { + easing((it * steps).toInt().toFloat() / steps) + } + fun cubic(f: (t: Float, b: Float, c: Float, d: Float) -> Float): Easing = Easing { f(it, 0f, 1f, 1f) } + fun combine(start: Easing, end: Easing): Easing = Easing { combine(it, start, end) } + inline fun combine(it: Float, start: Easing, end: Easing): Float = + if (it < .5f) .5f * start(it * 2f) else .5f * end((it - .5f) * 2f) + .5f + + val LINEAR = Easing { it } + val SMOOTH = Easing { it * it * (3 - 2 * it) } + } +} + + +interface Interpolable { + fun interpolateWith(ratio: Ratio, other: T): T +} + +interface MutableInterpolable { + fun setToInterpolated(ratio: Ratio, l: T, r: T): T +} + +fun Ratio.interpolate(l: Float, r: Float): Float = (l + (r - l) * this.toFloat()) +fun Ratio.interpolate(l: Double, r: Double): Double = (l + (r - l) * this.toDouble()) +fun Ratio.interpolate(l: Ratio, r: Ratio): Ratio = (l + (r - l) * this) +fun Ratio.interpolate(l: Int, r: Int): Int = (l + (r - l) * this.toDouble()).toInt() +fun Ratio.interpolate(l: Long, r: Long): Long = (l + (r - l) * this.toDouble()).toLong() +fun > Ratio.interpolate(l: T, r: T): T = l.interpolateWith(this, r) diff --git a/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt b/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt new file mode 100644 index 0000000..197c7a1 --- /dev/null +++ b/math/src/main/java/com/icegps/math/interpolation/Interpolation.vector.kt @@ -0,0 +1,8 @@ +package com.icegps.math.interpolation + +import com.icegps.math.geometry.Vector2D +import com.icegps.math.geometry.Vector2F + + +fun Ratio.interpolate(l: Vector2D, r: Vector2D): Vector2D = Vector2D(interpolate(l.x, r.x), interpolate(l.y, r.y)) +fun Ratio.interpolate(l: Vector2F, r: Vector2F): Vector2F = Vector2F(interpolate(l.x, r.x), interpolate(l.y, r.y)) diff --git a/math/src/main/java/com/icegps/math/interpolation/Ratio.kt b/math/src/main/java/com/icegps/math/interpolation/Ratio.kt new file mode 100644 index 0000000..c915d73 --- /dev/null +++ b/math/src/main/java/com/icegps/math/interpolation/Ratio.kt @@ -0,0 +1,109 @@ +package com.icegps.math.interpolation + +import com.icegps.math.* +import kotlin.math.* + +//inline class Ratio(val valueD: Double) : Comparable { +// constructor(ratio: Float) : this(ratio.toDouble()) +// val value: Double get() = valueD +// val valueF: Float get() = value.toFloat() +inline class Ratio(val value: Double) : Comparable { + constructor(ratio: Float) : this(ratio.toDouble()) + + fun toFloat(): Float = value.toFloat() + fun toDouble(): Double = value.toDouble() + + constructor(value: Int, maximum: Int) : this(value.toFloat() / maximum.toFloat()) + constructor(value: Float, maximum: Float) : this(value / maximum) + constructor(value: Double, maximum: Double) : this(value / maximum) + + operator fun unaryPlus(): Ratio = Ratio(+this.value) + operator fun unaryMinus(): Ratio = Ratio(-this.value) + operator fun plus(that: Ratio): Ratio = Ratio(this.value + that.value) + operator fun minus(that: Ratio): Ratio = Ratio(this.value - that.value) + + operator fun times(that: Ratio): Ratio = Ratio(this.value * that.value) + operator fun div(that: Ratio): Ratio = Ratio(this.value / that.value) + operator fun times(that: Double): Double = (this.value * that) + operator fun div(that: Double): Double = (this.value / that) + + val absoluteValue: Ratio get() = Ratio(value.absoluteValue) + val clamped: Ratio get() = Ratio(value.clamp01()) + + fun convertToRange(min: Float, max: Float): Float = this.toFloat().convertRange(0f, 1f, min, max) + fun convertToRange(min: Double, max: Double): Double = this.toDouble().convertRange(0.0, 1.0, min, max) + fun convertToRange(min: Ratio, max: Ratio): Ratio = Ratio(this.toDouble().convertRange(0.0, 1.0, min.toDouble(), max.toDouble())) + + override fun compareTo(other: Ratio): Int = value.compareTo(other.value) + + fun isNaN(): Boolean = value.isNaN() + + override fun toString(): String = "$value" + + companion object { + val ZERO = Ratio(0.0) + val QUARTER = Ratio(.25) + val HALF = Ratio(.5) + val ONE = Ratio(1.0) + val NaN = Ratio(Float.NaN) + + inline fun fromValueInRange(value: Number, min: Number, max: Number): Ratio = + value.toDouble().convertRange(min.toDouble(), max.toDouble(), 0.0, 1.0).toRatio() + + inline fun fromValueInRangeClamped(value: Number, min: Number, max: Number): Ratio = + value.toDouble().convertRangeClamped(min.toDouble(), max.toDouble(), 0.0, 1.0).toRatio() + + inline fun forEachRatio(steps: Int, include0: Boolean = true, include1: Boolean = true, block: (ratio: Ratio) -> Unit) { + val NS = steps - 1 + val NSd = NS.toDouble() + val start = if (include0) 0 else 1 + val end = if (include1) NS else NS - 1 + for (n in start..end) { + val ratio = n.toFloat() / NSd + block(ratio.toRatio()) + } + } + } +} + +inline operator fun Float.times(ratio: Ratio): Float = (this * ratio.value).toFloat() +inline operator fun Double.times(ratio: Ratio): Double = this * ratio.value +inline operator fun Int.times(ratio: Ratio): Double = this.toDouble() * ratio.value +inline operator fun Float.div(ratio: Ratio): Float = (this / ratio.value).toFloat() +inline operator fun Double.div(ratio: Ratio): Double = this / ratio.value +inline operator fun Int.div(ratio: Ratio): Double = this.toDouble() / ratio.value + +inline operator fun Ratio.times(value: Ratio): Ratio = Ratio(this.value * value.value) + +inline operator fun Ratio.times(value: Float): Float = (this.value * value).toFloat() +inline operator fun Ratio.times(value: Double): Double = this.value * value +inline operator fun Ratio.div(value: Float): Float = (this.value / value).toFloat() +inline operator fun Ratio.div(value: Double): Double = this.value / value + +@Deprecated("", ReplaceWith("this")) fun Ratio.toRatio(): Ratio = this + +inline fun Number.toRatio(): Ratio = Ratio(this.toDouble()) +fun Float.toRatio(): Ratio = Ratio(this) +fun Double.toRatio(): Ratio = Ratio(this) + +inline fun Number.toRatio(max: Number): Ratio = Ratio(this.toDouble(), max.toDouble()) +fun Float.toRatio(max: Float): Ratio = Ratio(this, max) +fun Double.toRatio(max: Double): Ratio = Ratio(this, max) + +fun Number.toRatioClamped(): Ratio = Ratio(this.toDouble().clamp01()) +fun Float.toRatioClamped(): Ratio = Ratio(this.clamp01()) +fun Double.toRatioClamped(): Ratio = Ratio(this.clamp01()) + +fun Ratio.convertRange(srcMin: Ratio, srcMax: Ratio, dstMin: Ratio, dstMax: Ratio): Ratio = Ratio(this.toDouble().convertRange(srcMin.toDouble(), srcMax.toDouble(), dstMin.toDouble(), dstMax.toDouble())) +fun Ratio.isAlmostEquals(that: Ratio, epsilon: Ratio = Ratio(0.000001)): Boolean = this.toDouble().isAlmostEquals(that.toDouble(), epsilon.toDouble()) +fun Ratio.isAlmostZero(epsilon: Ratio = Ratio(0.000001)): Boolean = this.isAlmostEquals(Ratio.ZERO, epsilon) +fun Ratio.roundDecimalPlaces(places: Int): Ratio = Ratio(value.roundDecimalPlaces(places)) + +fun abs(a: Ratio): Ratio = Ratio(a.value.absoluteValue) +fun min(a: Ratio, b: Ratio): Ratio = Ratio(kotlin.math.min(a.value, b.value)) +fun max(a: Ratio, b: Ratio): Ratio = Ratio(kotlin.math.max(a.value, b.value)) +fun Ratio.clamp(min: Ratio, max: Ratio): Ratio = when { + this < min -> min + this > max -> max + else -> this +} diff --git a/math/src/main/java/com/icegps/math/range/OpenRange.kt b/math/src/main/java/com/icegps/math/range/OpenRange.kt new file mode 100644 index 0000000..9f0c563 --- /dev/null +++ b/math/src/main/java/com/icegps/math/range/OpenRange.kt @@ -0,0 +1,8 @@ +package com.icegps.math.range + +class OpenRange>(val start: T, val endExclusive: T) + +// @TODO: Would cause conflicts with Int until Int for example +//infix fun > T.until(other: T) = OpenRange(this, other) + +operator fun > OpenRange.contains(item: T) = item >= this.start && item < this.endExclusive diff --git a/math/src/main/java/com/icegps/math/range/Ranges.kt b/math/src/main/java/com/icegps/math/range/Ranges.kt new file mode 100644 index 0000000..2edc6dd --- /dev/null +++ b/math/src/main/java/com/icegps/math/range/Ranges.kt @@ -0,0 +1,21 @@ +@file:Suppress("PackageDirectoryMismatch") + +package com.icegps.math.range + +data class DoubleRangeExclusive(val start: Double, val endExclusive: Double) { + val length: Double get() = endExclusive - start + operator fun contains(value: Double): Boolean = value >= start && value < endExclusive + override fun toString(): String = "${start.toString().removeSuffix(".0")} until ${endExclusive.toString().removeSuffix(".0")}" +} + +inline infix fun Double.until(endExclusive: Double): DoubleRangeExclusive = DoubleRangeExclusive(this, endExclusive) + +data class FloatInRange(val value: Float, val min: Float, val max: Float, val inclusive: Boolean = true) + +data class FloatRangeExclusive(val start: Float, val endExclusive: Float) { + val length: Float get() = endExclusive - start + operator fun contains(value: Double): Boolean = value >= start && value < endExclusive + override fun toString(): String = "${start.toString().removeSuffix(".0")} until ${endExclusive.toString().removeSuffix(".0")}" +} + +inline infix fun Float.until(endExclusive: Float): FloatRangeExclusive = FloatRangeExclusive(this, endExclusive) diff --git a/math/src/main/java/com/icegps/memory/Bits.kt b/math/src/main/java/com/icegps/memory/Bits.kt new file mode 100644 index 0000000..d965b1d --- /dev/null +++ b/math/src/main/java/com/icegps/memory/Bits.kt @@ -0,0 +1,349 @@ +package com.icegps.memory + +import com.icegps.math.* +import kotlin.rotateLeft as rotateLeftKotlin +import kotlin.rotateRight as rotateRightKotlin + +/** Returns the bits in memory of [this] float */ +public inline fun Float.reinterpretAsInt(): Int = this.toRawBits() +/** Returns the bits in memory of [this] float */ +public inline fun Double.reinterpretAsLong(): Long = this.toRawBits() + +/** Returns the float representation of [this] memory bits */ +public inline fun Int.reinterpretAsFloat(): Float = Float.fromBits(this) +/** Returns the float representation of [this] memory bits */ +public inline fun Long.reinterpretAsDouble(): Double = Double.fromBits(this) + +/** Rotates [this] [bits] bits to the left */ +public fun UInt.rotateLeft(bits: Int): UInt = this.rotateLeftKotlin(bits) +/** Rotates [this] [bits] bits to the left */ +public fun Int.rotateLeft(bits: Int): Int = this.rotateLeftKotlin(bits) +/** Rotates [this] [bits] bits to the left */ +public fun Long.rotateLeft(bits: Int): Long = this.rotateLeftKotlin(bits) + +/** Rotates [this] [bits] bits to the right */ +public fun UInt.rotateRight(bits: Int): UInt = this.rotateRightKotlin(bits) +/** Rotates [this] [bits] bits to the right */ +public fun Int.rotateRight(bits: Int): Int = this.rotateRightKotlin(bits) +/** Rotates [this] [bits] bits to the right */ +public fun Long.rotateRight(bits: Int): Long = this.rotateRightKotlin(bits) + +/** Reverses the bytes of [this] [Short]: AABB -> BBAA */ +public fun Short.reverseBytes(): Short { + val low = ((this.toInt() ushr 0) and 0xFF) + val high = ((this.toInt() ushr 8) and 0xFF) + return ((high and 0xFF) or (low shl 8)).toShort() +} + +/** Reverses the bytes of [this] [Char]: AABB -> BBAA */ +public fun Char.reverseBytes(): Char = this.code.toShort().reverseBytes().toInt().toChar() + +/** Reverses the bytes of [this] [Int]: AABBCCDD -> DDCCBBAA */ +public fun Int.reverseBytes(): Int { + val v0 = ((this ushr 0) and 0xFF) + val v1 = ((this ushr 8) and 0xFF) + val v2 = ((this ushr 16) and 0xFF) + val v3 = ((this ushr 24) and 0xFF) + return (v0 shl 24) or (v1 shl 16) or (v2 shl 8) or (v3 shl 0) +} + +/** Reverses the bytes of [this] [Long]: AABBCCDDEEFFGGHH -> HHGGFFEEDDCCBBAA */ +public fun Long.reverseBytes(): Long { + val v0 = (this ushr 0).toInt().reverseBytes().toLong() and 0xFFFFFFFFL + val v1 = (this ushr 32).toInt().reverseBytes().toLong() and 0xFFFFFFFFL + return (v0 shl 32) or (v1 shl 0) +} + +/** Reverse the bits of [this] Int: abcdef...z -> z...fedcba */ +public fun Int.reverseBits(): Int { + var v = this + v = ((v ushr 1) and 0x55555555) or ((v and 0x55555555) shl 1) // swap odd and even bits + v = ((v ushr 2) and 0x33333333) or ((v and 0x33333333) shl 2) // swap consecutive pairs + v = ((v ushr 4) and 0x0F0F0F0F) or ((v and 0x0F0F0F0F) shl 4) // swap nibbles ... + v = ((v ushr 8) and 0x00FF00FF) or ((v and 0x00FF00FF) shl 8) // swap bytes + v = ((v ushr 16) and 0x0000FFFF) or ((v and 0x0000FFFF) shl 16) // swap 2-byte long pairs + return v +} + +/** Returns the number of leading zeros of the bits of [this] integer */ +public inline fun Int.countLeadingZeros(): Int = this.countLeadingZeroBits() + +/** Returns the number of trailing zeros of the bits of [this] integer */ +public fun Int.countTrailingZeros(): Int = this.countTrailingZeroBits() + +/** Returns the number of leading ones of the bits of [this] integer */ +public fun Int.countLeadingOnes(): Int = this.inv().countLeadingZeros() + +/** Returns the number of trailing ones of the bits of [this] integer */ +public fun Int.countTrailingOnes(): Int = this.inv().countTrailingZeros() + +/** Takes n[bits] of [this] [Int], and extends the last bit, creating a plain [Int] in one's complement */ +public fun Int.signExtend(bits: Int): Int = (this shl (32 - bits)) shr (32 - bits) // Int.SIZE_BITS +/** Takes n[bits] of [this] [Long], and extends the last bit, creating a plain [Long] in one's complement */ +public fun Long.signExtend(bits: Int): Long = (this shl (64 - bits)) shr (64 - bits) // Long.SIZE_BITS + +/** Creates an [Int] with [this] bits set to 1 */ +public fun Int.mask(): Int = (1 shl this) - 1 +/** Creates a [Long] with [this] bits set to 1 */ +public fun Long.mask(): Long = (1L shl this.toInt()) - 1L + +/** Creates an [Int] with [this] bits set to 1, displaced [offset] bits */ +public fun Int.mask(offset: Int): Int = mask() shl offset +/** Creates a [Long] with [this] bits set to 1, displaced [offset] bits */ +public fun Long.mask(offset: Int): Long = mask() shl offset + +inline class IntMaskRange private constructor(val raw: Int) { + val offset: Int get() = raw.extract8(0) + val size: Int get() = raw.extract8(8) + fun toMask(): Int = size.mask(offset) + + override fun toString(): String = "IntMaskRange(offset=$offset, size=$size)" + + fun extract(value: Int): Int = value.extract(offset, size) + fun extractSigned(value: Int, signed: Boolean = true): Int = value.extractSigned(offset, size, signed) + + companion object { + fun fromRange(offset: Int, size: Int): IntMaskRange = IntMaskRange(0.insert8(offset, 0).insert8(size, 8)) + fun fromMask(mask: Int): IntMaskRange { + if (mask == 0) return IntMaskRange(0) + val offset = mask.countTrailingZeroBits() + val size = (32 - mask.countLeadingZeroBits()) - offset + return fromRange(offset, size) + } + } + operator fun component1(): Int = offset + operator fun component2(): Int = size +} + +fun Int.extractMaskRange(): IntMaskRange = IntMaskRange.fromMask(this) + +//fun Int.getBit(offset: Int): Boolean = ((this ushr offset) and 1) != 0 +//fun Int.getBits(offset: Int, count: Int): Int = (this ushr offset) and count.mask() + +/** Extracts [count] bits at [offset] from [this] [Int] */ +public fun Int.extract(offset: Int, count: Int): Int = (this ushr offset) and count.mask() +/** Extracts a bits at [offset] from [this] [Int] (returning a [Boolean]) */ +inline fun Int.extract(offset: Int): Boolean = extract1(offset) != 0 +/** Extracts a bits at [offset] from [this] [Int] (returning a [Boolean]) */ +inline fun Int.extractBool(offset: Int): Boolean = extract1(offset) != 0 +/** Extracts 1 bit at [offset] from [this] [Int] */ +inline fun Int.extract1(offset: Int): Int = (this ushr offset) and 0b1 +/** Extracts 2 bits at [offset] from [this] [Int] */ +inline fun Int.extract2(offset: Int): Int = (this ushr offset) and 0b11 +/** Extracts 3 bits at [offset] from [this] [Int] */ +inline fun Int.extract3(offset: Int): Int = (this ushr offset) and 0b111 +/** Extracts 4 bits at [offset] from [this] [Int] */ +inline fun Int.extract4(offset: Int): Int = (this ushr offset) and 0b1111 +/** Extracts 5 bits at [offset] from [this] [Int] */ +inline fun Int.extract5(offset: Int): Int = (this ushr offset) and 0b11111 +/** Extracts 6 bits at [offset] from [this] [Int] */ +inline fun Int.extract6(offset: Int): Int = (this ushr offset) and 0b111111 +/** Extracts 7 bits at [offset] from [this] [Int] */ +inline fun Int.extract7(offset: Int): Int = (this ushr offset) and 0b1111111 +/** Extracts 8 bits at [offset] from [this] [Int] */ +inline fun Int.extract8(offset: Int): Int = (this ushr offset) and 0b11111111 +/** Extracts 9 bits at [offset] from [this] [Int] */ +inline fun Int.extract9(offset: Int): Int = (this ushr offset) and 0b111111111 +/** Extracts 10 bits at [offset] from [this] [Int] */ +inline fun Int.extract10(offset: Int): Int = (this ushr offset) and 0b1111111111 +/** Extracts 11 bits at [offset] from [this] [Int] */ +inline fun Int.extract11(offset: Int): Int = (this ushr offset) and 0b11111111111 +/** Extracts 12 bits at [offset] from [this] [Int] */ +inline fun Int.extract12(offset: Int): Int = (this ushr offset) and 0b111111111111 +/** Extracts 13 bits at [offset] from [this] [Int] */ +inline fun Int.extract13(offset: Int): Int = (this ushr offset) and 0b1111111111111 +/** Extracts 14 bits at [offset] from [this] [Int] */ +inline fun Int.extract14(offset: Int): Int = (this ushr offset) and 0b11111111111111 +/** Extracts 15 bits at [offset] from [this] [Int] */ +inline fun Int.extract15(offset: Int): Int = (this ushr offset) and 0b111111111111111 +/** Extracts 16 bits at [offset] from [this] [Int] */ +inline fun Int.extract16(offset: Int): Int = (this ushr offset) and 0b1111111111111111 +/** Extracts 17 bits at [offset] from [this] [Int] */ +inline fun Int.extract17(offset: Int): Int = (this ushr offset) and 0b11111111111111111 +/** Extracts 18 bits at [offset] from [this] [Int] */ +inline fun Int.extract18(offset: Int): Int = (this ushr offset) and 0b111111111111111111 +/** Extracts 19 bits at [offset] from [this] [Int] */ +inline fun Int.extract19(offset: Int): Int = (this ushr offset) and 0b1111111111111111111 +/** Extracts 20 bits at [offset] from [this] [Int] */ +inline fun Int.extract20(offset: Int): Int = (this ushr offset) and 0b11111111111111111111 +/** Extracts 21 bits at [offset] from [this] [Int] */ +inline fun Int.extract21(offset: Int): Int = (this ushr offset) and 0b111111111111111111111 +/** Extracts 22 bits at [offset] from [this] [Int] */ +inline fun Int.extract22(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111 +/** Extracts 23 bits at [offset] from [this] [Int] */ +inline fun Int.extract23(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111 +/** Extracts 24 bits at [offset] from [this] [Int] */ +inline fun Int.extract24(offset: Int): Int = (this ushr offset) and 0xFFFFFF +/** Extracts 25 bits at [offset] from [this] [Int] */ +inline fun Int.extract25(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111 +/** Extracts 26 bits at [offset] from [this] [Int] */ +inline fun Int.extract26(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111111 +/** Extracts 27 bits at [offset] from [this] [Int] */ +inline fun Int.extract27(offset: Int): Int = (this ushr offset) and 0b111111111111111111111111111 +/** Extracts 28 bits at [offset] from [this] [Int] */ +inline fun Int.extract28(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111111 +/** Extracts 29 bits at [offset] from [this] [Int] */ +inline fun Int.extract29(offset: Int): Int = (this ushr offset) and 0b11111111111111111111111111111 +/** Extracts 30 bits at [offset] from [this] [Int] */ +inline fun Int.extract30(offset: Int): Int = (this ushr offset) and 0b111111111111111111111111111111 +/** Extracts 31 bits at [offset] from [this] [Int] */ +inline fun Int.extract31(offset: Int): Int = (this ushr offset) and 0b1111111111111111111111111111111 +/** Extracts 32 bits at [offset] from [this] [Int] */ +inline fun Int.extract32(offset: Int): Int = (this ushr offset) and -1 + + +/** Extracts [count] bits at [offset] from [this] [Int] sign-extending its result if [signed] is set to true */ +public fun Int.extractSigned(offset: Int, count: Int, signed: Boolean): Int = if (signed) extractSigned(offset, count) else extract(offset, count) + +/** Extracts [count] bits at [offset] from [this] [Int] sign-extending its result */ +public fun Int.extractSigned(offset: Int, count: Int): Int = ((this ushr offset) and count.mask()).signExtend(count) +/** Extracts 8 bits at [offset] from [this] [Int] sign-extending its result */ +public fun Int.extract8Signed(offset: Int): Int = (this ushr offset).toByte().toInt() +/** Extracts 16 bits at [offset] from [this] [Int] sign-extending its result */ +public fun Int.extract16Signed(offset: Int): Int = (this ushr offset).toShort().toInt() + +/** Extracts 8 bits at [offset] from [this] [Int] as [Byte] */ +public fun Int.extractByte(offset: Int): Byte = (this ushr offset).toByte() +/** Extracts 16 bits at [offset] from [this] [Int] as [Short] */ +public fun Int.extractShort(offset: Int): Short = (this ushr offset).toShort() + +/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..[scale] */ +public fun Int.extractScaled(offset: Int, count: Int, scale: Int): Int = (extract(offset, count) * scale) / count.mask() +/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0.0..1.0 */ +public fun Int.extractScaledf01(offset: Int, count: Int): Float = extract(offset, count).toFloat() / count.mask().toFloat() + +/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..0xFF */ +public fun Int.extractScaledFF(offset: Int, count: Int): Int = extractScaled(offset, count, 0xFF) +/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0x00..0xFF (if there are 0 bits, returns [default]) */ +public fun Int.extractScaledFFDefault(offset: Int, count: Int, default: Int): Int = + if (count == 0) default else extractScaled(offset, count, 0xFF) + +/** Replaces [this] bits from [offset] to [offset]+[count] with [value] and returns the result of doing such replacement */ +public fun Int.insert(value: Int, offset: Int, count: Int): Int { + val mask = count.mask() shl offset + val ovalue = (value shl offset) and mask + return (this and mask.inv()) or ovalue +} + +public fun Int.insertNoClear(value: Int, offset: Int, count: Int): Int { + return this or ((value and count.mask()) shl offset) +} + +public fun Int.clear(offset: Int, count: Int): Int { + return (this and (count.mask() shl offset).inv()) +} + +public fun Int.insert1(value: Int, offset: Int): Int = insertMask(value, offset, 0b1) +public fun Int.insert2(value: Int, offset: Int): Int = insertMask(value, offset, 0b11) +public fun Int.insert3(value: Int, offset: Int): Int = insertMask(value, offset, 0b111) +public fun Int.insert4(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111) +public fun Int.insert5(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111) +public fun Int.insert6(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111) +public fun Int.insert7(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111) +public fun Int.insert8(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111) +public fun Int.insert9(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111) +public fun Int.insert10(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111) +public fun Int.insert11(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111) +public fun Int.insert12(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111) +public fun Int.insert13(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111) +public fun Int.insert14(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111) +public fun Int.insert15(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111) +public fun Int.insert16(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111) +public fun Int.insert17(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111) +public fun Int.insert18(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111) +public fun Int.insert19(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111) +public fun Int.insert20(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111) +public fun Int.insert21(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111) +public fun Int.insert22(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111) +public fun Int.insert23(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111) +public fun Int.insert24(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111) +public fun Int.insert25(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111) +public fun Int.insert26(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111111) +public fun Int.insert27(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111111) +public fun Int.insert28(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111111) +public fun Int.insert29(value: Int, offset: Int): Int = insertMask(value, offset, 0b11111111111111111111111111111) +public fun Int.insert30(value: Int, offset: Int): Int = insertMask(value, offset, 0b111111111111111111111111111111) +public fun Int.insert31(value: Int, offset: Int): Int = insertMask(value, offset, 0b1111111111111111111111111111111) +public fun Int.insert32(value: Int, offset: Int): Int = insertMask(value, offset, -1) + +/** Fast Insert: do not clear bits, assume affecting bits are 0 */ +public fun Int.finsert(value: Int, offset: Int): Int = this or (value shl offset) +public fun Int.finsert24(value: Int, offset: Int): Int = this or ((value and 0xFFFFFF) shl offset) +public fun Int.finsert16(value: Int, offset: Int): Int = this or ((value and 0xFFFF) shl offset) +public fun Int.finsert12(value: Int, offset: Int): Int = this or ((value and 0xFFF) shl offset) +public fun Int.finsert8(value: Int, offset: Int): Int = this or ((value and 0xFF) shl offset) +public fun Int.finsert7(value: Int, offset: Int): Int = this or ((value and 0b1111111) shl offset) +public fun Int.finsert6(value: Int, offset: Int): Int = this or ((value and 0b111111) shl offset) +public fun Int.finsert5(value: Int, offset: Int): Int = this or ((value and 0b11111) shl offset) +public fun Int.finsert4(value: Int, offset: Int): Int = this or ((value and 0b1111) shl offset) +public fun Int.finsert3(value: Int, offset: Int): Int = this or ((value and 0b111) shl offset) +public fun Int.finsert2(value: Int, offset: Int): Int = this or ((value and 0b11) shl offset) +public fun Int.finsert1(value: Int, offset: Int): Int = this or ((value and 0b1) shl offset) +public fun Int.finsert(value: Boolean, offset: Int): Int = finsert(value.toInt(), offset) + +inline fun Int.insertMask(value: Int, offset: Int, mask: Int): Int { + return (this and (mask shl offset).inv()) or ((value and mask) shl offset) +} +/** Replaces 1 bit at [offset] with [value] and returns the result of doing such replacement */ +public fun Int.insert(value: Boolean, offset: Int): Int { + val bits = (1 shl offset) + return if (value) this or bits else this and bits.inv() +} + +public fun Int.insertScaled(value: Int, offset: Int, count: Int, scale: Int): Int = insert((value * count.mask()) / scale, offset, count) +public fun Int.insertScaledFF(value: Int, offset: Int, count: Int): Int = if (count == 0) this else this.insertScaled(value, offset, count, 0xFF) +/** Extracts [count] at [offset] from [this] [Int] and convert the possible values into the range 0.0..1.0 */ +public fun Int.insertScaledf01(value: Float, offset: Int, count: Int): Int = this.insert((value.coerceIn(0f, 1f) * offset.mask()).toInt(), offset, count) + + +/** Check if [this] has all the bits set in [bits] set */ +public infix fun Int.hasFlags(bits: Int): Boolean = (this and bits) == bits +public infix fun Int.hasBits(bits: Int): Boolean = (this and bits) == bits + +/** Check if a specific bit at [index] is set */ +public infix fun Int.hasBitSet(index: Int): Boolean = ((this ushr index) and 1) != 0 + +public infix fun Long.hasFlags(bits: Long): Boolean = (this and bits) == bits +public infix fun Long.hasBits(bits: Long): Boolean = (this and bits) == bits + +/** Creates an integer with only bit [bit] set */ +public fun bit(bit: Int): Int = 1 shl bit + +/** Returns the integer [this] without the [bits] set */ +public fun Int.unsetBits(bits: Int): Int = this and bits.inv() + +/** Returns the integer [this] with the [bits] set */ +public fun Int.setBits(bits: Int): Int = this or bits + +/** Returns the integer [this] with the [bits] set or unset depending on the [set] parameter */ +public fun Int.setBits(bits: Int, set: Boolean): Int = if (set) setBits(bits) else unsetBits(bits) + +public fun Int.without(bits: Int): Int = this and bits.inv() +public fun Int.with(bits: Int): Int = this or bits + +public fun Long.without(bits: Long): Long = this and bits.inv() +public fun Long.with(bits: Long): Long = this or bits + +/** Get high 32-bits of this Long */ +val Long.high: Int get() = (this ushr 32).toInt() +/** Get low 32-bits of this Long */ +val Long.low: Int get() = this.toInt() + +/** Get high 32-bits of this Long */ +val Long._high: Int get() = (this ushr 32).toInt() +/** Get low 32-bits of this Long */ +val Long._low: Int get() = this.toInt() + +inline fun Long.Companion.fromLowHigh(low: Int, high: Int): Long = (low.toLong() and 0xFFFFFFFFL) or (high.toLong() shl 32) + +inline fun Int.fastForEachOneBits(block: (Int) -> Unit) { + var value = this + var index = 0 + while (value != 0) { + val shift = value.countTrailingZeroBits() + index += shift + if (index < 32) block(index) + value = value ushr (shift + 1) + index++ + } +} diff --git a/math/src/main/java/com/icegps/memory/DoubleBits.kt b/math/src/main/java/com/icegps/memory/DoubleBits.kt new file mode 100644 index 0000000..87a800a --- /dev/null +++ b/math/src/main/java/com/icegps/memory/DoubleBits.kt @@ -0,0 +1,47 @@ +package com.icegps.memory + +// S | EEEEEEEEEEE | FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF +// S=1 +// E=11 +// F=52 + +fun Double.toStringInfo() = buildString(128) { + append(this@toStringInfo) + append(" = Double.fromParts(") + append("sign=") + append(this@toStringInfo.bitsSign) + append(", exponent=0b") + append(this@toStringInfo.bitsExponent.toString(2).padStart(11, '0')) + append(", mantissa=0b") + //append(this@toStringInfo.bitsMantissaLong.toString(2).padStart(52, '0')) + append(this@toStringInfo.bitsMantissaHigh.toString(2).padStart(20, '0')) + append(this@toStringInfo.bitsMantissaLow.toString(2).padStart(32, '0')) + append(")") +} + +private const val TWO_POW_32_DOUBLE = 4294967296.0 +val Double.Companion.TWO_POW_32 get() = TWO_POW_32_DOUBLE + +fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissa: Double): Double = fromParts(sign, exponent, (mantissa % TWO_POW_32_DOUBLE).toInt(), (mantissa / TWO_POW_32_DOUBLE).toInt()) +fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissa: Long): Double = fromParts(sign, exponent, mantissa.low, mantissa.high) +fun Double.Companion.fromParts(sign: Int, exponent: Int, mantissaLow: Int, mantissaHigh: Int): Double = fromLowHigh(mantissaLow, mantissaHigh.insert12(exponent, 20).insert1(sign, 31)) + +fun Double.Companion.fromLowHigh(low: Int, high: Int): Double = fromLowHighBitsSlow(low, high) +inline fun Double.getLowHighBits(block: (low: Int, high: Int) -> T): T = getLowHighBitsSlow(block) +/** Bit-wise equals without considering NaNs */ +fun Double.equalsRaw(other: Double): Boolean = equalsRawSlow(other) +val Double.lowBits: Int get() = lowSlow +val Double.highBits: Int get() = highSlow + +val Double.bitsSign: Int get() = highBits.extract1(31) +val Double.bitsExponent: Int get() = highBits.extract11(20) +val Double.bitsMantissaHigh: Int get() = highBits.extract20(0) +val Double.bitsMantissaLow: Int get() = lowBits +val Double.bitsMantissaDouble: Double get() = bitsMantissaLow.toDouble() + bitsMantissaHigh.toDouble() * TWO_POW_32_DOUBLE +val Double.bitsMantissaLong: Long get() = Long.fromLowHigh(bitsMantissaLow, bitsMantissaHigh) + +@PublishedApi internal fun Double.Companion.fromLowHighBitsSlow(low: Int, high: Int): Double = Double.fromBits(Long.fromLowHigh(low, high)) +@PublishedApi internal inline fun Double.getLowHighBitsSlow(block: (low: Int, high: Int) -> T): T = block(lowSlow, highSlow) +@PublishedApi internal inline fun Double.equalsRawSlow(other: Double): Boolean = this.reinterpretAsLong().equals(other.reinterpretAsLong()) +@PublishedApi internal val Double.lowSlow: Int get() = this.reinterpretAsLong().low +@PublishedApi internal val Double.highSlow: Int get() = this.reinterpretAsLong().high diff --git a/math/src/main/java/com/icegps/memory/Int64.kt b/math/src/main/java/com/icegps/memory/Int64.kt new file mode 100644 index 0000000..27f7930 --- /dev/null +++ b/math/src/main/java/com/icegps/memory/Int64.kt @@ -0,0 +1,194 @@ +package com.icegps.memory + +import kotlin.contracts.* + +inline class Int64Array(val raw: DoubleArray) : Iterable { + inline val indices: IntRange get() = raw.indices + + constructor(size: Int, value: Int64 = Int64.ZERO) : this(DoubleArray(size) { value.raw }) + companion object { + inline operator fun invoke(size: Int, gen: (Int) -> Int64): Int64Array = Int64Array(DoubleArray(size) { gen(it).raw }) + } + + inline val size: Int get() = raw.size + inline operator fun get(index: Int): Int64 = Int64.fromRaw(raw[index]) + inline operator fun set(index: Int, value: Int64) { raw[index] = value.raw } + override fun iterator(): Iterator = object : Iterator { + var index = 0 + override fun hasNext(): Boolean = index < raw.size + override fun next(): Int64 = this@Int64Array[index].also { index++ } + } + + override fun toString(): String = "IntArray64($size)" +} + +inline fun int64ArrayOf(vararg values: T): Int64Array = Int64Array(values.size) { values[it] } +inline fun int64ArrayOf(vararg values: Int): Int64Array = Int64Array(values.size) { values[it].toInt64() } +inline fun int64ArrayOf(vararg values: Long): Int64Array = Int64Array(values.size) { values[it].toInt64() } + +fun Int64Array.copyOf(newSize: Int = this.size): Int64Array = Int64Array(raw.copyOf(newSize)) +fun Int64Array.copyOfRange(fromIndex: Int, toIndex: Int): Int64Array = Int64Array(raw.copyOfRange(fromIndex, toIndex)) +public fun Int64Array.getOrNull(index: Int): Int64? = if (index in indices) get(index) else null +//@kotlin.internal.InlineOnly +@OptIn(ExperimentalContracts::class) +public inline fun Int64Array.getOrElse(index: Int, defaultValue: (Int) -> Int64): Int64 { + contract { callsInPlace(defaultValue, InvocationKind.AT_MOST_ONCE) } + return if (index in indices) get(index) else defaultValue(index) +} + +infix fun Int64Array?.contentEquals(other: Int64Array?): Boolean = this?.raw.contentEquals(other?.raw) +fun Int64Array?.contentHashCode(): Int = this?.raw.contentHashCode() +fun Int64Array?.contentToString(): String = if (this == null) "null" else "[" + this.raw.joinToString(", ") { it.toString() } + "]" + +/** + * Allocation-less Long implementation that uses a Double with reinterpreted values + * + * IMPORTANT: + * + * Due to Kotlin not supporting [equals] in inline classes, + * Equality fails in some cases where Int64 represents a NaN or an Infinity. + * For comparing Int64, use [Int64.equalsSafe] instead. + */ +inline class Int64(val raw: Double) : Comparable { + companion object { + val ZERO = Int64(0, 0) + + fun equals(a: Int64, b: Int64): Boolean = a.raw.equalsRaw(b.raw) + + inline operator fun invoke(value: Long): Int64 = Int64(value.reinterpretAsDouble()) + inline operator fun invoke(low: Int, high: Int): Int64 = Int64(Double.fromLowHigh(low, high)) + inline operator fun invoke(value: Int64): Int64 = Int64(value.raw) + inline operator fun invoke(value: UInt): Int64 = Int64(Double.fromLowHigh(value.toInt(), 0)) + inline operator fun invoke(value: Int): Int64 = when { + value < 0 -> Int64(Double.fromLowHigh(value and (1 shl 31), 1 shl 31)) + else -> Int64(Double.fromLowHigh(value, 0)) + } + + inline fun fromRaw(value: Double) = Int64(value) + inline fun fromInt52(values: Double) = Int64(Double.fromParts(0, 0, values)) + + fun add(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 { + val low = low1 + low2 + val carry = if (low < low1) 1 else 0 + val high = high1 + high2 + carry + return Int64(low.toInt(), high) + } + fun sub(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 { + val lowDiff = low1 - low2 + val borrow = if (low1 < low2) 1 else 0 + val highDiff = high1 - high2 - borrow + return Int64(lowDiff.toInt(), highDiff) + } + // @TODO: Fix this + fun imul(low1: UInt, high1: Int, low2: UInt, high2: Int): Int64 { + if (low1 == 0u && high1 == 0) return Int64.ZERO + if (low2 == 0u && high2 == 0) return Int64.ZERO + + /* + if (equalsLong(_this__u8e3s4, get_MIN_VALUE())) { + return if (isOdd(other)) get_MIN_VALUE() else get_ZERO() + } else if (equalsLong(other, get_MIN_VALUE())) { + return if (isOdd(_this__u8e3s4)) get_MIN_VALUE() else get_ZERO() + } + if (isNegative(_this__u8e3s4)) { + val tmp: Unit + if (isNegative(other)) { + tmp = multiply(negate(_this__u8e3s4), negate(other)) + } else { + tmp = negate(multiply(negate(_this__u8e3s4), other)) + } + return tmp + } else if (isNegative(other)) { + return negate(multiply(_this__u8e3s4, negate(other))) + } + if (lessThan(_this__u8e3s4, get_TWO_PWR_24_()) && lessThan(other, get_TWO_PWR_24_())) { + return fromNumber(toNumber(_this__u8e3s4) * toNumber(other)) + } + val a48: Unit = _this__u8e3s4.high_1 ushr 16 or 0 + val a32: Unit = _this__u8e3s4.high_1 and 65535 + val a16: Unit = _this__u8e3s4.low_1 ushr 16 or 0 + val a00: Unit = _this__u8e3s4.low_1 and 65535 + val b48: Unit = other.high_1 ushr 16 or 0 + val b32: Unit = other.high_1 and 65535 + val b16: Unit = other.low_1 ushr 16 or 0 + val b00: Unit = other.low_1 and 65535 + var c48 = 0 + var c32 = 0 + var c16 = 0 + var c00 = 0 + c00 = c00 + imul(a00, b00) or 0 + c16 = c16 + (c00 ushr 16 or 0) or 0 + c00 = c00 and 65535 + c16 = c16 + imul(a16, b00) or 0 + c32 = c32 + (c16 ushr 16 or 0) or 0 + c16 = c16 and 65535 + c16 = c16 + imul(a00, b16) or 0 + c32 = c32 + (c16 ushr 16 or 0) or 0 + c16 = c16 and 65535 + c32 = c32 + imul(a32, b00) or 0 + c48 = c48 + (c32 ushr 16 or 0) or 0 + c32 = c32 and 65535 + c32 = c32 + imul(a16, b16) or 0 + c48 = c48 + (c32 ushr 16 or 0) or 0 + c32 = c32 and 65535 + c32 = c32 + imul(a00, b32) or 0 + c48 = c48 + (c32 ushr 16 or 0) or 0 + c32 = c32 and 65535 + c48 = c48 + (((imul(a48, b00) + imul(a32, b16) or 0) + imul(a16, b32) or 0) + imul(a00, b48) or 0) or 0 + c48 = c48 and 65535 + return Long(c16 shl 16 or c00, c48 shl 16 or c32) + */ + TODO() + } + } + + inline val isNegative get() = high.extract1(31) != 0 + inline val isPositive get() = !isNegative + inline val isZero get() = low == 0 && high == 0 + + operator fun unaryPlus(): Int64 = this + operator fun unaryMinus(): Int64 = Int64(low, -high) + fun inv(): Int64 = Int64(low.inv(), high.inv()) + + operator fun plus(other: Int64): Int64 = add(ulow, high, other.ulow, other.high) + operator fun minus(other: Int64): Int64 = sub(ulow, high, other.ulow, other.high) + infix fun xor(other: Int64): Int64 = Int64(low xor other.low, high xor other.high) + infix fun and(other: Int64): Int64 = Int64(low and other.low, high and other.high) + infix fun or(other: Int64): Int64 = Int64(low or other.low, high or other.high) + + //infix fun shl(other: Int): Int64 = Int64(low shl other, high shl other) // @TODO: Fix this + //infix fun shr(other: Int): Int64 = Int64(low shr other, high shr other) // @TODO: Fix this + //infix fun ushr(other: Int): Int64 = Int64(low ushr other, high ushr other) // @TODO: Fix this + + // @TODO: SLOW (USE INTERMEDIARY LONGS) + infix fun shl(other: Int): Int64 = Int64(toLong() shl other) + infix fun shr(other: Int): Int64 = Int64(toLong() shr other) + infix fun ushr(other: Int): Int64 = Int64(toLong() ushr other) + operator fun times(other: Int64): Int64 { + if (this.isZero || other.isZero) return Int64.ZERO + return Int64(toLong() * other.toLong()) + } + //operator fun times(other: Int64): Int64 = imul(ulow, high, other.ulow, other.high) // @TODO: Fix this + operator fun div(other: Int64): Int64 = Int64(toLong() / other.toLong()) + operator fun rem(other: Int64): Int64 = Int64(toLong() % other.toLong()) + override fun compareTo(other: Int64): Int = this.toLong().compareTo(other.toLong()) + // @TODO /END SLOW (USE INTERMEDIARY LONGS) + + //val int52: Double get() = raw.bitsMantissaDouble + inline val ulow: UInt get() = raw.lowBits.toUInt() + inline val low: Int get() = raw.lowBits + inline val high: Int get() = raw.highBits + + fun equalsSafe(other: Int64): Boolean = equals(this, other) + + fun toInt(): Int = if (isPositive) low and 0x7FFFFFFF else -(low and 0x7FFFFFFF) + inline fun toLong(): Long = raw.reinterpretAsLong() + + override fun toString(): String = "${toLong()}" +} + +fun Byte.toInt64(): Int64 = Int64(this.toInt()) +fun Int.toInt64(): Int64 = Int64(this) +fun Long.toInt64(): Int64 = Int64(this) +fun Double.toInt64(): Int64 = Int64.fromInt52(this) +fun Number.toInt64(): Int64 = Int64(this.toLong()) diff --git a/math/src/main/java/com/icegps/number/StringExt.kt b/math/src/main/java/com/icegps/number/StringExt.kt new file mode 100644 index 0000000..f04a64e --- /dev/null +++ b/math/src/main/java/com/icegps/number/StringExt.kt @@ -0,0 +1,71 @@ +package com.icegps.number + +import com.icegps.math.* +import kotlin.math.* + +val Double.niceStr: String get() = niceStr(-1, zeroSuffix = false) +fun Double.niceStr(decimalPlaces: Int, zeroSuffix: Boolean = false): String = buildString { appendNice(this@niceStr.roundDecimalPlaces(decimalPlaces), zeroSuffix = zeroSuffix && decimalPlaces > 0) } + +val Float.niceStr: String get() = niceStr(-1, zeroSuffix = false) +fun Float.niceStr(decimalPlaces: Int, zeroSuffix: Boolean = false): String = buildString { appendNice(this@niceStr.roundDecimalPlaces(decimalPlaces), zeroSuffix = zeroSuffix && decimalPlaces > 0) } + +fun StringBuilder.appendNice(value: Double, zeroSuffix: Boolean = false): Unit { + when { + round(value).isAlmostEquals(value) -> when { + value >= Int.MIN_VALUE.toDouble() && value <= Int.MAX_VALUE.toDouble() -> append(round(value).toInt()) + else -> append(round(value).toLong()) + } + else -> { + append(value) + return + } + } + if (zeroSuffix) append(".0") +} +fun StringBuilder.appendNice(value: Float, zeroSuffix: Boolean = false): Unit { + when { + round(value).isAlmostEquals(value) -> when { + value >= Int.MIN_VALUE.toFloat() && value <= Int.MAX_VALUE.toFloat() -> append(value.toInt()) + else -> append(value.toLong()) + } + else -> { + append(value) + return + } + } + if (zeroSuffix) append(".0") +} +fun StringBuilder.appendGenericArray(size: Int, appendElement: StringBuilder.(Int) -> Unit) { + append("[") + for (n in 0 until size) { + if (n != 0) append(", ") + appendElement(n) + } + append("]") +} + +//val Float.niceStr: String get() = buildString { appendNice(this@niceStr) } +//fun Float.niceStr(decimalPlaces: Int): String = roundDecimalPlaces(decimalPlaces).niceStr +//val Float.niceStr: String get() = buildString { appendNice(this@niceStr) } +//fun Float.niceStr(decimalPlaces: Int): String = roundDecimalPlaces(decimalPlaces).niceStr + +/* +internal fun StringBuilder.appendNice(value: Double) { + when { + round(value).isAlmostEquals(value) -> when { + value >= Int.MIN_VALUE.toDouble() && value <= Int.MAX_VALUE.toDouble() -> append(value.toInt()) + else -> append(value.toLong()) + } + else -> append(value) + } +} +internal fun StringBuilder.appendNice(value: Float) { + when { + round(value).isAlmostEquals(value) -> when { + value >= Int.MIN_VALUE.toFloat() && value <= Int.MAX_VALUE.toFloat() -> append(value.toInt()) + else -> append(value.toLong()) + } + else -> append(value) + } +} +*/ diff --git a/math/src/test/java/com/icegps/math/geometry/AngleTest.kt b/math/src/test/java/com/icegps/math/geometry/AngleTest.kt new file mode 100644 index 0000000..4cc9d21 --- /dev/null +++ b/math/src/test/java/com/icegps/math/geometry/AngleTest.kt @@ -0,0 +1,22 @@ +package com.icegps.math.geometry + +import kotlin.test.Test + +/** + * @author tabidachinokaze + * @date 2025/10/28 + */ +class AngleTest { + @Test + fun testAngle() { + val angle = 90.degrees + println(angle) + println(angle.degrees) + + val angle1 = 1.9.radians + println(angle1) + println(angle1.radians) + + println(angle1.degrees) + } +} \ No newline at end of file diff --git a/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt b/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt new file mode 100644 index 0000000..ecc25ca --- /dev/null +++ b/math/src/test/java/com/icegps/math/geometry/EulerRotationTest.kt @@ -0,0 +1,15 @@ +package com.icegps.math.geometry + +import kotlin.test.Test + +/** + * @author tabidachinokaze + * @date 2025/10/19 + */ +class EulerRotationTest { + @Test + fun testEulerRotation() { + val eulerRotation = EulerRotation(12.degrees, 12.degrees, 12.degrees) + println(eulerRotation) + } +} \ No newline at end of file diff --git a/math/src/test/java/com/icegps/number/NiceStrTest.kt b/math/src/test/java/com/icegps/number/NiceStrTest.kt new file mode 100644 index 0000000..cba9dc0 --- /dev/null +++ b/math/src/test/java/com/icegps/number/NiceStrTest.kt @@ -0,0 +1,15 @@ +package com.icegps.number + +import com.icegps.math.geometry.degrees +import kotlin.test.Test + +/** + * @author tabidachinokaze + * @date 2025/10/19 + */ +class NiceStrTest { + @Test + fun testNiceStr() { + println((12.0 / 12.1).degrees.degrees.niceStr(2)) + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..661b9c2 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,29 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex("com\\.android.*") + includeGroupByRegex("com\\.google.*") + includeGroupByRegex("androidx.*") + } + } + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + // Mapbox Maven repository + maven { + url = uri("https://api.mapbox.com/downloads/v2/releases/maven") + } + } +} + +rootProject.name = "geotools" +include ':app' +include ':delaunator' +include ':math'