1/* 2 * Copyright 2024 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import { Derivate } from './feature_derivate'; 18import { 19 DataPoint, 20 DataPointObject, 21 isNotFound, 22 MotionGoldenFeature, 23} from './golden'; 24import { 25 DataPointVisualization, 26 LineGraphVisualization, 27 PROBE_COLORS, 28 Visualization, 29} from './visualization'; 30 31export class Feature { 32 constructor( 33 readonly id: string, 34 readonly name: string, 35 readonly type: string, 36 readonly dataPoints: Array<DataPoint> 37 ) {} 38 39 private _visualization: Visualization | undefined; 40 41 get visualization(): Visualization { 42 if (this._visualization === undefined) { 43 this._visualization = createVisualization(this); 44 } 45 46 return this._visualization; 47 } 48 49 private _subFeatures: Array<Feature> | undefined; 50 51 get subFeatures(): Array<Feature> { 52 if (this._subFeatures === undefined) { 53 this._subFeatures = createSubFeatures(this); 54 } 55 56 return this._subFeatures!!; 57 } 58 59 private _derivates: Array<Derivate> | undefined; 60 61 get derivates(): Array<Derivate> { 62 if (this._derivates === undefined) { 63 this._derivates = createDerivatives(this); 64 } 65 return this._derivates; 66 } 67} 68 69export function recordedFeatureFactory( 70 featureData: MotionGoldenFeature 71): Feature { 72 return new Feature( 73 featureData.name, 74 featureData.name, 75 featureData.type, 76 featureData.data_points 77 ); 78} 79 80function createVisualization(feature: Feature): Visualization { 81 const { name, type } = feature; 82 83 let color: string | null = null; 84 85 if (name === 'input') { 86 color = PROBE_COLORS[0]; 87 } else if (name === 'output_target') { 88 color = PROBE_COLORS[1]; 89 } else if (name === 'output') { 90 color = PROBE_COLORS[2]; 91 } 92 93 if (name === 'alpha' && type === 'float') { 94 return new LineGraphVisualization(/* minValue */ 0, /*maxValue*/ 1, color); 95 } 96 97 if (['float', 'int', 'dp'].includes(type)) { 98 const numericValues = feature.dataPoints.filter( 99 (it): it is number => typeof it === 'number' 100 ); 101 const minValue = Math.min(...numericValues) ?? 0; 102 let maxValue = Math.max(...numericValues) ?? 1; 103 104 if (minValue === maxValue) { 105 maxValue += 1; 106 } 107 108 return new LineGraphVisualization(minValue, maxValue, color); 109 } 110 111 return new DataPointVisualization(); 112} 113 114function createDerivatives(feature: Feature): Derivate[] { 115 const { name, type } = feature; 116 117 const result: Derivate[] = []; 118 119 return result; 120} 121 122function createSubFeatures(feature: Feature): Feature[] { 123 const { name, type } = feature; 124 125 switch (type) { 126 case 'intSize': 127 case 'dpSize': 128 return [ 129 createSubFeature(feature, 'width', 'float', (point) => point['width']), 130 createSubFeature( 131 feature, 132 'height', 133 'float', 134 (point) => point['height'] 135 ), 136 ]; 137 case 'intOffset': 138 case 'dpOffset': 139 case 'offset': 140 return [ 141 createSubFeature(feature, 'x', 'float', (point) => point['x']), 142 createSubFeature(feature, 'y', 'float', (point) => point['y']), 143 ]; 144 145 case 'animatedVisibilityTransitions': 146 return [ 147 ...new Set( 148 feature.dataPoints.flatMap((it) => Object.keys(it as DataPointObject)) 149 ), 150 ] 151 .sort() 152 .map((it) => 153 createSubFeature( 154 feature, 155 it, 156 'animatedVisibilityValues', 157 (point) => point[it] 158 ) 159 ); 160 161 case 'animatedVisibilityValues': 162 return [ 163 ...new Set( 164 feature.dataPoints.flatMap((it) => 165 it ? Object.keys(it as DataPointObject) : [] 166 ) 167 ), 168 ] 169 .sort() 170 .flatMap((name) => { 171 let type: string; 172 173 switch (name) { 174 case 'alpha': 175 type = 'float'; 176 break; 177 case 'slide': 178 type = 'intOffset'; 179 break; 180 case 'scale': 181 type = 'float'; 182 break; 183 case 'size': 184 type = 'intSize'; 185 break; 186 default: 187 return []; 188 } 189 190 return [ 191 createSubFeature(feature, name, type, (point) => point[name]), 192 ]; 193 }); 194 } 195 196 return []; 197} 198 199function createSubFeature( 200 parent: Feature, 201 key: string, 202 type: string, 203 extract: (dataPoint: DataPointObject) => DataPoint 204): Feature { 205 return new Feature( 206 `${parent.id}::${key}`, 207 `${parent.name}[${key}]`, 208 type, 209 parent.dataPoints.map((it) => { 210 if (!isNotFound(it) && it instanceof Object) 211 return extract(it as DataPointObject); 212 213 return { type: 'not_found' }; 214 }) 215 ); 216} 217