xref: /aosp_15_r20/external/libmonet/temperature/TemperatureCache.java (revision 970e10460f970939fd510dd6ad3e0d65908272e3)
1*970e1046SAndroid Build Coastguard Worker /*
2*970e1046SAndroid Build Coastguard Worker  * Copyright 2022 Google LLC
3*970e1046SAndroid Build Coastguard Worker  *
4*970e1046SAndroid Build Coastguard Worker  * Licensed under the Apache License, Version 2.0 (the "License");
5*970e1046SAndroid Build Coastguard Worker  * you may not use this file except in compliance with the License.
6*970e1046SAndroid Build Coastguard Worker  * You may obtain a copy of the License at
7*970e1046SAndroid Build Coastguard Worker  *
8*970e1046SAndroid Build Coastguard Worker  *      http://www.apache.org/licenses/LICENSE-2.0
9*970e1046SAndroid Build Coastguard Worker  *
10*970e1046SAndroid Build Coastguard Worker  * Unless required by applicable law or agreed to in writing, software
11*970e1046SAndroid Build Coastguard Worker  * distributed under the License is distributed on an "AS IS" BASIS,
12*970e1046SAndroid Build Coastguard Worker  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*970e1046SAndroid Build Coastguard Worker  * See the License for the specific language governing permissions and
14*970e1046SAndroid Build Coastguard Worker  * limitations under the License.
15*970e1046SAndroid Build Coastguard Worker  */
16*970e1046SAndroid Build Coastguard Worker 
17*970e1046SAndroid Build Coastguard Worker package com.google.ux.material.libmonet.temperature;
18*970e1046SAndroid Build Coastguard Worker 
19*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.hct.Hct;
20*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.utils.ColorUtils;
21*970e1046SAndroid Build Coastguard Worker import com.google.ux.material.libmonet.utils.MathUtils;
22*970e1046SAndroid Build Coastguard Worker import java.util.ArrayList;
23*970e1046SAndroid Build Coastguard Worker import java.util.Collections;
24*970e1046SAndroid Build Coastguard Worker import java.util.Comparator;
25*970e1046SAndroid Build Coastguard Worker import java.util.HashMap;
26*970e1046SAndroid Build Coastguard Worker import java.util.List;
27*970e1046SAndroid Build Coastguard Worker import java.util.Map;
28*970e1046SAndroid Build Coastguard Worker 
29*970e1046SAndroid Build Coastguard Worker /**
30*970e1046SAndroid Build Coastguard Worker  * Design utilities using color temperature theory.
31*970e1046SAndroid Build Coastguard Worker  *
32*970e1046SAndroid Build Coastguard Worker  * <p>Analogous colors, complementary color, and cache to efficiently, lazily, generate data for
33*970e1046SAndroid Build Coastguard Worker  * calculations when needed.
34*970e1046SAndroid Build Coastguard Worker  */
35*970e1046SAndroid Build Coastguard Worker public final class TemperatureCache {
36*970e1046SAndroid Build Coastguard Worker   private final Hct input;
37*970e1046SAndroid Build Coastguard Worker 
38*970e1046SAndroid Build Coastguard Worker   private Hct precomputedComplement;
39*970e1046SAndroid Build Coastguard Worker   private List<Hct> precomputedHctsByTemp;
40*970e1046SAndroid Build Coastguard Worker   private List<Hct> precomputedHctsByHue;
41*970e1046SAndroid Build Coastguard Worker   private Map<Hct, Double> precomputedTempsByHct;
42*970e1046SAndroid Build Coastguard Worker 
TemperatureCache()43*970e1046SAndroid Build Coastguard Worker   private TemperatureCache() {
44*970e1046SAndroid Build Coastguard Worker     throw new UnsupportedOperationException();
45*970e1046SAndroid Build Coastguard Worker   }
46*970e1046SAndroid Build Coastguard Worker 
47*970e1046SAndroid Build Coastguard Worker   /**
48*970e1046SAndroid Build Coastguard Worker    * Create a cache that allows calculation of ex. complementary and analogous colors.
49*970e1046SAndroid Build Coastguard Worker    *
50*970e1046SAndroid Build Coastguard Worker    * @param input Color to find complement/analogous colors of. Any colors will have the same tone,
51*970e1046SAndroid Build Coastguard Worker    * and chroma as the input color, modulo any restrictions due to the other hues having lower
52*970e1046SAndroid Build Coastguard Worker    * limits on chroma.
53*970e1046SAndroid Build Coastguard Worker    */
TemperatureCache(Hct input)54*970e1046SAndroid Build Coastguard Worker   public TemperatureCache(Hct input) {
55*970e1046SAndroid Build Coastguard Worker     this.input = input;
56*970e1046SAndroid Build Coastguard Worker   }
57*970e1046SAndroid Build Coastguard Worker 
58*970e1046SAndroid Build Coastguard Worker   /**
59*970e1046SAndroid Build Coastguard Worker    * A color that complements the input color aesthetically.
60*970e1046SAndroid Build Coastguard Worker    *
61*970e1046SAndroid Build Coastguard Worker    * <p>In art, this is usually described as being across the color wheel. History of this shows
62*970e1046SAndroid Build Coastguard Worker    * intent as a color that is just as cool-warm as the input color is warm-cool.
63*970e1046SAndroid Build Coastguard Worker    */
getComplement()64*970e1046SAndroid Build Coastguard Worker   public Hct getComplement() {
65*970e1046SAndroid Build Coastguard Worker     if (precomputedComplement != null) {
66*970e1046SAndroid Build Coastguard Worker       return precomputedComplement;
67*970e1046SAndroid Build Coastguard Worker     }
68*970e1046SAndroid Build Coastguard Worker 
69*970e1046SAndroid Build Coastguard Worker     double coldestHue = getColdest().getHue();
70*970e1046SAndroid Build Coastguard Worker     double coldestTemp = getTempsByHct().get(getColdest());
71*970e1046SAndroid Build Coastguard Worker 
72*970e1046SAndroid Build Coastguard Worker     double warmestHue = getWarmest().getHue();
73*970e1046SAndroid Build Coastguard Worker     double warmestTemp = getTempsByHct().get(getWarmest());
74*970e1046SAndroid Build Coastguard Worker     double range = warmestTemp - coldestTemp;
75*970e1046SAndroid Build Coastguard Worker     boolean startHueIsColdestToWarmest = isBetween(input.getHue(), coldestHue, warmestHue);
76*970e1046SAndroid Build Coastguard Worker     double startHue = startHueIsColdestToWarmest ? warmestHue : coldestHue;
77*970e1046SAndroid Build Coastguard Worker     double endHue = startHueIsColdestToWarmest ? coldestHue : warmestHue;
78*970e1046SAndroid Build Coastguard Worker     double directionOfRotation = 1.;
79*970e1046SAndroid Build Coastguard Worker     double smallestError = 1000.;
80*970e1046SAndroid Build Coastguard Worker     Hct answer = getHctsByHue().get((int) Math.round(input.getHue()));
81*970e1046SAndroid Build Coastguard Worker 
82*970e1046SAndroid Build Coastguard Worker     double complementRelativeTemp = (1. - getRelativeTemperature(input));
83*970e1046SAndroid Build Coastguard Worker     // Find the color in the other section, closest to the inverse percentile
84*970e1046SAndroid Build Coastguard Worker     // of the input color. This is the complement.
85*970e1046SAndroid Build Coastguard Worker     for (double hueAddend = 0.; hueAddend <= 360.; hueAddend += 1.) {
86*970e1046SAndroid Build Coastguard Worker       double hue = MathUtils.sanitizeDegreesDouble(
87*970e1046SAndroid Build Coastguard Worker           startHue + directionOfRotation * hueAddend);
88*970e1046SAndroid Build Coastguard Worker       if (!isBetween(hue, startHue, endHue)) {
89*970e1046SAndroid Build Coastguard Worker         continue;
90*970e1046SAndroid Build Coastguard Worker       }
91*970e1046SAndroid Build Coastguard Worker       Hct possibleAnswer = getHctsByHue().get((int) Math.round(hue));
92*970e1046SAndroid Build Coastguard Worker       double relativeTemp =
93*970e1046SAndroid Build Coastguard Worker           (getTempsByHct().get(possibleAnswer) - coldestTemp) / range;
94*970e1046SAndroid Build Coastguard Worker       double error = Math.abs(complementRelativeTemp - relativeTemp);
95*970e1046SAndroid Build Coastguard Worker       if (error < smallestError) {
96*970e1046SAndroid Build Coastguard Worker         smallestError = error;
97*970e1046SAndroid Build Coastguard Worker         answer = possibleAnswer;
98*970e1046SAndroid Build Coastguard Worker       }
99*970e1046SAndroid Build Coastguard Worker     }
100*970e1046SAndroid Build Coastguard Worker     precomputedComplement = answer;
101*970e1046SAndroid Build Coastguard Worker     return precomputedComplement;
102*970e1046SAndroid Build Coastguard Worker   }
103*970e1046SAndroid Build Coastguard Worker 
104*970e1046SAndroid Build Coastguard Worker   /**
105*970e1046SAndroid Build Coastguard Worker    * 5 colors that pair well with the input color.
106*970e1046SAndroid Build Coastguard Worker    *
107*970e1046SAndroid Build Coastguard Worker    * <p>The colors are equidistant in temperature and adjacent in hue.
108*970e1046SAndroid Build Coastguard Worker    */
getAnalogousColors()109*970e1046SAndroid Build Coastguard Worker   public List<Hct> getAnalogousColors() {
110*970e1046SAndroid Build Coastguard Worker     return getAnalogousColors(5, 12);
111*970e1046SAndroid Build Coastguard Worker   }
112*970e1046SAndroid Build Coastguard Worker 
113*970e1046SAndroid Build Coastguard Worker   /**
114*970e1046SAndroid Build Coastguard Worker    * A set of colors with differing hues, equidistant in temperature.
115*970e1046SAndroid Build Coastguard Worker    *
116*970e1046SAndroid Build Coastguard Worker    * <p>In art, this is usually described as a set of 5 colors on a color wheel divided into 12
117*970e1046SAndroid Build Coastguard Worker    * sections. This method allows provision of either of those values.
118*970e1046SAndroid Build Coastguard Worker    *
119*970e1046SAndroid Build Coastguard Worker    * <p>Behavior is undefined when count or divisions is 0. When divisions < count, colors repeat.
120*970e1046SAndroid Build Coastguard Worker    *
121*970e1046SAndroid Build Coastguard Worker    * @param count The number of colors to return, includes the input color.
122*970e1046SAndroid Build Coastguard Worker    * @param divisions The number of divisions on the color wheel.
123*970e1046SAndroid Build Coastguard Worker    */
getAnalogousColors(int count, int divisions)124*970e1046SAndroid Build Coastguard Worker   public List<Hct> getAnalogousColors(int count, int divisions) {
125*970e1046SAndroid Build Coastguard Worker     // The starting hue is the hue of the input color.
126*970e1046SAndroid Build Coastguard Worker     int startHue = (int) Math.round(input.getHue());
127*970e1046SAndroid Build Coastguard Worker     Hct startHct = getHctsByHue().get(startHue);
128*970e1046SAndroid Build Coastguard Worker     double lastTemp = getRelativeTemperature(startHct);
129*970e1046SAndroid Build Coastguard Worker 
130*970e1046SAndroid Build Coastguard Worker     List<Hct> allColors = new ArrayList<>();
131*970e1046SAndroid Build Coastguard Worker     allColors.add(startHct);
132*970e1046SAndroid Build Coastguard Worker 
133*970e1046SAndroid Build Coastguard Worker     double absoluteTotalTempDelta = 0.f;
134*970e1046SAndroid Build Coastguard Worker     for (int i = 0; i < 360; i++) {
135*970e1046SAndroid Build Coastguard Worker       int hue = MathUtils.sanitizeDegreesInt(startHue + i);
136*970e1046SAndroid Build Coastguard Worker       Hct hct = getHctsByHue().get(hue);
137*970e1046SAndroid Build Coastguard Worker       double temp = getRelativeTemperature(hct);
138*970e1046SAndroid Build Coastguard Worker       double tempDelta = Math.abs(temp - lastTemp);
139*970e1046SAndroid Build Coastguard Worker       lastTemp = temp;
140*970e1046SAndroid Build Coastguard Worker       absoluteTotalTempDelta += tempDelta;
141*970e1046SAndroid Build Coastguard Worker     }
142*970e1046SAndroid Build Coastguard Worker 
143*970e1046SAndroid Build Coastguard Worker     int hueAddend = 1;
144*970e1046SAndroid Build Coastguard Worker     double tempStep = absoluteTotalTempDelta / (double) divisions;
145*970e1046SAndroid Build Coastguard Worker     double totalTempDelta = 0.0;
146*970e1046SAndroid Build Coastguard Worker     lastTemp = getRelativeTemperature(startHct);
147*970e1046SAndroid Build Coastguard Worker     while (allColors.size() < divisions) {
148*970e1046SAndroid Build Coastguard Worker       int hue = MathUtils.sanitizeDegreesInt(startHue + hueAddend);
149*970e1046SAndroid Build Coastguard Worker       Hct hct = getHctsByHue().get(hue);
150*970e1046SAndroid Build Coastguard Worker       double temp = getRelativeTemperature(hct);
151*970e1046SAndroid Build Coastguard Worker       double tempDelta = Math.abs(temp - lastTemp);
152*970e1046SAndroid Build Coastguard Worker       totalTempDelta += tempDelta;
153*970e1046SAndroid Build Coastguard Worker 
154*970e1046SAndroid Build Coastguard Worker       double desiredTotalTempDeltaForIndex = (allColors.size() * tempStep);
155*970e1046SAndroid Build Coastguard Worker       boolean indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
156*970e1046SAndroid Build Coastguard Worker       int indexAddend = 1;
157*970e1046SAndroid Build Coastguard Worker       // Keep adding this hue to the answers until its temperature is
158*970e1046SAndroid Build Coastguard Worker       // insufficient. This ensures consistent behavior when there aren't
159*970e1046SAndroid Build Coastguard Worker       // `divisions` discrete steps between 0 and 360 in hue with `tempStep`
160*970e1046SAndroid Build Coastguard Worker       // delta in temperature between them.
161*970e1046SAndroid Build Coastguard Worker       //
162*970e1046SAndroid Build Coastguard Worker       // For example, white and black have no analogues: there are no other
163*970e1046SAndroid Build Coastguard Worker       // colors at T100/T0. Therefore, they should just be added to the array
164*970e1046SAndroid Build Coastguard Worker       // as answers.
165*970e1046SAndroid Build Coastguard Worker       while (indexSatisfied && allColors.size() < divisions) {
166*970e1046SAndroid Build Coastguard Worker         allColors.add(hct);
167*970e1046SAndroid Build Coastguard Worker         desiredTotalTempDeltaForIndex = ((allColors.size() + indexAddend) * tempStep);
168*970e1046SAndroid Build Coastguard Worker         indexSatisfied = totalTempDelta >= desiredTotalTempDeltaForIndex;
169*970e1046SAndroid Build Coastguard Worker         indexAddend++;
170*970e1046SAndroid Build Coastguard Worker       }
171*970e1046SAndroid Build Coastguard Worker       lastTemp = temp;
172*970e1046SAndroid Build Coastguard Worker       hueAddend++;
173*970e1046SAndroid Build Coastguard Worker 
174*970e1046SAndroid Build Coastguard Worker       if (hueAddend > 360) {
175*970e1046SAndroid Build Coastguard Worker         while (allColors.size() < divisions) {
176*970e1046SAndroid Build Coastguard Worker           allColors.add(hct);
177*970e1046SAndroid Build Coastguard Worker         }
178*970e1046SAndroid Build Coastguard Worker         break;
179*970e1046SAndroid Build Coastguard Worker       }
180*970e1046SAndroid Build Coastguard Worker     }
181*970e1046SAndroid Build Coastguard Worker 
182*970e1046SAndroid Build Coastguard Worker     List<Hct> answers = new ArrayList<>();
183*970e1046SAndroid Build Coastguard Worker     answers.add(input);
184*970e1046SAndroid Build Coastguard Worker 
185*970e1046SAndroid Build Coastguard Worker     int ccwCount = (int) Math.floor(((double) count - 1.0) / 2.0);
186*970e1046SAndroid Build Coastguard Worker     for (int i = 1; i < (ccwCount + 1); i++) {
187*970e1046SAndroid Build Coastguard Worker       int index = 0 - i;
188*970e1046SAndroid Build Coastguard Worker       while (index < 0) {
189*970e1046SAndroid Build Coastguard Worker         index = allColors.size() + index;
190*970e1046SAndroid Build Coastguard Worker       }
191*970e1046SAndroid Build Coastguard Worker       if (index >= allColors.size()) {
192*970e1046SAndroid Build Coastguard Worker         index = index % allColors.size();
193*970e1046SAndroid Build Coastguard Worker       }
194*970e1046SAndroid Build Coastguard Worker       answers.add(0, allColors.get(index));
195*970e1046SAndroid Build Coastguard Worker     }
196*970e1046SAndroid Build Coastguard Worker 
197*970e1046SAndroid Build Coastguard Worker     int cwCount = count - ccwCount - 1;
198*970e1046SAndroid Build Coastguard Worker     for (int i = 1; i < (cwCount + 1); i++) {
199*970e1046SAndroid Build Coastguard Worker       int index = i;
200*970e1046SAndroid Build Coastguard Worker       while (index < 0) {
201*970e1046SAndroid Build Coastguard Worker         index = allColors.size() + index;
202*970e1046SAndroid Build Coastguard Worker       }
203*970e1046SAndroid Build Coastguard Worker       if (index >= allColors.size()) {
204*970e1046SAndroid Build Coastguard Worker         index = index % allColors.size();
205*970e1046SAndroid Build Coastguard Worker       }
206*970e1046SAndroid Build Coastguard Worker       answers.add(allColors.get(index));
207*970e1046SAndroid Build Coastguard Worker     }
208*970e1046SAndroid Build Coastguard Worker 
209*970e1046SAndroid Build Coastguard Worker     return answers;
210*970e1046SAndroid Build Coastguard Worker   }
211*970e1046SAndroid Build Coastguard Worker 
212*970e1046SAndroid Build Coastguard Worker   /**
213*970e1046SAndroid Build Coastguard Worker    * Temperature relative to all colors with the same chroma and tone.
214*970e1046SAndroid Build Coastguard Worker    *
215*970e1046SAndroid Build Coastguard Worker    * @param hct HCT to find the relative temperature of.
216*970e1046SAndroid Build Coastguard Worker    * @return Value on a scale from 0 to 1.
217*970e1046SAndroid Build Coastguard Worker    */
getRelativeTemperature(Hct hct)218*970e1046SAndroid Build Coastguard Worker   public double getRelativeTemperature(Hct hct) {
219*970e1046SAndroid Build Coastguard Worker     double range = getTempsByHct().get(getWarmest()) - getTempsByHct().get(getColdest());
220*970e1046SAndroid Build Coastguard Worker     double differenceFromColdest =
221*970e1046SAndroid Build Coastguard Worker         getTempsByHct().get(hct) - getTempsByHct().get(getColdest());
222*970e1046SAndroid Build Coastguard Worker     // Handle when there's no difference in temperature between warmest and
223*970e1046SAndroid Build Coastguard Worker     // coldest: for example, at T100, only one color is available, white.
224*970e1046SAndroid Build Coastguard Worker     if (range == 0.) {
225*970e1046SAndroid Build Coastguard Worker       return 0.5;
226*970e1046SAndroid Build Coastguard Worker     }
227*970e1046SAndroid Build Coastguard Worker     return differenceFromColdest / range;
228*970e1046SAndroid Build Coastguard Worker   }
229*970e1046SAndroid Build Coastguard Worker 
230*970e1046SAndroid Build Coastguard Worker   /**
231*970e1046SAndroid Build Coastguard Worker    * Value representing cool-warm factor of a color. Values below 0 are considered cool, above,
232*970e1046SAndroid Build Coastguard Worker    * warm.
233*970e1046SAndroid Build Coastguard Worker    *
234*970e1046SAndroid Build Coastguard Worker    * <p>Color science has researched emotion and harmony, which art uses to select colors. Warm-cool
235*970e1046SAndroid Build Coastguard Worker    * is the foundation of analogous and complementary colors. See: - Li-Chen Ou's Chapter 19 in
236*970e1046SAndroid Build Coastguard Worker    * Handbook of Color Psychology (2015). - Josef Albers' Interaction of Color chapters 19 and 21.
237*970e1046SAndroid Build Coastguard Worker    *
238*970e1046SAndroid Build Coastguard Worker    * <p>Implementation of Ou, Woodcock and Wright's algorithm, which uses Lab/LCH color space.
239*970e1046SAndroid Build Coastguard Worker    * Return value has these properties:<br>
240*970e1046SAndroid Build Coastguard Worker    * - Values below 0 are cool, above 0 are warm.<br>
241*970e1046SAndroid Build Coastguard Worker    * - Lower bound: -9.66. Chroma is infinite. Assuming max of Lab chroma 130.<br>
242*970e1046SAndroid Build Coastguard Worker    * - Upper bound: 8.61. Chroma is infinite. Assuming max of Lab chroma 130.
243*970e1046SAndroid Build Coastguard Worker    */
rawTemperature(Hct color)244*970e1046SAndroid Build Coastguard Worker   public static double rawTemperature(Hct color) {
245*970e1046SAndroid Build Coastguard Worker     double[] lab = ColorUtils.labFromArgb(color.toInt());
246*970e1046SAndroid Build Coastguard Worker     double hue = MathUtils.sanitizeDegreesDouble(Math.toDegrees(Math.atan2(lab[2], lab[1])));
247*970e1046SAndroid Build Coastguard Worker     double chroma = Math.hypot(lab[1], lab[2]);
248*970e1046SAndroid Build Coastguard Worker     return -0.5
249*970e1046SAndroid Build Coastguard Worker         + 0.02
250*970e1046SAndroid Build Coastguard Worker             * Math.pow(chroma, 1.07)
251*970e1046SAndroid Build Coastguard Worker             * Math.cos(Math.toRadians(MathUtils.sanitizeDegreesDouble(hue - 50.)));
252*970e1046SAndroid Build Coastguard Worker   }
253*970e1046SAndroid Build Coastguard Worker 
254*970e1046SAndroid Build Coastguard Worker   /** Coldest color with same chroma and tone as input. */
getColdest()255*970e1046SAndroid Build Coastguard Worker   private Hct getColdest() {
256*970e1046SAndroid Build Coastguard Worker     return getHctsByTemp().get(0);
257*970e1046SAndroid Build Coastguard Worker   }
258*970e1046SAndroid Build Coastguard Worker 
259*970e1046SAndroid Build Coastguard Worker   /**
260*970e1046SAndroid Build Coastguard Worker    * HCTs for all colors with the same chroma/tone as the input.
261*970e1046SAndroid Build Coastguard Worker    *
262*970e1046SAndroid Build Coastguard Worker    * <p>Sorted by hue, ex. index 0 is hue 0.
263*970e1046SAndroid Build Coastguard Worker    */
getHctsByHue()264*970e1046SAndroid Build Coastguard Worker   private List<Hct> getHctsByHue() {
265*970e1046SAndroid Build Coastguard Worker     if (precomputedHctsByHue != null) {
266*970e1046SAndroid Build Coastguard Worker       return precomputedHctsByHue;
267*970e1046SAndroid Build Coastguard Worker     }
268*970e1046SAndroid Build Coastguard Worker     List<Hct> hcts = new ArrayList<>();
269*970e1046SAndroid Build Coastguard Worker     for (double hue = 0.; hue <= 360.; hue += 1.) {
270*970e1046SAndroid Build Coastguard Worker       Hct colorAtHue = Hct.from(hue, input.getChroma(), input.getTone());
271*970e1046SAndroid Build Coastguard Worker       hcts.add(colorAtHue);
272*970e1046SAndroid Build Coastguard Worker     }
273*970e1046SAndroid Build Coastguard Worker     precomputedHctsByHue = Collections.unmodifiableList(hcts);
274*970e1046SAndroid Build Coastguard Worker     return precomputedHctsByHue;
275*970e1046SAndroid Build Coastguard Worker   }
276*970e1046SAndroid Build Coastguard Worker 
277*970e1046SAndroid Build Coastguard Worker   /**
278*970e1046SAndroid Build Coastguard Worker    * HCTs for all colors with the same chroma/tone as the input.
279*970e1046SAndroid Build Coastguard Worker    *
280*970e1046SAndroid Build Coastguard Worker    * <p>Sorted from coldest first to warmest last.
281*970e1046SAndroid Build Coastguard Worker    */
282*970e1046SAndroid Build Coastguard Worker   // Prevent lint for Comparator not being available on Android before API level 24, 7.0, 2016.
283*970e1046SAndroid Build Coastguard Worker   // "AndroidJdkLibsChecker" for one linter, "NewApi" for another.
284*970e1046SAndroid Build Coastguard Worker   // A java_library Bazel rule with an Android constraint cannot skip these warnings without this
285*970e1046SAndroid Build Coastguard Worker   // annotation; another solution would be to create an android_library rule and supply
286*970e1046SAndroid Build Coastguard Worker   // AndroidManifest with an SDK set higher than 23.
287*970e1046SAndroid Build Coastguard Worker   @SuppressWarnings({"AndroidJdkLibsChecker", "NewApi"})
getHctsByTemp()288*970e1046SAndroid Build Coastguard Worker   private List<Hct> getHctsByTemp() {
289*970e1046SAndroid Build Coastguard Worker     if (precomputedHctsByTemp != null) {
290*970e1046SAndroid Build Coastguard Worker       return precomputedHctsByTemp;
291*970e1046SAndroid Build Coastguard Worker     }
292*970e1046SAndroid Build Coastguard Worker 
293*970e1046SAndroid Build Coastguard Worker     List<Hct> hcts = new ArrayList<>(getHctsByHue());
294*970e1046SAndroid Build Coastguard Worker     hcts.add(input);
295*970e1046SAndroid Build Coastguard Worker     Comparator<Hct> temperaturesComparator =
296*970e1046SAndroid Build Coastguard Worker         Comparator.comparing((Hct arg) -> getTempsByHct().get(arg), Double::compareTo);
297*970e1046SAndroid Build Coastguard Worker     Collections.sort(hcts, temperaturesComparator);
298*970e1046SAndroid Build Coastguard Worker     precomputedHctsByTemp = hcts;
299*970e1046SAndroid Build Coastguard Worker     return precomputedHctsByTemp;
300*970e1046SAndroid Build Coastguard Worker   }
301*970e1046SAndroid Build Coastguard Worker 
302*970e1046SAndroid Build Coastguard Worker   /** Keys of HCTs in getHctsByTemp, values of raw temperature. */
getTempsByHct()303*970e1046SAndroid Build Coastguard Worker   private Map<Hct, Double> getTempsByHct() {
304*970e1046SAndroid Build Coastguard Worker     if (precomputedTempsByHct != null) {
305*970e1046SAndroid Build Coastguard Worker       return precomputedTempsByHct;
306*970e1046SAndroid Build Coastguard Worker     }
307*970e1046SAndroid Build Coastguard Worker 
308*970e1046SAndroid Build Coastguard Worker     List<Hct> allHcts = new ArrayList<>(getHctsByHue());
309*970e1046SAndroid Build Coastguard Worker     allHcts.add(input);
310*970e1046SAndroid Build Coastguard Worker 
311*970e1046SAndroid Build Coastguard Worker     Map<Hct, Double> temperaturesByHct = new HashMap<>();
312*970e1046SAndroid Build Coastguard Worker     for (Hct hct : allHcts) {
313*970e1046SAndroid Build Coastguard Worker       temperaturesByHct.put(hct, rawTemperature(hct));
314*970e1046SAndroid Build Coastguard Worker     }
315*970e1046SAndroid Build Coastguard Worker 
316*970e1046SAndroid Build Coastguard Worker     precomputedTempsByHct = temperaturesByHct;
317*970e1046SAndroid Build Coastguard Worker     return precomputedTempsByHct;
318*970e1046SAndroid Build Coastguard Worker   }
319*970e1046SAndroid Build Coastguard Worker 
320*970e1046SAndroid Build Coastguard Worker   /** Warmest color with same chroma and tone as input. */
getWarmest()321*970e1046SAndroid Build Coastguard Worker   private Hct getWarmest() {
322*970e1046SAndroid Build Coastguard Worker     return getHctsByTemp().get(getHctsByTemp().size() - 1);
323*970e1046SAndroid Build Coastguard Worker   }
324*970e1046SAndroid Build Coastguard Worker 
325*970e1046SAndroid Build Coastguard Worker   /** Determines if an angle is between two other angles, rotating clockwise. */
isBetween(double angle, double a, double b)326*970e1046SAndroid Build Coastguard Worker   private static boolean isBetween(double angle, double a, double b) {
327*970e1046SAndroid Build Coastguard Worker     if (a < b) {
328*970e1046SAndroid Build Coastguard Worker       return a <= angle && angle <= b;
329*970e1046SAndroid Build Coastguard Worker     }
330*970e1046SAndroid Build Coastguard Worker     return a <= angle || angle <= b;
331*970e1046SAndroid Build Coastguard Worker   }
332*970e1046SAndroid Build Coastguard Worker }
333