1   /* Copyright 2002-2026 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.function.ToDoubleFunction;
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  import org.orekit.utils.units.Unit;
31  
32  /** Container for a complete grid.
33   * @author Bryan Cazabonne
34   * @author Luc Maisonobe
35   * @since 12.1
36   */
37  class Grid {
38  
39      /** Latitude indexer. */
40      private final Indexer latitudeIndexer;
41  
42      /** Longitude indexer. */
43      private final Indexer longitudeIndexer;
44  
45      /** Grid entries. */
46      private final GridEntry[][] entries;
47  
48      /** Simple constructor.
49       * @param loadedEntries loaded entries, organized as a simple list
50       * @param name file name
51       */
52      Grid(final List<GridEntry> loadedEntries, final String name) {
53  
54          // set up indexers
55          latitudeIndexer  = new Indexer(loadedEntries, GridEntry::getLatitude, name);
56          longitudeIndexer = new Indexer(loadedEntries, GridEntry::getLongitude, name);
57  
58          // organize entries in the regular grid (with one extra column for wrapping in longitude)
59          entries = new GridEntry[latitudeIndexer.n][longitudeIndexer.n + 1];
60          for (final GridEntry entry : loadedEntries) {
61              final int ia = latitudeIndexer.closeIndex(entry.getLatitude());
62              final int io = longitudeIndexer.closeIndex(entry.getLongitude());
63              entries[ia][io] = entry;
64          }
65  
66          // wrap the grid around the Earth in longitude
67          for (int ia = 0; ia < latitudeIndexer.n; ia++) {
68              if (entries[ia][0] != null) {
69                  entries[ia][longitudeIndexer.n] = entries[ia][0].buildWrappedEntry();
70              }
71          }
72  
73          // check all regularly spaced coordinates are present in the loaded entries
74          for (final GridEntry[] row : entries) {
75              for (final GridEntry entry : row) {
76                  if (entry == null) {
77                      throw new OrekitException(OrekitMessages.IRREGULAR_OR_INCOMPLETE_GRID, name);
78                  }
79              }
80          }
81  
82      }
83  
84      /** Get index of South entries in the grid.
85       * @param latitude latitude to locate (radians)
86       * @return index of South entries in the grid
87       */
88      private int getSouthIndex(final double latitude) {
89          // make sure we have at least one point remaining on North by clipping to size - 2
90          return FastMath.min(latitudeIndexer.lowIndex(latitude), latitudeIndexer.n - 2);
91      }
92  
93      /** Get index of West entries in the grid.
94       * @param longitude longitude to locate (radians)
95       * @return index of West entries in the grid
96       */
97      private int getWestIndex(final double longitude) {
98          // we don't do clipping in longitude because we have added a column to wrap around the Earth
99          return longitudeIndexer.lowIndex(longitude);
100     }
101 
102     /** Get interpolator within a cell.
103      * @param latitude latitude of point of interest
104      * @param longitude longitude of point of interest
105      * @param altitude altitude of point of interest
106      * @param deltaRef duration since reference date
107      * @return interpolator for the cell
108      */
109     CellInterpolator getInterpolator(final double latitude, final double longitude,
110                                      final double altitude, final double deltaRef) {
111 
112         // keep longitude within grid range
113         final double normalizedLongitude =
114                         MathUtils.normalizeAngle(longitude,
115                                                  entries[0][0].getLongitude() + FastMath.PI);
116 
117         // find neighboring grid entries
118         final int southIndex = getSouthIndex(latitude);
119         final int westIndex  = getWestIndex(normalizedLongitude);
120 
121         final double coef = (deltaRef / Constants.JULIAN_YEAR) * 2 * FastMath.PI;
122         final SinCos sc1  = FastMath.sinCos(coef);
123         final SinCos sc2  = FastMath.sinCos(2.0 * coef);
124 
125         // build interpolator
126         return new CellInterpolator(latitude, normalizedLongitude,
127                                     entries[southIndex    ][westIndex    ].evaluate(sc1, sc2, altitude),
128                                     entries[southIndex    ][westIndex + 1].evaluate(sc1, sc2, altitude),
129                                     entries[southIndex + 1][westIndex    ].evaluate(sc1, sc2, altitude),
130                                     entries[southIndex + 1][westIndex + 1].evaluate(sc1, sc2, altitude));
131 
132     }
133 
134     /** Get interpolator within a cell.
135      * @param <T> type of the field elements
136      * @param latitude latitude of point of interest
137      * @param longitude longitude of point of interest
138      * @param altitude altitude of point of interest
139      * @param deltaRef duration since reference date
140      * @return interpolator for the cell
141      */
142     <T extends CalculusFieldElement<T>> FieldCellInterpolator<T> getInterpolator(final T latitude, final T longitude,
143                                                                                  final T altitude, final T deltaRef) {
144 
145         // keep longitude within grid range
146         final T normalizedLongitude =
147                         MathUtils.normalizeAngle(longitude,
148                                                  longitude.newInstance(entries[0][0].getLongitude() + FastMath.PI));
149 
150         // find neighboring grid entries
151         final int southIndex = getSouthIndex(latitude.getReal());
152         final int westIndex  = getWestIndex(normalizedLongitude.getReal());
153 
154         final T              coef = deltaRef.multiply(2 * FastMath.PI / Constants.JULIAN_YEAR);
155         final FieldSinCos<T> sc1  = FastMath.sinCos(coef);
156         final FieldSinCos<T> sc2  = FastMath.sinCos(coef.multiply(2));
157 
158          // build interpolator
159         return new FieldCellInterpolator<>(latitude, normalizedLongitude,
160                                            entries[southIndex    ][westIndex    ].evaluate(sc1, sc2, altitude),
161                                            entries[southIndex    ][westIndex + 1].evaluate(sc1, sc2, altitude),
162                                            entries[southIndex + 1][westIndex    ].evaluate(sc1, sc2, altitude),
163                                            entries[southIndex + 1][westIndex + 1].evaluate(sc1, sc2, altitude));
164 
165     }
166 
167     /** Check if grid contains all specified models.
168      * @param types models types
169      * @return true if grid contain the model
170      */
171     boolean hasModels(final SeasonalModelType... types) {
172         boolean hasAll = true;
173         for (final SeasonalModelType type : types) {
174             hasAll &= entries[0][0].hasModel(type);
175         }
176         return hasAll;
177     }
178 
179     /** Indexer for latitude/longitude.
180      * @since 14.0
181      */
182     private static class Indexer {
183 
184         /** Minimum value. */
185         private final double min;
186 
187         /** Step between values. */
188         private final double step;
189 
190         /** Number of sampling points. */
191         private final int n;
192 
193         /** Build an indexer.
194          * @param entries   all loaded entries
195          * @param extractor extractor for the coordinate we are looking for
196          * @param name      file name
197          */
198         Indexer(final List<GridEntry> entries, final ToDoubleFunction<GridEntry> extractor, final String name) {
199 
200             final double tolerance = Unit.parse("mas").toSI(1.0);
201 
202             // look for minimum and maximum grid row/column
203             double inf = Double.POSITIVE_INFINITY;
204             double sup = Double.NEGATIVE_INFINITY;
205             for (final GridEntry entry : entries) {
206                 final double coordinate = extractor.applyAsDouble(entry);
207                 inf = FastMath.min(inf, coordinate);
208                 sup = FastMath.max(sup, coordinate);
209             }
210 
211             // look for first step
212             double firstStep = Double.POSITIVE_INFINITY;
213             for (final GridEntry entry : entries) {
214                 final double delta = extractor.applyAsDouble(entry) - inf;
215                 if (delta > tolerance) {
216                     // this entry does not belong to the minimum grid row/column
217                     firstStep = FastMath.min(firstStep, delta);
218                 }
219             }
220 
221             // store grid characteristics
222             this.min  = inf;
223             this.step = firstStep;
224             this.n    = 1 + (int) FastMath.rint((sup - inf) / firstStep);
225 
226             // check regularity
227             for (final GridEntry entry : entries) {
228                 final double coordinate = extractor.applyAsDouble(entry);
229                 final double rebuilt = min + closeIndex(coordinate) * step;
230                 if (FastMath.abs(coordinate - rebuilt) > tolerance) {
231                     throw new OrekitException(OrekitMessages.IRREGULAR_OR_INCOMPLETE_GRID, name);
232                 }
233             }
234 
235         }
236 
237         /** Find index corresponding to coordinate.
238          * @param coordinate coordinate along axis
239          * @return index of grid point at or just below coordinate
240          */
241         public int lowIndex(final double coordinate) {
242             return (int) FastMath.floor((coordinate - min) / step);
243         }
244 
245         /** Find index corresponding to coordinate.
246          * @param coordinate coordinate along axis
247          * @return index of grid point closest to coordinate
248          */
249         public int closeIndex(final double coordinate) {
250             return (int) FastMath.rint((coordinate - min) / step);
251         }
252 
253     }
254 
255 }