Skip to content

Commit 2ee2fe4

Browse files
authored
Merge pull request #1644 from NASA-AMMOS/feat/windows-qol-updates
Implement more convenient delegate methods for Windows and Interval
2 parents c467d39 + e983361 commit 2ee2fe4

File tree

4 files changed

+182
-0
lines changed

4 files changed

+182
-0
lines changed

procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/Interval.kt

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package gov.nasa.ammos.aerie.procedural.timeline
22

3+
import gov.nasa.ammos.aerie.procedural.timeline.collections.Windows
34
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration
45
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo
56
import gov.nasa.ammos.aerie.procedural.timeline.payloads.IntervalLike
@@ -218,6 +219,8 @@ data class Interval @JvmOverloads constructor(
218219
return between(start, end, startInclusivity, endInclusivity)
219220
}
220221

222+
infix fun intersection(other: Windows) = other intersection this
223+
221224
/**
222225
* Calculates the union between this interval and another, as a list of intervals.
223226
*
@@ -252,6 +255,8 @@ data class Interval @JvmOverloads constructor(
252255
return listOf(between(start, end, startInclusivity, endInclusivity))
253256
}
254257

258+
infix fun union(other: Windows) = other union this
259+
255260
/** The smallest interval that contains both this and another interval. */
256261
infix fun hull(other: Interval): Interval {
257262
val union = union(other)

procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/Windows.kt

+73
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package gov.nasa.ammos.aerie.procedural.timeline.collections
22

33
import gov.nasa.ammos.aerie.procedural.timeline.BaseTimeline
4+
import gov.nasa.ammos.aerie.procedural.timeline.BoundsTransformer
45
import gov.nasa.ammos.aerie.procedural.timeline.Interval
56
import gov.nasa.ammos.aerie.procedural.timeline.Timeline
67
import gov.nasa.ammos.aerie.procedural.timeline.ops.NonZeroDurationOps
78
import gov.nasa.ammos.aerie.procedural.timeline.ops.SerialOps
89
import gov.nasa.ammos.aerie.procedural.timeline.ops.coalesce.CoalesceIntervalsOp
910
import gov.nasa.ammos.aerie.procedural.timeline.util.preprocessList
1011
import gov.nasa.ammos.aerie.procedural.timeline.util.sorted
12+
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration
1113

1214
/** A coalescing timeline of [Intervals][Interval] with no extra data. */
1315
data class Windows(private val timeline: Timeline<Interval, Windows>):
@@ -25,10 +27,16 @@ data class Windows(private val timeline: Timeline<Interval, Windows>):
2527
combined.sorted()
2628
}
2729

30+
/** Calculates the union of this and a single [Interval]. */
31+
infix fun union(other: Interval) = union(Windows(other))
32+
2833
/** Calculates the intersection of this and another [Windows]. */
2934
infix fun intersection(other: Windows) =
3035
unsafeMap2(::Windows, other) { _, _, i -> i }
3136

37+
/** Calculates the intersection of this and a single [Interval]. */
38+
infix fun intersection(other: Interval) = select(other)
39+
3240
/** Calculates the complement; i.e. highlights everything that is not highlighted in this timeline. */
3341
fun complement() = unsafeOperate { opts ->
3442
val result = mutableListOf(opts.bounds)
@@ -37,4 +45,69 @@ data class Windows(private val timeline: Timeline<Interval, Windows>):
3745
}
3846
result
3947
}
48+
49+
/** Subtracts the intersection with another [Windows] from this. */
50+
fun minus(other: Windows) = intersection(other.complement())
51+
52+
/**
53+
* Subtracts the intersection with a single [Interval] from this.
54+
*
55+
* Essentially a rename of [gov.nasa.ammos.aerie.procedural.timeline.ops.GeneralOps.unset].
56+
*/
57+
fun minus(other: Interval) = unset(other)
58+
59+
/**
60+
* Returns a new [Windows] where each interval is replaced by just the point at its start time.
61+
*
62+
* Doesn't care about inclusivity.
63+
* If an input interval doesn't contain its start point, the output will still be at the same time.
64+
*/
65+
fun starts() = unsafeMapIntervals(BoundsTransformer.IDENTITY, false) {
66+
Interval.at(it.start)
67+
}
68+
69+
/**
70+
* Returns a new [Windows] where each interval is replaced by just the point at its end time.
71+
*
72+
* Doesn't care about inclusivity.
73+
* If an input interval doesn't contain its end point, the output will still be at the same time.
74+
*/
75+
fun ends() = unsafeMapIntervals(BoundsTransformer.IDENTITY, false) {
76+
Interval.at(it.end)
77+
}
78+
79+
/**
80+
* Independently shift the start and end points of each interval.
81+
*
82+
* The start and end can be shifted by different amounts, stretching or squishing the interval.
83+
* If the interval is empty after the shift, it is removed.
84+
*
85+
* Unlike [gov.nasa.ammos.aerie.procedural.timeline.ops.ParallelOps.shiftEndpoints], this function
86+
* DOES coalesce the output. If you stretch the intervals such that they start to
87+
* overlap, those overlapping intervals will be combined into one. This means that applying
88+
* the reverse operation (i.e. `windows.shiftEndpoints(Duration.ZERO, Duration.MINUTE).shiftEndpoints(Duration.ZERO, Duration.MINUTE.negate())`
89+
* does NOT necessarily result in the same timeline.
90+
*
91+
* To turn off coalescing behavior, convert it into a [Universal] timeline first with `.isolate($ -> true)`
92+
* or `.unsafeCast(Universal::new)`. You can undo this with `.highlight($ -> true)`.
93+
*/
94+
fun shiftEndpoints(shiftStart: Duration, shiftEnd: Duration = shiftStart) =
95+
unsafeMapIntervals(
96+
{ i ->
97+
Interval.between(
98+
Duration.min(i.start.minus(shiftStart), i.start.minus(shiftEnd)),
99+
Duration.max(i.end.minus(shiftStart), i.end.minus(shiftEnd)),
100+
i.startInclusivity,
101+
i.endInclusivity
102+
)
103+
},
104+
true
105+
) { t -> t.interval.shiftBy(shiftStart, shiftEnd) }
106+
107+
/**
108+
* Extends the end of each interval by a duration. The duration can be negative.
109+
*
110+
* See [shiftEndpoints] for a warning about coalescing behavior.
111+
*/
112+
fun extend(shiftEnd: Duration) = shiftEndpoints(Duration.ZERO, shiftEnd)
40113
}

procedural/timeline/src/main/kotlin/gov/nasa/ammos/aerie/procedural/timeline/ops/ParallelOps.kt

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo
2222
interface ParallelOps<T: IntervalLike<T>, THIS: ParallelOps<T, THIS>>: GeneralOps<T, THIS>, CoalesceNoOp<T, THIS> {
2323

2424
override fun isAlwaysSorted() = false
25+
26+
/**
27+
* Returns just the intervals from the timeline, without coalescing.
28+
*/
29+
fun collectIntervals() = collect().map { it.interval }
2530

2631
/** [(DOC)][highlightAll] Highlights all objects in the timeline in a new [Windows] timeline. */
2732
fun highlightAll() = unsafeMap(::Windows, BoundsTransformer.IDENTITY, true) { it.interval }
@@ -138,6 +143,9 @@ interface ParallelOps<T: IntervalLike<T>, THIS: ParallelOps<T, THIS>>: GeneralOp
138143
true
139144
) { t -> t.interval.shiftBy(shiftStart, shiftEnd) }
140145

146+
/** [(DOC)][extend] Extends just the end of each object's interval by a duration. The duration can be negative. */
147+
fun extend(shiftEnd: Duration) = shiftEndpoints(Duration.ZERO, shiftEnd)
148+
141149
/** [(DOC)][active] Returns a [Booleans] profile that is true when this timeline has an active object. */
142150
fun active() = flattenIntoProfile(::Booleans) { _ -> true }.assignGaps(Booleans(false))
143151

procedural/timeline/src/test/kotlin/gov/nasa/ammos/aerie/procedural/timeline/collections/WindowsTest.kt

+96
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration.seconds
77
import gov.nasa.ammos.aerie.procedural.timeline.Interval.Companion.at
88
import gov.nasa.ammos.aerie.procedural.timeline.Interval.Companion.between
99
import gov.nasa.ammos.aerie.procedural.timeline.Interval.Inclusivity.Exclusive
10+
import gov.nasa.ammos.aerie.procedural.timeline.Interval.Inclusivity.Inclusive
1011
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeTo
1112
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.rangeUntil
13+
import gov.nasa.ammos.aerie.procedural.timeline.util.duration.unaryMinus
1214
import org.junit.jupiter.api.Assertions.assertIterableEquals
1315
import org.junit.jupiter.api.Test
1416

@@ -129,4 +131,98 @@ class WindowsTest {
129131
)
130132
}
131133

134+
@Test
135+
fun starts() {
136+
val w = Windows(
137+
at(seconds(1)),
138+
seconds(2)..seconds(3),
139+
seconds(4)..<seconds(5),
140+
between(seconds(6), seconds(7), Exclusive, Inclusive)
141+
)
142+
143+
val result = w.starts()
144+
145+
assertIterableEquals(
146+
listOf(
147+
at(seconds(1)),
148+
at(seconds(2)),
149+
at(seconds(4)),
150+
at(seconds(6))
151+
),
152+
result
153+
)
154+
}
155+
156+
@Test
157+
fun ends() {
158+
val w = Windows(
159+
at(seconds(1)),
160+
seconds(2)..seconds(3),
161+
seconds(4)..<seconds(5),
162+
between(seconds(6), seconds(7), Exclusive, Inclusive)
163+
)
164+
165+
val result = w.ends()
166+
167+
assertIterableEquals(
168+
listOf(
169+
at(seconds(1)),
170+
at(seconds(3)),
171+
at(seconds(5)),
172+
at(seconds(7))
173+
),
174+
result
175+
)
176+
}
177+
178+
@Test
179+
fun shiftEndpointsShrink() {
180+
val w = Windows(
181+
at(seconds(1)),
182+
seconds(2)..seconds(3),
183+
seconds(4)..seconds(6),
184+
between(seconds(7), seconds(9), Exclusive, Inclusive),
185+
seconds(10)..<seconds(12),
186+
seconds(13)..seconds(17)
187+
)
188+
189+
val result1 = w.shiftEndpoints(seconds(2), Duration.ZERO)
190+
191+
assertIterableEquals(
192+
listOf(
193+
at(seconds(6)),
194+
seconds(15)..seconds(17)
195+
),
196+
result1
197+
)
198+
199+
val result2 = w.shiftEndpoints(Duration.ZERO, -seconds(2))
200+
201+
assertIterableEquals(
202+
listOf(
203+
at(seconds(4)),
204+
seconds(13)..seconds(15)
205+
),
206+
result2
207+
)
208+
}
209+
210+
@Test
211+
fun shiftEndpointsGrow() {
212+
val w = Windows(
213+
at(seconds(0)),
214+
seconds(2)..<seconds(3),
215+
seconds(4)..seconds(5),
216+
)
217+
218+
val result = w.extend(seconds(1))
219+
220+
assertIterableEquals(
221+
listOf(
222+
seconds(0)..seconds(1),
223+
seconds(2)..seconds(6),
224+
),
225+
result
226+
)
227+
}
132228
}

0 commit comments

Comments
 (0)