Skip to content

Commit 639b718

Browse files
committed
Extract isoline executor
1 parent c443791 commit 639b718

File tree

6 files changed

+137
-53
lines changed

6 files changed

+137
-53
lines changed

src/main/kotlin/com/kylecorry/sol/math/interpolation/Interpolation.kt

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.kylecorry.sol.math.interpolation
22

33
import com.kylecorry.sol.math.Vector2
4+
import com.kylecorry.sol.shared.Executor
5+
import com.kylecorry.sol.shared.SequentialExecutor
46
import kotlin.math.ceil
57
import kotlin.math.floor
68

@@ -135,36 +137,20 @@ object Interpolation {
135137
* Interpolates the isoline for a grid of values using the Marching Squares algorithm.
136138
* @param grid A 2D grid of point to value pairs. The points should be equidistant. It is recommended to supply 1 extra row and column on each side of the grid to ensure the isoline extends to the edges.
137139
* @param threshold The value to use as the isoline threshold.
140+
* @param executor The executor of the isoline calculations to optionally process in parallel. Defaults to sequential processing.
138141
* @param interpolator A function that takes a percentage (0 to 1) and two values (percent from a to b), and returns the interpolated point.
139142
* @return A list of isoline segments.
140143
*/
141144
fun <T> getIsoline(
142145
grid: List<List<Pair<T, Float>>>,
143146
threshold: Float,
147+
executor: Executor = SequentialExecutor(),
144148
interpolator: (percent: Float, a: T, b: T) -> T
145149
): List<IsolineSegment<T>> {
146150
return MarchingSquares.getIsoline(
147151
grid,
148152
threshold,
149-
interpolator
150-
)
151-
}
152-
153-
/**
154-
* Interpolates the isoline for a grid of values using the Marching Squares algorithm. This function returns the calculators so it can be calculated in parallel.
155-
* @param grid A 2D grid of point to value pairs. The points should be equidistant. It is recommended to supply 1 extra row and column on each side of the grid to ensure the isoline extends to the edges.
156-
* @param threshold The value to use as the isoline threshold.
157-
* @param interpolator A function that takes a percentage (0 to 1) and two values (percent from a to b), and returns the interpolated point.
158-
* @return A list of isoline segment calculators.
159-
*/
160-
fun <T> getIsolineCalculators(
161-
grid: List<List<Pair<T, Float>>>,
162-
threshold: Float,
163-
interpolator: (percent: Float, a: T, b: T) -> T
164-
): List<() -> List<IsolineSegment<T>>> {
165-
return MarchingSquares.getIsolineCalculators(
166-
grid,
167-
threshold,
153+
executor,
168154
interpolator
169155
)
170156
}

src/main/kotlin/com/kylecorry/sol/math/interpolation/MarchingSquares.kt

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package com.kylecorry.sol.math.interpolation
22

33
import com.kylecorry.sol.math.SolMath
4+
import com.kylecorry.sol.shared.Executor
5+
import com.kylecorry.sol.shared.SequentialExecutor
46
import kotlin.math.max
57
import kotlin.math.min
68

79
internal object MarchingSquares {
810

9-
fun <T> getIsolineCalculators(
11+
fun <T> getIsoline(
1012
grid: List<List<Pair<T, Float>>>,
1113
threshold: Float,
14+
executor: Executor = SequentialExecutor(),
1215
interpolator: (percent: Float, a: T, b: T) -> T
13-
): List<() -> List<IsolineSegment<T>>> {
14-
val squares = mutableListOf<List<Pair<T, Float>>>()
16+
): List<IsolineSegment<T>> {
17+
val calculators = mutableListOf<() -> List<IsolineSegment<T>>>()
1518
for (i in 0 until grid.size - 1) {
1619
for (j in 0 until grid[i].size - 1) {
1720
val square = listOf(
@@ -20,24 +23,13 @@ internal object MarchingSquares {
2023
grid[i + 1][j + 1],
2124
grid[i + 1][j]
2225
)
23-
squares.add(square)
24-
}
25-
}
26-
27-
return squares.map { square ->
28-
{
29-
marchingSquares(square, threshold, interpolator)
26+
calculators.add {
27+
marchingSquares(square, threshold, interpolator)
28+
}
3029
}
3130
}
32-
}
3331

34-
fun <T> getIsoline(
35-
grid: List<List<Pair<T, Float>>>,
36-
threshold: Float,
37-
interpolator: (percent: Float, a: T, b: T) -> T
38-
): List<IsolineSegment<T>> {
39-
val calculators = getIsolineCalculators(grid, threshold, interpolator)
40-
return calculators.flatMap { it() }
32+
return executor.map(calculators).flatten()
4133
}
4234

4335
private fun <T> marchingSquares(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.kylecorry.sol.shared
2+
3+
interface Executor {
4+
/**
5+
* Execute tasks that map to a value.
6+
* @param tasks the tasks
7+
* @return the evaluated values in the same order as the tasks
8+
*/
9+
fun <T> map(tasks: List<() -> T>): List<T>
10+
11+
/**
12+
* Execute tasks.
13+
* @param tasks the tasks
14+
*/
15+
fun run(tasks: List<() -> Unit>)
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.kylecorry.sol.shared
2+
3+
class SequentialExecutor : Executor {
4+
override fun <T> map(tasks: List<() -> T>): List<T> {
5+
return tasks.map { it() }
6+
}
7+
8+
override fun run(tasks: List<() -> Unit>) {
9+
tasks.forEach { it() }
10+
}
11+
}

src/test/kotlin/com/kylecorry/sol/math/interpolation/InterpolationTest.kt

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.kylecorry.sol.math.Vector2
44
import org.junit.jupiter.api.Assertions.assertEquals
55
import org.junit.jupiter.api.Assertions.assertTrue
66
import org.junit.jupiter.api.Test
7+
import kotlin.math.abs
78

89
class InterpolationTest {
910

@@ -36,7 +37,7 @@ class InterpolationTest {
3637
fun linearVector() {
3738
val point1 = Vector2(0f, 0f)
3839
val point2 = Vector2(2f, 4f)
39-
40+
4041
assertEquals(0f, Interpolation.linear(0f, point1, point2), 0.0001f)
4142
assertEquals(2f, Interpolation.linear(1f, point1, point2), 0.0001f)
4243
assertEquals(4f, Interpolation.linear(2f, point1, point2), 0.0001f)
@@ -65,7 +66,7 @@ class InterpolationTest {
6566
val point1 = Vector2(1f, 1f)
6667
val point2 = Vector2(2f, 4f)
6768
val point3 = Vector2(3f, 9f)
68-
69+
6970
assertEquals(1f, Interpolation.cubic(1f, point0, point1, point2, point3), 0.0001f)
7071
assertEquals(4f, Interpolation.cubic(2f, point0, point1, point2, point3), 0.0001f)
7172
assertEquals(2.25f, Interpolation.cubic(1.5f, point0, point1, point2, point3), 0.0001f)
@@ -83,16 +84,16 @@ class InterpolationTest {
8384
// 3 points (quadratic interpolation)
8485
val xs = listOf(0f, 1f, 2f)
8586
val ys = listOf(0f, 1f, 4f)
86-
87+
8788
assertEquals(0f, Interpolation.interpolate(0f, xs, ys), 0.0001f)
8889
assertEquals(1f, Interpolation.interpolate(1f, xs, ys), 0.0001f)
8990
assertEquals(4f, Interpolation.interpolate(2f, xs, ys), 0.0001f)
9091
assertEquals(0.25f, Interpolation.interpolate(0.5f, xs, ys), 0.0001f)
91-
92+
9293
// 4 points (cubic interpolation)
9394
val xs2 = listOf(0f, 1f, 2f, 3f)
9495
val ys2 = listOf(0f, 1f, 8f, 27f)
95-
96+
9697
assertEquals(0f, Interpolation.interpolate(0f, xs2, ys2), 0.0001f)
9798
assertEquals(1f, Interpolation.interpolate(1f, xs2, ys2), 0.0001f)
9899
assertEquals(8f, Interpolation.interpolate(2f, xs2, ys2), 0.0001f)
@@ -104,19 +105,19 @@ class InterpolationTest {
104105
fun getMultiplesBetweenFloat() {
105106
val result1 = Interpolation.getMultiplesBetween(0f, 10f, 2f)
106107
assertEquals(listOf(0f, 2f, 4f, 6f, 8f, 10f), result1)
107-
108+
108109
val result2 = Interpolation.getMultiplesBetween(1f, 9f, 2f)
109110
assertEquals(listOf(2f, 4f, 6f, 8f), result2)
110-
111+
111112
val result3 = Interpolation.getMultiplesBetween(0f, 2f, 0.5f)
112113
assertEquals(listOf(0f, 0.5f, 1f, 1.5f, 2f), result3)
113-
114+
114115
val result4 = Interpolation.getMultiplesBetween(-5f, 5f, 2.5f)
115116
assertEquals(listOf(-5f, -2.5f, 0f, 2.5f, 5f), result4)
116-
117+
117118
val result5 = Interpolation.getMultiplesBetween(0.1f, 0.4f, 1f)
118119
assertTrue(result5.isEmpty())
119-
120+
120121
val result6 = Interpolation.getMultiplesBetween(2f, 2f, 1f)
121122
assertEquals(listOf(2f), result6)
122123
}
@@ -125,22 +126,22 @@ class InterpolationTest {
125126
fun getMultiplesBetweenDouble() {
126127
val result1 = Interpolation.getMultiplesBetween(0.0, 10.0, 2.0)
127128
assertEquals(listOf(0.0, 2.0, 4.0, 6.0, 8.0, 10.0), result1)
128-
129+
129130
val result2 = Interpolation.getMultiplesBetween(1.0, 9.0, 2.0)
130131
assertEquals(listOf(2.0, 4.0, 6.0, 8.0), result2)
131-
132+
132133
val result3 = Interpolation.getMultiplesBetween(0.0, 2.0, 0.5)
133134
assertEquals(listOf(0.0, 0.5, 1.0, 1.5, 2.0), result3)
134-
135+
135136
val result4 = Interpolation.getMultiplesBetween(-5.0, 5.0, 2.5)
136137
assertEquals(listOf(-5.0, -2.5, 0.0, 2.5, 5.0), result4)
137-
138+
138139
val result5 = Interpolation.getMultiplesBetween(0.1, 0.4, 1.0)
139140
assertTrue(result5.isEmpty())
140-
141+
141142
val result6 = Interpolation.getMultiplesBetween(2.0, 2.0, 1.0)
142143
assertEquals(listOf(2.0), result6)
143-
144+
144145
val result7 = Interpolation.getMultiplesBetween(0.0, 0.31, 0.1)
145146
assertEquals(4, result7.size)
146147
assertEquals(0.0, result7[0], 0.0001)
@@ -149,4 +150,46 @@ class InterpolationTest {
149150
assertEquals(0.3, result7[3], 0.0001)
150151
}
151152

153+
@Test
154+
fun getIsoline() {
155+
val grid = listOf(
156+
listOf(Vector2(0f, 0f) to 0f, Vector2(1f, 0f) to 0f, Vector2(2f, 0f) to 0f),
157+
listOf(Vector2(0f, 1f) to 0f, Vector2(1f, 1f) to 1f, Vector2(2f, 1f) to 0f),
158+
listOf(Vector2(0f, 2f) to 0f, Vector2(1f, 2f) to 0f, Vector2(2f, 2f) to 0f)
159+
)
160+
161+
val isolines = Interpolation.getIsoline(grid, 0.5f) { pct, a, b ->
162+
Vector2(a.x + (b.x - a.x) * pct, a.y + (b.y - a.y) * pct)
163+
}
164+
165+
assertEquals(4, isolines.size)
166+
167+
assertSegment(isolines[0], 1f, 0.5f, 0.5f, 1f)
168+
assertSegment(isolines[1], 1f, 0.5f, 1.5f, 1f)
169+
assertSegment(isolines[2], 0.5f, 1f, 1f, 1.5f)
170+
assertSegment(isolines[3], 1.5f, 1f, 1f, 1.5f)
171+
}
172+
173+
private fun assertSegment(
174+
segment: IsolineSegment<Vector2>,
175+
x1: Float,
176+
y1: Float,
177+
x2: Float,
178+
y2: Float,
179+
threshold: Float = 0.001f
180+
) {
181+
val start = segment.start
182+
val end = segment.end
183+
184+
val match1 = (abs(start.x - x1) < threshold && abs(start.y - y1) < threshold &&
185+
abs(end.x - x2) < threshold && abs(end.y - y2) < threshold)
186+
187+
val match2 = (abs(start.x - x2) < threshold && abs(start.y - y2) < threshold &&
188+
abs(end.x - x1) < threshold && abs(end.y - y1) < threshold)
189+
190+
assertTrue(
191+
match1 || match2,
192+
"Expected ($x1, $y1)-($x2, $y2) but got (${start.x}, ${start.y})-(${end.x}, ${end.y})"
193+
)
194+
}
152195
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.kylecorry.sol.shared
2+
3+
import org.junit.jupiter.api.Assertions.assertEquals
4+
import org.junit.jupiter.api.Test
5+
6+
class SequentialExecutorTest {
7+
8+
@Test
9+
fun map() {
10+
val executor = SequentialExecutor()
11+
val tasks = listOf(
12+
{ 1 },
13+
{ 2 },
14+
{ 3 }
15+
)
16+
17+
val result = executor.map(tasks)
18+
19+
assertEquals(listOf(1, 2, 3), result)
20+
}
21+
22+
@Test
23+
fun run() {
24+
val executor = SequentialExecutor()
25+
val result = mutableListOf<Int>()
26+
val tasks = listOf<() -> Unit>(
27+
{ result.add(1) },
28+
{ result.add(2) },
29+
{ result.add(3) }
30+
)
31+
32+
executor.run(tasks)
33+
34+
assertEquals(listOf(1, 2, 3), result)
35+
}
36+
}

0 commit comments

Comments
 (0)