1   /* Copyright 2002-2025 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.models.earth.weather;
18  
19  import java.util.List;
20  import java.util.SortedSet;
21  
22  import org.hipparchus.CalculusFieldElement;
23  import org.hipparchus.util.FastMath;
24  import org.hipparchus.util.FieldSinCos;
25  import org.hipparchus.util.MathUtils;
26  import org.hipparchus.util.SinCos;
27  import org.orekit.errors.OrekitException;
28  import org.orekit.errors.OrekitMessages;
29  import org.orekit.utils.Constants;
30  
31  /** Container for a complete grid.
32   * @author Bryan Cazabonne
33   * @author Luc Maisonobe
34   * @since 12.1
35   */
36  class Grid {
37  
38      /** Latitude sample. */
39      private final SortedSet<Integer> latitudeSample;
40  
41      /** Longitude sample. */
42      private final SortedSet<Integer> longitudeSample;
43  
44      /** Grid entries. */
45      private final GridEntry[][] entries;
46  
47      /** Simple constructor.
48       * @param latitudeSample latitude sample
49       * @param longitudeSample longitude sample
50       * @param loadedEntries loaded entries, organized as a simple list
51       * @param name file name
52       */
53      Grid(final SortedSet<Integer> latitudeSample, final SortedSet<Integer> longitudeSample,
54           final List<GridEntry> loadedEntries, final String name) {
55  
56          final int nA         = latitudeSample.size();
57          final int nO         = longitudeSample.size() + 1; // we add one here for wrapping the grid
58          this.entries         = new GridEntry[nA][nO];
59          this.latitudeSample  = latitudeSample;
60          this.longitudeSample = longitudeSample;
61  
62          // organize entries in the regular grid
63          for (final GridEntry entry : loadedEntries) {
64              final int latitudeIndex  = latitudeSample.headSet(entry.getLatKey() + 1).size() - 1;
65              final int longitudeIndex = longitudeSample.headSet(entry.getLonKey() + 1).size() - 1;
66              entries[latitudeIndex][longitudeIndex] = entry;
67          }
68  
69          // finalize the grid
70          for (final GridEntry[] row : entries) {
71  
72              // check for missing entries
73              for (int longitudeIndex = 0; longitudeIndex < nO - 1; ++longitudeIndex) {
74                  if (row[longitudeIndex] == null) {
75                      throw new OrekitException(OrekitMessages.IRREGULAR_OR_INCOMPLETE_GRID, name);
76                  }
77              }
78  
79              // wrap the grid around the Earth in longitude
80              row[nO - 1] = row[0].buildWrappedEntry();
81  
82          }
83  
84      }
85  
86      /** Get index of South entries in the grid.
87       * @param latitude latitude to locate (radians)
88       * @return index of South entries in the grid
89       */
90      private int getSouthIndex(final double latitude) {
91  
92          final int latKey = (int) FastMath.rint(FastMath.toDegrees(latitude) * GridEntry.DEG_TO_MAS);
93          final int index  = latitudeSample.headSet(latKey + 1).size() - 1;
94  
95          // make sure we have at least one point remaining on North by clipping to size - 2
96          return FastMath.min(index, latitudeSample.size() - 2);
97  
98      }
99  
100     /** Get index of West entries in the grid.
101      * @param longitude longitude to locate (radians)
102      * @return index of West entries in the grid
103      */
104     private int getWestIndex(final double longitude) {
105 
106         final int lonKey = (int) FastMath.rint(FastMath.toDegrees(longitude) * GridEntry.DEG_TO_MAS);
107 
108         // we don't do clipping in longitude because we have added a row to wrap around the Earth
109         return longitudeSample.headSet(lonKey + 1).size() - 1;
110 
111     }
112 
113     /** Get interpolator within a cell.
114      * @param latitude latitude of point of interest
115      * @param longitude longitude of point of interest
116      * @param altitude altitude of point of interest
117      * @param deltaRef duration since reference date
118      * @return interpolator for the cell
119      */
120     CellInterpolator getInterpolator(final double latitude, final double longitude,
121                                      final double altitude, final double deltaRef) {
122 
123         // keep longitude within grid range
124         final double normalizedLongitude =
125                         MathUtils.normalizeAngle(longitude,
126                                                  entries[0][0].getLongitude() + FastMath.PI);
127 
128         // find neighboring grid entries
129         final int southIndex = getSouthIndex(latitude);
130         final int westIndex  = getWestIndex(normalizedLongitude);
131 
132         final double coef = (deltaRef / Constants.JULIAN_YEAR) * 2 * FastMath.PI;
133         final SinCos sc1  = FastMath.sinCos(coef);
134         final SinCos sc2  = FastMath.sinCos(2.0 * coef);
135 
136         // build interpolator
137         return new CellInterpolator(latitude, normalizedLongitude,
138                                     entries[southIndex    ][westIndex    ].evaluate(sc1, sc2, altitude),
139                                     entries[southIndex    ][westIndex + 1].evaluate(sc1, sc2, altitude),
140                                     entries[southIndex + 1][westIndex    ].evaluate(sc1, sc2, altitude),
141                                     entries[southIndex + 1][westIndex + 1].evaluate(sc1, sc2, altitude));
142 
143     }
144 
145     /** Get interpolator within a cell.
146      * @param <T> type of the field elements
147      * @param latitude latitude of point of interest
148      * @param longitude longitude of point of interest
149      * @param altitude altitude of point of interest
150      * @param deltaRef duration since reference date
151      * @return interpolator for the cell
152      */
153     <T extends CalculusFieldElement<T>> FieldCellInterpolator<T> getInterpolator(final T latitude, final T longitude,
154                                                                                  final T altitude, final T deltaRef) {
155 
156         // keep longitude within grid range
157         final T normalizedLongitude =
158                         MathUtils.normalizeAngle(longitude,
159                                                  longitude.newInstance(entries[0][0].getLongitude() + FastMath.PI));
160 
161         // find neighboring grid entries
162         final int southIndex = getSouthIndex(latitude.getReal());
163         final int westIndex  = getWestIndex(normalizedLongitude.getReal());
164 
165         final T              coef = deltaRef.multiply(2 * FastMath.PI / Constants.JULIAN_YEAR);
166         final FieldSinCos<T> sc1  = FastMath.sinCos(coef);
167         final FieldSinCos<T> sc2  = FastMath.sinCos(coef.multiply(2));
168 
169          // build interpolator
170         return new FieldCellInterpolator<>(latitude, normalizedLongitude,
171                                            entries[southIndex    ][westIndex    ].evaluate(sc1, sc2, altitude),
172                                            entries[southIndex    ][westIndex + 1].evaluate(sc1, sc2, altitude),
173                                            entries[southIndex + 1][westIndex    ].evaluate(sc1, sc2, altitude),
174                                            entries[southIndex + 1][westIndex + 1].evaluate(sc1, sc2, altitude));
175 
176     }
177 
178     /** Check if grid contains all specified models.
179      * @param types models types
180      * @return true if grid contain the model
181      */
182     boolean hasModels(final SeasonalModelType... types) {
183         boolean hasAll = true;
184         for (final SeasonalModelType type : types) {
185             hasAll &= entries[0][0].hasModel(type);
186         }
187         return hasAll;
188     }
189 
190 }