1   /* Copyright 2013-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.rugged.raster;
18  
19  import java.lang.reflect.Array;
20  
21  import org.hipparchus.util.FastMath;
22  import org.hipparchus.util.MathUtils;
23  import org.hipparchus.util.Precision;
24  import org.orekit.bodies.GeodeticPoint;
25  import org.orekit.rugged.errors.DumpManager;
26  import org.orekit.rugged.errors.RuggedException;
27  import org.orekit.rugged.errors.RuggedMessages;
28  
29  /** Cache for Digital Elevation Model {@link Tile tiles}.
30   * <p>
31   * Beware, this cache is <em>not</em> thread-safe!
32   * </p>
33   * @param <T> Type of tiles.
34   * @author Luc Maisonobe
35   * @author Guylaine Prat
36   */
37  public class TilesCache<T extends Tile> {
38  
39      /** Epsilon to test step equality in latitude and longitude. */
40      private static double STEP_EQUALITY = 5 * Precision.EPSILON;
41  
42      /** Factory for empty tiles. */
43      private final TileFactory<T> factory;
44  
45      /** Updater for retrieving tiles data. */
46      private final TileUpdater updater;
47  
48      /**Flag to tell if the Digital Elevation Model tiles are overlapping.
49       * @since 4.0 */
50      private final boolean isOverlapping;
51  
52      /** Cache. */
53      private final T[] tiles;
54  
55      /** Simple constructor.
56       * @param factory factory for creating empty tiles
57       * @param updater updater for retrieving tiles data
58       * @param maxTiles maximum number of tiles stored simultaneously in the cache
59       * @param isOverlappingTiles flag to tell if the DEM tiles are overlapping:
60       *                          true if overlapping; false otherwise.
61       */
62      public TilesCache(final TileFactory<T> factory, final TileUpdater updater,
63                        final int maxTiles, final boolean isOverlappingTiles) {
64          this.factory       = factory;
65          this.updater    = updater;
66          this.isOverlapping = isOverlappingTiles;
67          @SuppressWarnings("unchecked")
68          final T[] array = (T[]) Array.newInstance(Tile.class, maxTiles);
69          this.tiles = array;
70      }
71  
72      /** Get the tile covering a ground point.
73       * @param latitude ground point latitude (rad)
74       * @param longitude ground point longitude (rad)
75       * @return tile covering the ground point
76       */
77      public T getTile(final double latitude, final double longitude) {
78  
79          // Search the current (latitude, longitude) in the tiles from the cache
80          for (int i = 0; i < tiles.length; ++i) {
81              final T tile = tiles[i];
82              if (tile != null && tile.getLocation(latitude, longitude) == Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
83                  // we have found the tile in the cache
84  
85                  // put it on the front as it becomes the most recently used
86                  while (i > 0) {
87                      tiles[i] = tiles[i - 1];
88                      --i;
89                  }
90                  tiles[0] = tile;
91                  return tile;
92              }
93          }
94  
95          // None of the tiles in the cache covers the specified point
96  
97          // Make some room in the cache, possibly evicting the least recently used one
98          // in order to add the new tile
99          for (int i = tiles.length - 1; i > 0; --i) {
100             tiles[i] = tiles[i - 1];
101         }
102 
103         // Fully create a tile given a latitude and longitude
104         final T tile = createTile(latitude, longitude);
105 
106         // At this stage the found tile must be checked (HAS_INTERPOLATION_NEIGHBORS ?)
107         // taking into account if the DEM tiles are overlapping or not
108 
109         if ( !isOverlapping ) { // DEM with seamless tiles (no overlapping)
110 
111             // Check if the tile HAS INTERPOLATION NEIGHBORS (the point (latitude, longitude) is inside the tile),
112             // otherwise the point (latitude, longitude) is on the edge of the tile:
113             // one must create a zipper tile because tiles are not overlapping ...
114             final Tile.Location pointLocation = tile.getLocation(latitude, longitude);
115 
116             // We are on the edge of the tile
117             if (pointLocation != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
118 
119                 // Create a "zipper tile"
120                 return createZipperTile(tile, latitude, longitude, pointLocation);
121 
122             } else { // we are NOT on the edge of the tile
123 
124                 tiles[0] = tile;
125                 return tile;
126 
127             }   // end if (location != Tile.Location.HAS_INTERPOLATION_NEIGHBORS)
128 
129         } else { // isOverlapping: DEM with overlapping tiles (according to the flag ...)
130 
131             // Check if the tile HAS INTERPOLATION NEIGHBORS (the (latitude, longitude) is inside the tile)
132             if (tile.getLocation(latitude, longitude) != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
133                 // this should happen only if user set up an inconsistent TileUpdater
134                 throw new RuggedException(RuggedMessages.TILE_WITHOUT_REQUIRED_NEIGHBORS_SELECTED,
135                                           FastMath.toDegrees(latitude), FastMath.toDegrees(longitude));
136             }
137 
138             tiles[0] = tile;
139             return tile;
140 
141         } // end if (!isOverlapping)
142     }
143 
144     /** Create a tile defines by its latitude and longitude.
145      * @param latitude latitude of the desired tile (rad)
146      * @param longitude longitude of the desired tile (rad)
147      * @return the tile
148      * @since 4.0
149      */
150     private T createTile(final double latitude, final double longitude) {
151 
152         // Create the tile according to the current (latitude, longitude) and retrieve its data
153         final T tile = factory.createTile();
154 
155         // In case dump is asked for, suspend the dump manager as we don't need to dump anything here.
156         // For instance for SRTM DEM, the user needs to read Geoid data that are not useful in the dump
157         final Boolean wasSuspended = DumpManager.suspend();
158 
159         // Retrieve the tile data
160         updater.updateTile(latitude, longitude, tile);
161 
162         // Resume the dump manager if necessary
163         DumpManager.resume(wasSuspended);
164 
165         // Last step to fully create the tile (in order to create the MinMax kd tree)
166         tile.tileUpdateCompleted();
167         return tile;
168     }
169 
170     /** Create a zipper tile for DEM with seamless tiles (no overlapping).
171      * @param currentTile current tile
172      * @param latitude ground point latitude (rad)
173      * @param longitude ground point longitude (rad)
174      * @param pointLocation ground point location with respect to the tile
175      * @return zipper tile covering the ground point
176      * @since 4.0
177      */
178     private T createZipperTile(final T currentTile,
179                                final double latitude, final double longitude,
180                                final Tile.Location pointLocation) {
181 
182         T zipperTile = null;
183 
184         // One must create a zipper tile between this tile and the neighbor tile
185         // according to the ground point location
186         switch (pointLocation) {
187 
188             case NORTH: // pointLocation such as latitudeIndex > latitudeRows - 2
189 
190                 zipperTile = createZipperNorthOrSouth(EarthHemisphere.NORTH, longitude, currentTile);
191                 break;
192 
193             case SOUTH: // pointLocation such as latitudeIndex < 0
194 
195                 zipperTile = createZipperNorthOrSouth(EarthHemisphere.SOUTH, longitude, currentTile);
196                 break;
197 
198             case WEST: // pointLocation such as longitudeIndex < 0
199 
200                 zipperTile = createZipperWestOrEast(EarthHemisphere.WEST, latitude, currentTile);
201                 break;
202 
203             case EAST: // pointLocation such as longitudeIndex > longitudeColumns - 2
204 
205                 zipperTile = createZipperWestOrEast(EarthHemisphere.EAST, latitude, currentTile);
206                 break;
207 
208                 // One must create a corner zipper tile between this tile and the 3 surrounding tiles
209                 // according to the ground point location
210             case NORTH_WEST:
211 
212                 zipperTile = createCornerZipper(EarthHemisphere.NORTH, EarthHemisphere.WEST, latitude, longitude, currentTile);
213                 break;
214 
215             case NORTH_EAST:
216 
217                 zipperTile = createCornerZipper(EarthHemisphere.NORTH, EarthHemisphere.EAST, latitude, longitude, currentTile);
218                 break;
219 
220             case SOUTH_WEST:
221 
222                 zipperTile = createCornerZipper(EarthHemisphere.SOUTH, EarthHemisphere.WEST, latitude, longitude, currentTile);
223                 break;
224 
225             case SOUTH_EAST:
226 
227                 zipperTile = createCornerZipper(EarthHemisphere.SOUTH, EarthHemisphere.EAST, latitude, longitude, currentTile);
228                 break;
229 
230             default:
231                 // impossible to reach
232                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
233 
234         } // end switch
235 
236         // again make some room in the cache, possibly evicting the 2nd least recently used one
237         for (int i = tiles.length - 1; i > 0; --i) {
238             tiles[i] = tiles[i - 1];
239         }
240 
241         tiles[1] = currentTile;
242         tiles[0] = zipperTile;
243 
244         return (T) zipperTile;
245     }
246 
247     /** Initialize the zipper tile for a given geometry and the full set of elevations.
248      * @param zipperLatMin zipper min latitude (ra)
249      * @param zipperLonMin zipper min longitude (rad)
250      * @param zipperLatStep zipper latitude step (rad)
251      * @param zipperLonStep zipper longitude step (rad)
252      * @param zipperLatRows zipper latitude rows
253      * @param zipperLonCols zipper longitude columns
254      * @param zipperElevations zipper elevations
255      * @return the zipper tile
256      * @since 4.0
257      */
258     private T initializeZipperTile(final double zipperLatMin, final double zipperLonMin,
259                                    final double zipperLatStep, final double zipperLonStep,
260                                    final int zipperLatRows, final int zipperLonCols,
261                                    final double[][] zipperElevations) {
262 
263         // Create an empty tile
264         final T zipperTile = factory.createTile();
265 
266         // Set the tile geometry
267         zipperTile.setGeometry(zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep,
268                                zipperLatRows, zipperLonCols);
269 
270         // Fill in the tile with the relevant elevations
271         for (int iLat = 0; iLat < zipperLatRows; iLat++) {
272             for (int jLon = 0; jLon < zipperLonCols; jLon++) {
273                 zipperTile.setElevation(iLat, jLon, zipperElevations[iLat][jLon]);
274             }
275         }
276 
277         // Last step in order to create the MinMax kd tree
278         zipperTile.tileUpdateCompleted();
279 
280         return zipperTile;
281     }
282 
283     /** Create the zipper tile between the current tile and the Northern or Southern tile of the current tile.
284      * for a given longitude. The Northern or Southern tiles of the current tile may have a change in
285      * resolution either in longitude or in latitude wrt the current tile.
286      * The zipper has 4 rows in latitude.
287      * @param latitudeHemisphere hemisphere for latitude: NORTH / SOUTH
288      * @param longitude given longitude (rad)
289      * @param currentTile current tile
290      * @return Northern or Southern zipper tile of the current tile (according to latitudeHemisphere)
291      * @since 4.0
292      */
293     private T createZipperNorthOrSouth(final EarthHemisphere latitudeHemisphere, final double longitude, final T currentTile) {
294 
295         final int currentTileLatRows = currentTile.getLatitudeRows();
296         final int currentTileLonCols = currentTile.getLongitudeColumns();
297         final double currentTileLatStep = currentTile.getLatitudeStep();
298         final double currentTileLonStep = currentTile.getLongitudeStep();
299         final double currentTileMinLat = currentTile.getMinimumLatitude();
300         final double currentTileMinLon = currentTile.getMinimumLongitude();
301 
302         // Get the Northern or Southern tile
303         final T tileNorthOrSouth = createNorthOrSouthTile(latitudeHemisphere, longitude, currentTileMinLat, currentTileLatStep, currentTileLatRows);
304 
305         // If NORTH hemisphere:
306         // Create zipper tile between the current tile and the Northern tile;
307         // 2 rows belong to the North part of the current tile
308         // 2 rows belong to the South part of the Northern tile
309 
310         // If SOUTH hemisphere:
311         // Create zipper tile between the current tile and the Southern tile;
312         // 2 rows belong to the South part of the current tile
313         // 2 rows belong to the North part of the Southern tile
314 
315         // Initialize the zipper latitude step and number of rows (4)
316         final int zipperLatRows = 4;
317         double zipperLatStep = currentTileLatStep;
318 
319         // Check if latitude steps are the same between the current tile and the Northern or Southern tile
320         boolean isSameStepLat = true;
321         final double northSouthLatitudeStep = tileNorthOrSouth.getLatitudeStep();
322 
323         if (!(Math.abs(currentTileLatStep - northSouthLatitudeStep) < STEP_EQUALITY)) {
324             // Steps are not the same
325             isSameStepLat = false;
326             // Recompute zipper latitude step if the North or South tile latitude step is smaller
327             if (northSouthLatitudeStep < currentTileLatStep) {
328                 zipperLatStep = northSouthLatitudeStep;
329             }
330         }
331 
332         // Initialize the zipper longitude step and number of columns with current tile step and columns
333         double zipperLonStep = currentTileLonStep;
334         int zipperLonCols = currentTileLonCols;
335 
336         // Check if longitude steps are the same
337         boolean isSameStepLon = true;
338         final double northSouthLongitudeStep = tileNorthOrSouth.getLongitudeStep();
339 
340         if (!(Math.abs(currentTileLonStep - northSouthLongitudeStep) < STEP_EQUALITY)) {
341             // Steps are not the same
342             isSameStepLon = false;
343             // Recompute zipper longitude step and columns if the North or South tile longitude step is smaller
344             if (northSouthLongitudeStep < currentTileLonStep) {
345                 zipperLonStep = northSouthLongitudeStep;
346                 zipperLonCols = tileNorthOrSouth.getLongitudeColumns();
347             }
348         }
349 
350         final double zipperLatMin;
351         final double zipperLonMin;
352         final double[][] elevations;
353 
354         switch (latitudeHemisphere) {
355             case NORTH:
356                 // Defines the zipper min latitude (center of the cell) wrt the current tile Northern latitude
357                 // Explanation: we want the 2 first rows belongs to the current tile and 2 last rows to the Northern tile
358                 //    If zipperLatStep = currentTileLatStep, the 2 first rows = exactly lines of the current tile
359                 //    otherwise (currentTileLatStep > North tile step), the 2 first rows will be smaller (in latitude) than the 2 lines of the current tile
360                 final double currentTileNorthernLatitude = currentTileMinLat - 0.5 * currentTileLatStep + currentTileLatRows * currentTileLatStep;
361                 // Zipper min latitude = center of the Southern zipper cell in latitude
362                 // In case of resolution change zipperLatStep may be different from currentTileLatStep
363                 zipperLatMin = currentTileNorthernLatitude - 2 * zipperLatStep + 0.5 * zipperLatStep;
364 
365                 // Define the zipper min longitude = current tile min longitude
366                 zipperLonMin = currentTileMinLon;
367 
368                 // Fill in the zipper elevations
369                 elevations = getZipperNorthSouthElevations(zipperLonCols, tileNorthOrSouth, currentTile,
370                                                            isSameStepLat, isSameStepLon,
371                                                            zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep);
372                 break;
373 
374             case SOUTH:
375                 // Defines the zipper min latitude wrt the current tile Southern latitude
376                 // Explanation: we want the 2 last rows belongs to the current tile and 2 first rows to the Southern tile
377                 //    If zipperLatStep = currentTileLatStep, the 2 last rows = exactly lines of the current tile
378                 //    otherwise (currentTileLatStep > South tile step), the 2 last rows will be smaller (in latitude) than the 2 lines of the current tile
379                 final double currentTileSouthernLatitude = currentTileMinLat - 0.5 * currentTileLatStep;
380                 // Zipper min latitude = center of the Southern zipper cell in latitude
381                 // In case of resolution change zipperLatStep may be different from currentTileLatStep
382                 zipperLatMin = currentTileSouthernLatitude - 2 * zipperLatStep + 0.5 * zipperLatStep;
383 
384                 // Define the zipper min longitude = current tile min longitude
385                 zipperLonMin = currentTileMinLon;
386 
387                 // Fill in the zipper elevations
388                 elevations = getZipperNorthSouthElevations(zipperLonCols, currentTile, tileNorthOrSouth,
389                                                           isSameStepLat, isSameStepLon,
390                                                           zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep);
391                 break;
392 
393             default:
394                 // impossible to reach
395                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
396         }
397 
398         final T zipperNorthOrSouth = initializeZipperTile(zipperLatMin, zipperLonMin,
399                                                           zipperLatStep, zipperLonStep,
400                                                           zipperLatRows, zipperLonCols, elevations);
401         return zipperNorthOrSouth;
402     }
403 
404     /** Create the zipper tile between the current tile and the Western or Eastern tile of the current tile.
405      * for a given latitude. The Western or Eastern tiles of the current tile have the same
406      * resolution in longitude and in latitude as the current tile (for known DEMs).
407      * The zipper has 4 columns in longitude.
408      * @param longitudeHemisphere hemisphere for longitude: WEST / EAST
409      * @param latitude given latitude (rad)
410      * @param currentTile current tile
411      * @return Western or Eastern zipper tile of the current tile (according to longitudeHemisphere)
412      * @since 4.0
413      */
414     private T createZipperWestOrEast(final EarthHemisphere longitudeHemisphere, final double latitude, final T currentTile) {
415 
416         final int currentTileLatRows = currentTile.getLatitudeRows();
417         final int currentTileLonCols = currentTile.getLongitudeColumns();
418         final double currentTileLatStep = currentTile.getLatitudeStep();
419         final double currentTileLonStep = currentTile.getLongitudeStep();
420         final double currentTileMinLon = currentTile.getMinimumLongitude();
421 
422         // Get the West or East Tile
423         final T tileWestOrEast = createEastOrWestTile(longitudeHemisphere, latitude, currentTileMinLon, currentTileLonStep, currentTileLonCols);
424 
425         if (!(Math.abs(currentTileLatStep - tileWestOrEast.getLatitudeStep()) < STEP_EQUALITY) ||
426             !(Math.abs(currentTileLonStep - tileWestOrEast.getLongitudeStep()) < STEP_EQUALITY)) {
427             // Steps are not the same.
428             // Along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
429             throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
430         }
431 
432         // If WEST hemisphere
433         // Create zipper tile between the current tile and the Western tile;
434         // 2 cols belong to the West part of the current tile
435         // 2 cols belong to the East part of the West tile
436 
437         // If EAST hemisphere
438         // Create zipper tile between the current tile and the Eastern tile;
439         // 2 cols belong to the East part of the current tile
440         // 2 cols belong to the West part of the East tile
441 
442         final double zipperLonStep = currentTileLonStep;
443         final int zipperLatRows = currentTileLatRows;
444         final int zipperLonCols = 4;
445 
446         final double zipperLonMin;
447         final double[][] elevations;
448 
449         switch (longitudeHemisphere) {
450             case WEST:
451                 zipperLonMin = currentTileMinLon - 2 * currentTileLonStep;
452                 elevations = getZipperEastWestElevations(zipperLatRows, currentTile, tileWestOrEast);
453                 break;
454 
455             case EAST:
456                 zipperLonMin = currentTileMinLon + (currentTileLonCols - 2) * currentTileLonStep;
457                 elevations = getZipperEastWestElevations(zipperLatRows, tileWestOrEast, currentTile);
458                 break;
459 
460             default:
461                 // impossible to reach
462                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
463         }
464 
465         final T zipperWestOrEast = initializeZipperTile(currentTile.getMinimumLatitude(), zipperLonMin,
466                                                         currentTileLatStep, zipperLonStep,
467                                                         zipperLatRows, zipperLonCols, elevations);
468         return zipperWestOrEast;
469     }
470 
471     /** Get the latitude index of a point.
472      * @param latitude geodetic latitude (rad)
473      * @param latitudeMin min latitude of the tile (rad)
474      * @param latitudeStep latitude step of the tile (rad)
475      * @param latitudeRows number of rows in latitude
476      * @return latitude index (it may lie outside of the tile!)
477      * @since 4.0
478      */
479     private int computeLatitudeIndex(final double latitude, final double latitudeMin, final double latitudeStep, final int latitudeRows) {
480         // Compute the difference in latitude wrt the Southern edge latitude of the tile
481         // TBN: latitude min is at the center of the Southern cell tiles.
482         final double doubleLatitudeIndex =  (latitude  - (latitudeMin - 0.5 * latitudeStep))  / latitudeStep;
483         return FastMath.max(0, FastMath.min(latitudeRows - 1, (int) FastMath.floor(doubleLatitudeIndex)));
484     }
485 
486     /** Get the longitude index of a point.
487      * @param longitude geodetic longitude (rad)
488      * @param longitudeMin min longitude of the tile (rad)
489      * @param longitudeStep longitude step of the tile (rad)
490      * @param longitudeColumns number of columns in longitude
491      * @return longitude index (it may lie outside of the tile!)
492      * @since 4.0
493      */
494     private int computeLongitudeIndex(final double longitude, final double longitudeMin, final double longitudeStep, final int longitudeColumns) {
495         // Compute the difference in longitude wrt the Western edge longitude of the tile
496         // TBN: longitude min is at the center of the Western cell tiles.
497         final double doubleLongitudeIndex = (longitude - (longitudeMin - 0.5 * longitudeStep)) / longitudeStep;
498         return FastMath.max(0, FastMath.min(longitudeColumns - 1, (int) FastMath.floor(doubleLongitudeIndex)));
499     }
500 
501     /** Get the elevations for the zipper tile between a northern and a southern tiles with 4 rows in latitude.
502      * @param zipperLonCols number of column in longitude
503      * @param northernTile the tile which is the northern
504      * @param southernTile the tile which is the southern
505      * @param isSameStepLat flag to tell is latitude steps are the same (= true)
506      * @param isSameStepLon flag to tell is longitude steps are the same (= true)
507      * @param zipperLatMin zipper tile min latitude (rad)
508      * @param zipperLonMin zipper tile min longitude (rad)
509      * @param zipperLatStep zipper tile latitude step (rad)
510      * @param zipperLonStep zipper tile longitude step (rad)
511      * @return the elevations to fill in the zipper tile between a northern and a southern tiles
512      * @since 4.0
513      */
514     private double[][] getZipperNorthSouthElevations(final int zipperLonCols,
515                                                      final T northernTile, final T southernTile,
516                                                      final boolean isSameStepLat, final boolean isSameStepLon,
517                                                      final double zipperLatMin, final double zipperLonMin,
518                                                      final double zipperLatStep, final double zipperLonStep) {
519 
520         final double[][] elevations = new double[4][zipperLonCols];
521 
522         if (isSameStepLat && isSameStepLon) { // tiles with same steps in latitude and longitude
523 
524             for (int jLon = 0; jLon < zipperLonCols; jLon++) {
525                 // Part from the northern tile
526                 final int lat3 = 1;
527                 elevations[3][jLon] = northernTile.getElevationAtIndices(lat3, jLon);
528                 final int lat2 = 0;
529                 elevations[2][jLon] = northernTile.getElevationAtIndices(lat2, jLon);
530 
531                 // Part from the southern tile
532                 final int lat1 = southernTile.getLatitudeRows() - 1;
533                 elevations[1][jLon] = southernTile.getElevationAtIndices(lat1, jLon);
534                 final int lat0 = southernTile.getLatitudeRows() - 2;
535                 elevations[0][jLon] = southernTile.getElevationAtIndices(lat0, jLon);
536             }
537 
538         } else { // tiles with different steps
539 
540             // To cover every cases and as zipper with such characteristics are rare,
541             // we will use conversion from (latitude, longitude) to tile indices
542 
543             // We assure to be inside a cell by adding a delta step (to avoid inappropriate index computation)
544             // TBN: zipperLatStep/zipperLonStep are the smallest steps vs Northern and Southern tiles
545             final double deltaLon = 0.1 * zipperLonStep;
546             double zipperLonCurrent = zipperLonMin + deltaLon;
547 
548             // Assure that the longitude belongs to [-180, + 180]
549             final double zipperLonMax = MathUtils.normalizeAngle(zipperLonMin + (zipperLonCols - 1) * zipperLonStep, 0.0);
550 
551             // Northern Tile
552             final double northernMinLat = northernTile.getMinimumLatitude();
553             final double northernLatStep = northernTile.getLatitudeStep();
554             final int northernLatRows = northernTile.getLatitudeRows();
555             final double northernMinLon = northernTile.getMinimumLongitude();
556             final double northernLonStep = northernTile.getLongitudeStep();
557             final int northernLonCols = northernTile.getLongitudeColumns();
558 
559             // Southern Tile
560             final double southernMinLat = southernTile.getMinimumLatitude();
561             final double southernLatStep = southernTile.getLatitudeStep();
562             final int southernLatRows = southernTile.getLatitudeRows();
563             final double southernMinLon = southernTile.getMinimumLongitude();
564             final double southernLonStep = southernTile.getLongitudeStep();
565             final int southernLonCols = southernTile.getLongitudeColumns();
566 
567             while (zipperLonCurrent <= zipperLonMax + 2 * deltaLon) {
568 
569                 // Compute zipper tile longitude index
570                 final int zipperLonIndex = computeLongitudeIndex(zipperLonCurrent, zipperLonMin, zipperLonStep, zipperLonCols);
571 
572                 // Part from the northern tile
573                 // ---------------------------
574                 // Compute northern longitude
575                 final int northenLongitudeIndex = computeLongitudeIndex(zipperLonCurrent, northernMinLon, northernLonStep, northernLonCols);
576 
577                 final double zipperLat3 = zipperLatMin + (3 + 0.1) * zipperLatStep;
578                 // lat3 would be 1 if Northern latitude step smallest; could be 0 if biggest
579                 final int lat3Index = computeLatitudeIndex(zipperLat3, northernMinLat, northernLatStep, northernLatRows);
580                 elevations[3][zipperLonIndex] = northernTile.getElevationAtIndices(lat3Index, northenLongitudeIndex);
581 
582                 // lat2 is 0 whatever Northern latitude step
583                 final int lat2Index = 0;
584                 elevations[2][zipperLonIndex] = northernTile.getElevationAtIndices(lat2Index, northenLongitudeIndex);
585 
586                 // Part from the southern tile
587                 // ---------------------------
588                 // Compute southern longitude
589                 final int southernLongitudeIndex = computeLongitudeIndex(zipperLonCurrent, southernMinLon, southernLonStep, southernLonCols);
590 
591                 // lat1 is Southern latitude rows - 1  whatever Southern latitude step
592                 final int lat1Index = southernTile.getLatitudeRows() - 1;
593                 elevations[1][zipperLonIndex] = southernTile.getElevationAtIndices(lat1Index, southernLongitudeIndex);
594 
595                 final double zipperLat0 = zipperLatMin + 0.1 * zipperLatStep;
596                 // lat1 would be Southern latitude rows - 2 if Southern latitude step smallest; could be Southern latitude rows - 1 if biggest
597                 final int lat0Index = computeLatitudeIndex(zipperLat0, southernMinLat, southernLatStep, southernLatRows);
598                 elevations[0][zipperLonIndex] = southernTile.getElevationAtIndices(lat0Index, southernLongitudeIndex);
599 
600                 // Next longitude
601                 // --------------
602                 zipperLonCurrent += zipperLonStep;
603 
604             } // end loop on zipperLonCurrent
605         }
606         return elevations;
607     }
608 
609     /** Get the elevations for the zipper tile between a eastern and a western tiles with 4 columns in longitude.
610      * @param zipperLatRows number of rows in latitude
611      * @param easternTile the tile which is the eastern
612      * @param westernTile the tile which is the western
613      * @return the elevations to fill in the zipper tile between a eastern and a western tiles
614      * @since 4.0
615      */
616     private double[][] getZipperEastWestElevations(final int zipperLatRows,
617                                                    final T easternTile, final T westernTile) {
618 
619         final double[][] elevations = new double[zipperLatRows][4];
620 
621         for (int iLat = 0; iLat < zipperLatRows; iLat++) {
622             // Part from the eastern tile
623             final int lon3 = 1;
624             elevations[iLat][3] = easternTile.getElevationAtIndices(iLat, lon3);
625             final int lon2 = 0;
626             elevations[iLat][2] = easternTile.getElevationAtIndices(iLat, lon2);
627 
628             // Part from the western tile
629             final int lon1 = westernTile.getLongitudeColumns() - 1;
630             elevations[iLat][1] = westernTile.getElevationAtIndices(iLat, lon1);
631             final int lon0 = westernTile.getLongitudeColumns() - 2;
632             elevations[iLat][0] = westernTile.getElevationAtIndices(iLat, lon0);
633         }
634         return elevations;
635     }
636 
637     /** Create the corner zipper tile.
638      * Hypothesis: along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
639      * @param latitudeHemisphere latitude hemisphere (North or South)
640      * @param longitudeHemisphere longitude hemisphere (West or East)
641      * @param latitude ground point latitude (rad)
642      * @param longitude ground point longitude (rad)
643      * @param currentTile current tile
644      * @return the corner zipper tile
645      * @since 4.0
646      */
647     private T createCornerZipper(final EarthHemisphere latitudeHemisphere, final EarthHemisphere longitudeHemisphere,
648                                  final double latitude, final double longitude, final T currentTile) {
649 
650         final int currentTileLatRows = currentTile.getLatitudeRows();
651         final int currentTileLonCols = currentTile.getLongitudeColumns();
652         final double currentTileLatStep = currentTile.getLatitudeStep();
653         final double currentTileLonStep = currentTile.getLongitudeStep();
654         final double currentTileLonMin = currentTile.getMinimumLongitude();
655         final double currentTileLatMin = currentTile.getMinimumLatitude();
656 
657         final T belowLeftTile;
658         final T belowRightTile;
659         final T aboveLeftTile;
660         final T aboveRightTile;
661 
662         switch (latitudeHemisphere) {
663             case NORTH:
664 
665                 switch (longitudeHemisphere) {
666                     case WEST:
667 
668                         // Get the West Tile
669                         final T tileWest = createEastOrWestTile(EarthHemisphere.WEST, latitude,
670                                                                 currentTileLonMin, currentTileLonStep, currentTileLonCols);
671                         // Get the North Tile
672                         T tileNorth = createNorthOrSouthTile(EarthHemisphere.NORTH, longitude,
673                                                              currentTileLatMin, currentTileLatStep, currentTileLatRows);
674                         // Get the North-West Tile
675                         final T tileNorthWest = createIntercardinalTile(EarthHemisphere.NORTH,
676                                                                         currentTileLatMin, currentTileLatStep, currentTileLatRows,
677                                                                         EarthHemisphere.WEST,
678                                                                         currentTileLonMin, currentTileLonStep, currentTileLonCols);
679                         belowLeftTile = tileWest;
680                         belowRightTile = currentTile;
681                         aboveLeftTile = tileNorthWest;
682                         aboveRightTile = tileNorth;
683 
684                         break;
685 
686                     case EAST:
687 
688                         // Get the East Tile
689                         final T tileEast = createEastOrWestTile(EarthHemisphere.EAST, latitude,
690                                                                 currentTileLonMin, currentTileLonStep, currentTileLonCols);
691                         // Get the North Tile
692                         tileNorth = createNorthOrSouthTile(EarthHemisphere.NORTH, longitude,
693                                                            currentTileLatMin, currentTileLatStep, currentTileLatRows);
694                         // Get the North-East Tile
695                         final T tileNorthEast = createIntercardinalTile(EarthHemisphere.NORTH,
696                                                                         currentTileLatMin, currentTileLatStep, currentTileLatRows,
697                                                                         EarthHemisphere.EAST,
698                                                                         currentTileLonMin, currentTileLonStep, currentTileLonCols);
699                         belowLeftTile = currentTile;
700                         belowRightTile = tileEast;
701                         aboveLeftTile = tileNorth;
702                         aboveRightTile = tileNorthEast;
703 
704                         break;
705 
706                     default:
707                         // impossible to reach
708                         throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
709                 } // end switch longitudeHemisphere
710 
711                 break;
712 
713             case SOUTH:
714 
715                 switch (longitudeHemisphere) {
716                     case WEST:
717 
718                         // Get the West Tile
719                         final T tileWest = createEastOrWestTile(EarthHemisphere.WEST, latitude,
720                                                                 currentTileLonMin, currentTileLonStep, currentTileLonCols);
721                         // Get the South Tile
722                         T tileSouth = createNorthOrSouthTile(EarthHemisphere.SOUTH, longitude,
723                                                              currentTileLatMin, currentTileLatStep, currentTileLatRows);
724                         // Get the South-West Tile
725                         final T tileSouthhWest = createIntercardinalTile(EarthHemisphere.SOUTH,
726                                                                          currentTileLatMin, currentTileLatStep, currentTileLatRows,
727                                                                          EarthHemisphere.WEST,
728                                                                          currentTileLonMin, currentTileLonStep, currentTileLonCols);
729                         belowLeftTile = tileSouthhWest;
730                         belowRightTile = tileSouth;
731                         aboveLeftTile = tileWest;
732                         aboveRightTile = currentTile;
733 
734                         break;
735 
736                     case EAST:
737 
738                         // Get the East Tile
739                         final T tileEast = createEastOrWestTile(EarthHemisphere.EAST, latitude,
740                                                                 currentTileLonMin, currentTileLonStep, currentTileLonCols);
741                         // Get the South Tile
742                         tileSouth = createNorthOrSouthTile(EarthHemisphere.SOUTH, longitude,
743                                                            currentTileLatMin, currentTileLatStep, currentTileLatRows);
744                         // Get the South-East Tile
745                         final T tileSouthhEast = createIntercardinalTile(EarthHemisphere.SOUTH,
746                                                                          currentTileLatMin, currentTileLatStep, currentTileLatRows,
747                                                                          EarthHemisphere.EAST,
748                                                                          currentTileLonMin, currentTileLonStep, currentTileLonCols);
749                         belowLeftTile = tileSouth;
750                         belowRightTile = tileSouthhEast;
751                         aboveLeftTile = currentTile;
752                         aboveRightTile = tileEast;
753 
754                         break;
755 
756                     default:
757                         // case impossible to reach
758                         throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
759                 } // end switch longitudeHemisphere
760 
761                 break;
762 
763             default:
764                 // case impossible to reach
765                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
766         } // end switch latitudeHemisphere
767 
768 
769         // Check that tiles at same latitude have same steps in longitude and latitude
770         // Along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
771         if (!(Math.abs(belowLeftTile.getLatitudeStep() - belowRightTile.getLatitudeStep()) < STEP_EQUALITY) ||
772             !(Math.abs(belowLeftTile.getLongitudeStep() - belowRightTile.getLongitudeStep()) < STEP_EQUALITY) ||
773             !(Math.abs(aboveLeftTile.getLatitudeStep() - aboveRightTile.getLatitudeStep()) < STEP_EQUALITY) ||
774             !(Math.abs(aboveLeftTile.getLongitudeStep() - aboveRightTile.getLongitudeStep()) < STEP_EQUALITY)) {
775             // Steps are not the same.
776             throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
777         }
778 
779 
780         // Compute the zipper steps in latitude and longitude.
781         // If steps are different, the zipper will have the smallest steps.
782 
783         // at this stage the latitude steps of the right and left tiles are the same
784         final double belowLatitudeStep = belowRightTile.getLatitudeStep();
785         final double aboveLatitudeStep = aboveRightTile.getLatitudeStep();
786 
787         // initialize the zipper latitude step
788         double zipperLatStep = belowLatitudeStep;
789 
790         // check if latitude steps are the same between the above and below tiles
791         boolean isSameStepLat = true;
792 
793         if (!(Math.abs(belowLatitudeStep - aboveLatitudeStep) < STEP_EQUALITY)) {
794 
795             // Latitude steps are not the same
796             isSameStepLat = false;
797 
798             // Recompute zipper latitude step if the above tiles latitude step is smaller
799             if (aboveLatitudeStep < belowLatitudeStep) {
800                 zipperLatStep = aboveLatitudeStep;
801             }
802         }
803 
804         // at this stage the longitude steps of the right and left tiles are the same
805         final double belowLongitudeStep = belowRightTile.getLongitudeStep();
806         final double aboveLongitudeStep = aboveRightTile.getLongitudeStep();
807 
808         // initialize the zipper longitude step
809         double zipperLonStep = belowLongitudeStep;
810 
811         // check if longitude steps are the same between the above and below tiles
812         boolean isSameStepLon = true;
813 
814         if (!(Math.abs(belowLongitudeStep - aboveLongitudeStep) < STEP_EQUALITY)) {
815 
816             // Longitude steps are not the same
817             isSameStepLon = false;
818 
819             // Recompute zipper longitude step if the above tiles longitude step is smaller
820             if (aboveLongitudeStep < belowLongitudeStep) {
821                 zipperLonStep = aboveLongitudeStep;
822             }
823         }
824 
825         // Define the zipper min latitude and min longitude.
826         // Explanation:
827         // We want the zipper 2 first rows belongs to the below tiles and the zipper 2 last rows to the above tiles.
828         // We want the zipper 2 first columns belongs to the left tiles and the zipper 2 last columns to the right tiles.
829         // We use the current tile origin AND the zipper steps to compute the zipper origin
830         final GeodeticPoint zipperCorner = computeCornerZipperOrigin(zipperLatStep, zipperLonStep,
831                                                                      latitudeHemisphere, currentTileLatMin,
832                                                                      currentTileLatStep, currentTileLatRows,
833                                                                      longitudeHemisphere, currentTileLonMin,
834                                                                      currentTileLonStep, currentTileLonCols);
835 
836         // Initialize corner tile
837         return initializeCornerZipperTile(zipperCorner.getLatitude(), zipperCorner.getLongitude(), zipperLatStep, zipperLonStep,
838                                           belowLeftTile, aboveLeftTile, belowRightTile, aboveRightTile,
839                                           isSameStepLat, isSameStepLon);
840 
841     }
842 
843     /** Initialize a corner zipper tile (with 4 rows in latitude and 4 columns in longitude).
844      * @param zipperLatMin zipper min latitude (ra)
845      * @param zipperLonMin zipper min longitude (rad)
846      * @param zipperLatStep zipper latitude step (rad)
847      * @param zipperLonStep zipper longitude step (rad)
848      * @param belowLeftTile below left tile
849      * @param aboveLeftTile above left tile
850      * @param belowRightTile below right tile
851      * @param aboveRightTile above right tile
852      * @param isSameStepLat flag to tell if latitude steps are the same (true)
853      * @param isSameStepLon flag to tell if longitude steps are the same (true)
854      * @return corner zipper tile
855      * @since 4.0
856      */
857     private T initializeCornerZipperTile(final double zipperLatMin, final double zipperLonMin,
858                                          final double zipperLatStep, final double zipperLonStep,
859                                          final T belowLeftTile, final T aboveLeftTile, final T belowRightTile, final T aboveRightTile,
860                                          final boolean isSameStepLat, final boolean isSameStepLon) {
861 
862         // Defines with 4 cells from each of the 4 tiles at the corner
863         final int zipperLatRows = 4;
864         final int zipperLonCols = 4;
865         final double[][] elevations = new double[zipperLatRows][zipperLonCols];
866 
867         if (isSameStepLat && isSameStepLon) { // tiles with same steps in latitude and longitude
868 
869             // Rows 0 and 1 of zipper:
870             // 2 first cells belong to the below left tile and 2 last cells to the below right tile
871 
872             // row 0
873             elevations[0][0] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 2, belowLeftTile.getLongitudeColumns() - 2);
874             elevations[0][1] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 2, belowLeftTile.getLongitudeColumns() - 1);
875 
876             elevations[0][2] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 2, 0);
877             elevations[0][3] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 2, 1);
878 
879             // row 1
880             elevations[1][0] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 1, belowLeftTile.getLongitudeColumns() - 2);
881             elevations[1][1] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 1, belowLeftTile.getLongitudeColumns() - 1);
882 
883             elevations[1][2] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 1, 0);
884             elevations[1][3] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 1, 1);
885 
886             // Rows 2 and 3 of zipper:
887             // 2 first cells belong to the above left tile and 2 last cells to the above right tile
888 
889             // row 2
890             elevations[2][0] = aboveLeftTile.getElevationAtIndices(0, aboveLeftTile.getLongitudeColumns() - 2);
891             elevations[2][1] = aboveLeftTile.getElevationAtIndices(0, aboveLeftTile.getLongitudeColumns() - 1);
892 
893             elevations[2][2] = aboveRightTile.getElevationAtIndices(0, 0);
894             elevations[2][3] = aboveRightTile.getElevationAtIndices(0, 1);
895 
896             // row 3
897             elevations[3][0] = aboveLeftTile.getElevationAtIndices(1, aboveLeftTile.getLongitudeColumns() - 2);
898             elevations[3][1] = aboveLeftTile.getElevationAtIndices(1, aboveLeftTile.getLongitudeColumns() - 1);
899 
900             elevations[3][2] = aboveRightTile.getElevationAtIndices(1, 0);
901             elevations[3][3] = aboveRightTile.getElevationAtIndices(1, 1);
902 
903 
904         } else { // tiles with different steps
905 
906             // To cover every cases and as zipper with such characteristics are rare,
907             // we will use conversion from (latitude, longitude) to tile indices
908 
909             // We assure to be inside a cell by adding a delta step (to avoid inappropriate index computation)
910             // TBN: zipperLatStep/zipperLonStep are the smallest steps vs below and above tiles
911 
912             // Compute latitude and longitude of each zipper cells
913             final double zipperLat0 = zipperLatMin + 0.1 * zipperLatStep;
914             final double zipperLat1 = zipperLat0 + zipperLatStep;
915             final double zipperLat2 = zipperLat1 + zipperLatStep;
916             final double zipperLat3 = zipperLat2 + zipperLatStep;
917 
918             final double zipperLon0 = zipperLonMin + 0.1 * zipperLonStep;
919             final double zipperLon1 = zipperLon0 + zipperLonStep;
920             final double zipperLon2 = zipperLon1 + zipperLonStep;
921             final double zipperLon3 = zipperLon2 + zipperLonStep;
922 
923             // Compute the tiles index in latitude
924             final int belowLeftLatitudeIndex0 = computeLatitudeIndex(zipperLat0, belowLeftTile.getMinimumLatitude(), belowLeftTile.getLatitudeStep(), belowLeftTile.getLatitudeRows());
925             final int belowLeftLatitudeIndex1 = computeLatitudeIndex(zipperLat1, belowLeftTile.getMinimumLatitude(), belowLeftTile.getLatitudeStep(), belowLeftTile.getLatitudeRows());
926 
927             final int belowRightLatitudeIndex0 = computeLatitudeIndex(zipperLat0, belowRightTile.getMinimumLatitude(), belowRightTile.getLatitudeStep(), belowRightTile.getLatitudeRows());
928             final int belowRightLatitudeIndex1 = computeLatitudeIndex(zipperLat1, belowRightTile.getMinimumLatitude(), belowRightTile.getLatitudeStep(), belowRightTile.getLatitudeRows());
929 
930             final int aboveLeftLatitudeIndex2 = computeLatitudeIndex(zipperLat2, aboveLeftTile.getMinimumLatitude(), aboveLeftTile.getLatitudeStep(), aboveLeftTile.getLatitudeRows());
931             final int aboveLeftLatitudeIndex3 = computeLatitudeIndex(zipperLat3, aboveLeftTile.getMinimumLatitude(), aboveLeftTile.getLatitudeStep(), aboveLeftTile.getLatitudeRows());
932 
933             final int aboveRightLatitudeIndex2 = computeLatitudeIndex(zipperLat2, aboveRightTile.getMinimumLatitude(), aboveRightTile.getLatitudeStep(), aboveRightTile.getLatitudeRows());
934             final int aboveRightLatitudeIndex3 = computeLatitudeIndex(zipperLat3, aboveRightTile.getMinimumLatitude(), aboveRightTile.getLatitudeStep(), aboveRightTile.getLatitudeRows());
935 
936             // Compute the tiles index in longitude
937             final int belowLeftLongitudeIndex0 = computeLongitudeIndex(zipperLon0, belowLeftTile.getMinimumLongitude(), belowLeftTile.getLongitudeStep(), belowLeftTile.getLongitudeColumns());
938             final int belowLeftLongitudeIndex1 = computeLongitudeIndex(zipperLon1, belowLeftTile.getMinimumLongitude(), belowLeftTile.getLongitudeStep(), belowLeftTile.getLongitudeColumns());
939 
940             final int belowRightLongitudeIndex2 = computeLongitudeIndex(zipperLon2, belowRightTile.getMinimumLongitude(), belowRightTile.getLongitudeStep(), belowRightTile.getLongitudeColumns());
941             final int belowRightLongitudeIndex3 = computeLongitudeIndex(zipperLon3, belowRightTile.getMinimumLongitude(), belowRightTile.getLongitudeStep(), belowRightTile.getLongitudeColumns());
942 
943             final int aboveLeftLongitudeIndex0 = computeLongitudeIndex(zipperLon0, aboveLeftTile.getMinimumLongitude(), aboveLeftTile.getLongitudeStep(), aboveLeftTile.getLongitudeColumns());
944             final int aboveLeftLongitudeIndex1 = computeLongitudeIndex(zipperLon1, aboveLeftTile.getMinimumLongitude(), aboveLeftTile.getLongitudeStep(), aboveLeftTile.getLongitudeColumns());
945 
946             final int aboveRightLongitudeIndex2 = computeLongitudeIndex(zipperLon2, aboveRightTile.getMinimumLongitude(), aboveRightTile.getLongitudeStep(), aboveRightTile.getLongitudeColumns());
947             final int aboveRightLongitudeIndex3 = computeLongitudeIndex(zipperLon3, aboveRightTile.getMinimumLongitude(), aboveRightTile.getLongitudeStep(), aboveRightTile.getLongitudeColumns());
948 
949             // Rows 0 and 1 of zipper:
950             // 2 first cells belong to the below left tile and 2 last cells to the below right tile
951 
952             // row 0
953             elevations[0][0] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex0, belowLeftLongitudeIndex0);
954             elevations[0][1] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex0, belowLeftLongitudeIndex1);
955 
956             elevations[0][2] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex0, belowRightLongitudeIndex2);
957             elevations[0][3] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex0, belowRightLongitudeIndex3);
958 
959             // row 1
960             elevations[1][0] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex1, belowLeftLongitudeIndex0);
961             elevations[1][1] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex1, belowLeftLongitudeIndex1);
962 
963             elevations[1][2] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex1, belowRightLongitudeIndex2);
964             elevations[1][3] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex1, belowRightLongitudeIndex3);
965 
966             // Rows 2 and 3 of zipper:
967             // 2 first cells belong to the above left tile and 2 last cells to the above right tile
968 
969             // row 2
970             elevations[2][0] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex2, aboveLeftLongitudeIndex0);
971             elevations[2][1] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex2, aboveLeftLongitudeIndex1);
972 
973             elevations[2][2] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex2, aboveRightLongitudeIndex2);
974             elevations[2][3] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex2, aboveRightLongitudeIndex3);
975 
976             // row 3
977             elevations[3][0] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex3, aboveLeftLongitudeIndex0);
978             elevations[3][1] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex3, aboveLeftLongitudeIndex1);
979 
980             elevations[3][2] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex3, aboveRightLongitudeIndex2);
981             elevations[3][3] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex3, aboveRightLongitudeIndex3);
982 
983         } // end test isSameStepLat && isSameStepLon
984 
985         // Initialize the corner zipper tile
986         final T cornerZipperTile = initializeZipperTile(zipperLatMin, zipperLonMin,
987                                                         zipperLatStep, zipperLonStep,
988                                                         zipperLatRows, zipperLonCols, elevations);
989 
990         return cornerZipperTile;
991     }
992 
993     /**
994      * Create the tile in intercardinal direction of the current Tile i.e. NW, NE, SW, SE.
995      * @param latitudeHemisphere hemisphere for latitude: NORTH / SOUTH
996      * @param latitudeMin latitude minimum for the tile (rad)
997      * @param latitudeStep latitude step (rad)
998      * @param latitudeRows latitude rows
999      * @param longitudeHemisphere hemisphere for longitude : WEST / EAST
1000      * @param longitudeMin longitude minimum for the tile (rad)
1001      * @param longitudeStep longitude step (rad)
1002      * @param longitudeCols longitude columns
1003      * @return the tile in intercardinal direction
1004      * @since 4.0
1005      */
1006     private T createIntercardinalTile(final EarthHemisphere latitudeHemisphere,
1007             final double latitudeMin, final double latitudeStep, final int latitudeRows,
1008             final EarthHemisphere longitudeHemisphere,
1009             final double longitudeMin, final double longitudeStep, final int longitudeCols) {
1010 
1011         // longitudeHemisphere = +1 : East or = -1 : West
1012         final int lonHemisphere;
1013         switch (longitudeHemisphere) {
1014             case EAST:
1015                 lonHemisphere = +1;
1016                 break;
1017             case WEST:
1018                 lonHemisphere = -1;
1019                 break;
1020             default:
1021                 // impossible to reach
1022                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1023         }
1024 
1025         // latitudeHemisphere = +1 : North or = -1 : South
1026         final int latHemisphere;
1027         switch (latitudeHemisphere) {
1028             case NORTH:
1029                 latHemisphere = +1;
1030                 break;
1031             case SOUTH:
1032                 latHemisphere = -1;
1033                 break;
1034             default:
1035                 // impossible to reach
1036                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1037         }
1038 
1039         final double latToGetIntercardinalTile = latitudeMin + latHemisphere * latitudeRows * latitudeStep;
1040         final double lonToGetIntercardinalTile = longitudeMin + lonHemisphere * longitudeCols * longitudeStep;
1041         final T intercardinalTile = createTile(latToGetIntercardinalTile, lonToGetIntercardinalTile);
1042         return intercardinalTile;
1043     }
1044 
1045     /** Compute the corner zipper tile origin (min latitude and min longitude).
1046      * @param zipperLatStep zipper latitude step (rad)
1047      * @param zipperLonStep zipper longitude step (rad)
1048      * @param latitudeHemisphere latitude hemisphere of the zipper vs the current tile
1049      * @param currentTileLatMin current tile latitude origin (rad)
1050      * @param currentTileLatStep current tile latitude step (rad)
1051      * @param currentTileLatRows current tile latitude rows
1052      * @param longitudeHemisphere longitude hemisphere of the zipper vs the current tile
1053      * @param currentTileLonMin current tile tile longitude origin (rad)
1054      * @param currentTileLonStep current tile longitude step (rad)
1055      * @param currentTileLonCols current tile longitude columns
1056      * @return corner zipper tile origin point
1057      * @since 4.0
1058      */
1059     private GeodeticPoint computeCornerZipperOrigin(final double zipperLatStep, final double zipperLonStep,
1060                                                     final EarthHemisphere latitudeHemisphere,
1061                                                     final double currentTileLatMin, final double currentTileLatStep, final int currentTileLatRows,
1062                                                     final EarthHemisphere longitudeHemisphere,
1063                                                     final double currentTileLonMin, final double currentTileLonStep, final int currentTileLonCols) {
1064         final double zipperLatMin;
1065         final double zipperLonMin;
1066 
1067         // Explanation:
1068         // We want the zipper 2 first rows belongs to the below tiles and the zipper 2 last rows to the above tiles.
1069         // We want the zipper 2 first columns belongs to the left tiles and the zipper 2 last columns to the right tiles.
1070         // We use the current tile origin AND the zipper steps to compute the zipper origin
1071 
1072         switch (latitudeHemisphere) {
1073             case NORTH:
1074 
1075                 switch (longitudeHemisphere) {
1076                     case WEST:
1077                         zipperLatMin = currentTileLatMin + currentTileLatRows * currentTileLatStep - 2 * zipperLatStep;
1078                         zipperLonMin = currentTileLonMin - 2 * zipperLonStep;
1079                         break;
1080 
1081                     case EAST:
1082                         zipperLatMin = currentTileLatMin + currentTileLatRows * currentTileLatStep - 2 * zipperLatStep;
1083                         zipperLonMin = currentTileLonMin + currentTileLonCols * currentTileLonStep - 2 * zipperLonStep;
1084                         break;
1085 
1086                     default:
1087                         // case impossible to reach
1088                         throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1089                 } // end switch longitudeHemisphere
1090 
1091                 break;
1092 
1093             case SOUTH:
1094 
1095                 switch (longitudeHemisphere) {
1096                     case WEST:
1097                         zipperLatMin = currentTileLatMin - 2 * zipperLatStep;
1098                         zipperLonMin = currentTileLonMin - 2 * zipperLonStep;
1099                         break;
1100 
1101                     case EAST:
1102                         zipperLatMin = currentTileLatMin - 2 * zipperLatStep;
1103                         zipperLonMin = currentTileLonMin + currentTileLonCols * currentTileLonStep - 2 * zipperLonStep;
1104                         break;
1105 
1106                     default:
1107                         // case impossible to reach
1108                         throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1109                 } // end switch longitudeHemisphere
1110 
1111                 break;
1112 
1113             default:
1114                 // impossible to reach
1115                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1116         } // end switch latitudeHemisphere
1117 
1118         return new GeodeticPoint(zipperLatMin, zipperLonMin, 0);
1119     }
1120 
1121 
1122     /** Create the Northern or Southern tile of a given tile defined by minLat, longitude, latitudeRows and latStep.
1123      * @param latitudeHemisphere latitude hemisphere
1124      * @param longitude longitude to define the tile (rad)
1125      * @param latitudeMin minimum latitude to define the tile (rad)
1126      * @param latitudeStep latitude step (rad)
1127      * @param latitudeRows latitude rows
1128      * @return North or South tile according to the Earth hemisphere
1129      * @since 4.0
1130      */
1131     private T createNorthOrSouthTile(final EarthHemisphere latitudeHemisphere, final double longitude,
1132                                      final double latitudeMin, final double latitudeStep, final int latitudeRows) {
1133         // hemisphere = +1 : North or = -1 : South
1134         final int hemisphere;
1135         switch (latitudeHemisphere) {
1136             case NORTH:
1137                 hemisphere = +1;
1138                 break;
1139             case SOUTH:
1140                 hemisphere = -1;
1141                 break;
1142             default:
1143                 // impossible to reach
1144                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1145         }
1146 
1147         final double latToGetNewTile = latitudeMin + hemisphere * latitudeRows * latitudeStep;
1148         return createTile(latToGetNewTile, longitude);
1149     }
1150 
1151 
1152     /** Create the Eastern or Western tile of a given tile defined by latitude, minLon, longitudeCols and lonStep.
1153      * @param longitudeHemisphere longitude hemisphere
1154      * @param latitude latitude to define the tile (rad)
1155      * @param longitudeMin minimum longitude to define the tile (rad)
1156      * @param longitudeStep longitude step (rad)
1157      * @param longitudeCols longitude columns
1158      * @return East or West tile  tile according to the Earth hemisphere
1159      * @since 4.0
1160      */
1161     private T createEastOrWestTile(final EarthHemisphere longitudeHemisphere, final double latitude,
1162                                    final double longitudeMin, final double longitudeStep, final int longitudeCols) {
1163         // hemisphere = +1 : East or = -1 : West
1164         final int hemisphere;
1165         switch (longitudeHemisphere) {
1166             case EAST:
1167                 hemisphere = +1;
1168                 break;
1169             case WEST:
1170                 hemisphere = -1;
1171                 break;
1172             default:
1173                 // impossible to reach
1174                 throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
1175         }
1176 
1177         final double lonToGetNewTile = longitudeMin + hemisphere * longitudeCols * longitudeStep;
1178         return createTile(latitude, lonToGetNewTile);
1179     }
1180 }