1// Copyright (C) 2024 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {TimeSpan, time} from './time'; 16import {HighPrecisionTime} from './high_precision_time'; 17 18/** 19 * Represents a time span using a high precision time value to represent the 20 * start of the span, and a number to represent the duration of the span. 21 */ 22export class HighPrecisionTimeSpan { 23 static readonly ZERO = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0); 24 25 readonly start: HighPrecisionTime; 26 readonly duration: number; 27 28 constructor(start: HighPrecisionTime, duration: number) { 29 this.start = start; 30 this.duration = duration; 31 } 32 33 /** 34 * Create a new span from integral start and end points. 35 * 36 * @param start The start of the span. 37 * @param end The end of the span. 38 */ 39 static fromTime(start: time, end: time): HighPrecisionTimeSpan { 40 return new HighPrecisionTimeSpan( 41 new HighPrecisionTime(start), 42 Number(end - start), 43 ); 44 } 45 46 /** 47 * The center point of the span. 48 */ 49 get midpoint(): HighPrecisionTime { 50 return this.start.addNumber(this.duration / 2); 51 } 52 53 /** 54 * The end of the span. 55 */ 56 get end(): HighPrecisionTime { 57 return this.start.addNumber(this.duration); 58 } 59 60 /** 61 * Checks if this span exactly equals another. 62 */ 63 equals(other: HighPrecisionTimeSpan): boolean { 64 return this.start.equals(other.start) && this.duration === other.duration; 65 } 66 67 /** 68 * Create a new span with the same duration but the start point moved through 69 * time by some amount of time. 70 */ 71 translate(time: number): HighPrecisionTimeSpan { 72 return new HighPrecisionTimeSpan(this.start.addNumber(time), this.duration); 73 } 74 75 /** 76 * Create a new span with the the start of the span moved backward and the end 77 * of the span moved forward by a certain amount of time. 78 */ 79 pad(time: number): HighPrecisionTimeSpan { 80 return new HighPrecisionTimeSpan( 81 this.start.subNumber(time), 82 this.duration + 2 * time, 83 ); 84 } 85 86 /** 87 * Create a new span which is zoomed in or out centered on a specific point. 88 * 89 * @param ratio The scaling ratio, the new duration will be the current 90 * duration * ratio. 91 * @param center The center point as a normalized value between 0 and 1 where 92 * 0 is the start of the time window and 1 is the end. 93 * @param minDur Don't allow the time span to become shorter than this. 94 */ 95 scale(ratio: number, center: number, minDur: number): HighPrecisionTimeSpan { 96 const currentDuration = this.duration; 97 const newDuration = Math.max(currentDuration * ratio, minDur); 98 // Delta between new and old duration 99 // +ve if new duration is shorter than old duration 100 const durationDeltaNanos = currentDuration - newDuration; 101 // If offset is 0, don't move the start at all 102 // If offset if 1, move the start by the amount the duration has changed 103 // If new duration is shorter - move start to right 104 // If new duration is longer - move start to left 105 const start = this.start.addNumber(durationDeltaNanos * center); 106 return new HighPrecisionTimeSpan(start, newDuration); 107 } 108 109 /** 110 * Create a new span that represents the intersection of this span with 111 * another. 112 * 113 * If the two spans do not overlap at all, the empty span is returned. 114 * 115 * @param start THe start of the other span. 116 * @param end The end of the other span. 117 */ 118 intersect(start: time, end: time): HighPrecisionTimeSpan { 119 if (!this.overlaps(start, end)) { 120 return HighPrecisionTimeSpan.ZERO; 121 } 122 const newStart = this.start.clamp(start, end); 123 const newEnd = this.end.clamp(start, end); 124 const newDuration = newEnd.sub(newStart).toNumber(); 125 return new HighPrecisionTimeSpan(newStart, newDuration); 126 } 127 128 /** 129 * Create a new timespan which fits within the specified bounds, preserving 130 * its duration if possible. 131 * 132 * This function moves the timespan forwards or backwards in time while 133 * keeping its duration unchanged, so that it fits entirely within the range 134 * defined by `start` and `end`. 135 * 136 * If the specified bounds are smaller than the current timespan's duration, a 137 * new timespan matching the bounds is returned. 138 * 139 * @param start The start of the bounds within which the timespan should fit. 140 * @param end The end of the bounds within which the timespan should fit. 141 * 142 * @example 143 * // assume `timespan` is defined as: [5, 8) 144 * timespan.fitWithin(10n, 20n); // -> [10, 13) 145 * timespan.fitWithin(-10n, -5n); // -> [-8, -5) 146 * timespan.fitWithin(1n, 2n); // -> [1, 2) 147 */ 148 fitWithin(start: time, end: time): HighPrecisionTimeSpan { 149 if (this.duration > Number(end - start)) { 150 // Current span is greater than the limits 151 return HighPrecisionTimeSpan.fromTime(start, end); 152 } 153 if (this.start.integral < start) { 154 // Current span starts before limits 155 return new HighPrecisionTimeSpan( 156 new HighPrecisionTime(start), 157 this.duration, 158 ); 159 } 160 if (this.end.gt(end)) { 161 // Current span ends after limits 162 return new HighPrecisionTimeSpan( 163 new HighPrecisionTime(end).subNumber(this.duration), 164 this.duration, 165 ); 166 } 167 return this; 168 } 169 170 /** 171 * Clamp duration to some minimum value. The start remains the same, just the 172 * duration is changed. 173 */ 174 clampDuration(minDuration: number): HighPrecisionTimeSpan { 175 if (this.duration < minDuration) { 176 return new HighPrecisionTimeSpan(this.start, minDuration); 177 } else { 178 return this; 179 } 180 } 181 182 /** 183 * Checks whether this span completely contains a time instant. 184 */ 185 contains(t: time): boolean { 186 return this.start.lte(t) && this.end.gt(t); 187 } 188 189 /** 190 * Checks whether this span entirely contains another span. 191 * 192 * @param start The start of the span to check. 193 * @param end The end of the span to check. 194 */ 195 containsSpan(start: time, end: time): boolean { 196 return this.start.lte(start) && this.end.gte(end); 197 } 198 199 /** 200 * Checks if this span overlaps at all with another. 201 * 202 * @param start The start of the span to check. 203 * @param end The end of the span to check. 204 */ 205 overlaps(start: time, end: time): boolean { 206 return !(this.start.gte(end) || this.end.lte(start)); 207 } 208 209 /** 210 * Get the span of integer intervals values that overlap this span. 211 */ 212 toTimeSpan(): TimeSpan { 213 return new TimeSpan(this.start.toTime('floor'), this.end.toTime('ceil')); 214 } 215} 216