TilesCache.java

/* Copyright 2013-2022 CS GROUP
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.rugged.raster;

import java.lang.reflect.Array;

import org.hipparchus.util.FastMath;
import org.hipparchus.util.MathUtils;
import org.hipparchus.util.Precision;
import org.orekit.bodies.GeodeticPoint;
import org.orekit.rugged.errors.DumpManager;
import org.orekit.rugged.errors.RuggedException;
import org.orekit.rugged.errors.RuggedMessages;

/** Cache for Digital Elevation Model {@link Tile tiles}.
 * <p>
 * Beware, this cache is <em>not</em> thread-safe!
 * </p>
 * @param <T> Type of tiles.
 * @author Luc Maisonobe
 * @author Guylaine Prat
 */
public class TilesCache<T extends Tile> {

    /** Epsilon to test step equality in latitude and longitude. */
    private static double STEP_EQUALITY = 5 * Precision.EPSILON;

    /** Factory for empty tiles. */
    private final TileFactory<T> factory;

    /** Updater for retrieving tiles data. */
    private final TileUpdater updater;

    /**Flag to tell if the Digital Elevation Model tiles are overlapping.
     * @since 4.0 */
    private final boolean isOverlapping;

    /** Cache. */
    private final T[] tiles;

    /** Simple constructor.
     * @param factory factory for creating empty tiles
     * @param updater updater for retrieving tiles data
     * @param maxTiles maximum number of tiles stored simultaneously in the cache
     * @param isOverlappingTiles flag to tell if the DEM tiles are overlapping:
     *                          true if overlapping; false otherwise.
     */
    public TilesCache(final TileFactory<T> factory, final TileUpdater updater,
                      final int maxTiles, final boolean isOverlappingTiles) {
        this.factory       = factory;
        this.updater    = updater;
        this.isOverlapping = isOverlappingTiles;
        @SuppressWarnings("unchecked")
        final T[] array = (T[]) Array.newInstance(Tile.class, maxTiles);
        this.tiles = array;
    }

    /** Get the tile covering a ground point.
     * @param latitude ground point latitude (rad)
     * @param longitude ground point longitude (rad)
     * @return tile covering the ground point
     */
    public T getTile(final double latitude, final double longitude) {

        // Search the current (latitude, longitude) in the tiles from the cache
        for (int i = 0; i < tiles.length; ++i) {
            final T tile = tiles[i];
            if (tile != null && tile.getLocation(latitude, longitude) == Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
                // we have found the tile in the cache

                // put it on the front as it becomes the most recently used
                while (i > 0) {
                    tiles[i] = tiles[i - 1];
                    --i;
                }
                tiles[0] = tile;
                return tile;
            }
        }

        // None of the tiles in the cache covers the specified point

        // Make some room in the cache, possibly evicting the least recently used one
        // in order to add the new tile
        for (int i = tiles.length - 1; i > 0; --i) {
            tiles[i] = tiles[i - 1];
        }

        // Fully create a tile given a latitude and longitude
        final T tile = createTile(latitude, longitude);

        // At this stage the found tile must be checked (HAS_INTERPOLATION_NEIGHBORS ?)
        // taking into account if the DEM tiles are overlapping or not

        if ( !isOverlapping ) { // DEM with seamless tiles (no overlapping)

            // Check if the tile HAS INTERPOLATION NEIGHBORS (the point (latitude, longitude) is inside the tile),
            // otherwise the point (latitude, longitude) is on the edge of the tile:
            // one must create a zipper tile because tiles are not overlapping ...
            final Tile.Location pointLocation = tile.getLocation(latitude, longitude);

            // We are on the edge of the tile
            if (pointLocation != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {

                // Create a "zipper tile"
                return createZipperTile(tile, latitude, longitude, pointLocation);

            } else { // we are NOT on the edge of the tile

                tiles[0] = tile;
                return tile;

            }   // end if (location != Tile.Location.HAS_INTERPOLATION_NEIGHBORS)

        } else { // isOverlapping: DEM with overlapping tiles (according to the flag ...)

            // Check if the tile HAS INTERPOLATION NEIGHBORS (the (latitude, longitude) is inside the tile)
            if (tile.getLocation(latitude, longitude) != Tile.Location.HAS_INTERPOLATION_NEIGHBORS) {
                // this should happen only if user set up an inconsistent TileUpdater
                throw new RuggedException(RuggedMessages.TILE_WITHOUT_REQUIRED_NEIGHBORS_SELECTED,
                                          FastMath.toDegrees(latitude), FastMath.toDegrees(longitude));
            }

            tiles[0] = tile;
            return tile;

        } // end if (!isOverlapping)
    }

    /** Create a tile defines by its latitude and longitude.
     * @param latitude latitude of the desired tile (rad)
     * @param longitude longitude of the desired tile (rad)
     * @return the tile
     * @since 4.0
     */
    private T createTile(final double latitude, final double longitude) {

        // Create the tile according to the current (latitude, longitude) and retrieve its data
        final T tile = factory.createTile();

        // In case dump is asked for, suspend the dump manager as we don't need to dump anything here.
        // For instance for SRTM DEM, the user needs to read Geoid data that are not useful in the dump
        final Boolean wasSuspended = DumpManager.suspend();

        // Retrieve the tile data
        updater.updateTile(latitude, longitude, tile);

        // Resume the dump manager if necessary
        DumpManager.resume(wasSuspended);

        // Last step to fully create the tile (in order to create the MinMax kd tree)
        tile.tileUpdateCompleted();
        return tile;
    }

    /** Create a zipper tile for DEM with seamless tiles (no overlapping).
     * @param currentTile current tile
     * @param latitude ground point latitude (rad)
     * @param longitude ground point longitude (rad)
     * @param pointLocation ground point location with respect to the tile
     * @return zipper tile covering the ground point
     * @since 4.0
     */
    private T createZipperTile(final T currentTile,
                               final double latitude, final double longitude,
                               final Tile.Location pointLocation) {

        T zipperTile = null;

        // One must create a zipper tile between this tile and the neighbor tile
        // according to the ground point location
        switch (pointLocation) {

            case NORTH: // pointLocation such as latitudeIndex > latitudeRows - 2

                zipperTile = createZipperNorthOrSouth(EarthHemisphere.NORTH, longitude, currentTile);
                break;

            case SOUTH: // pointLocation such as latitudeIndex < 0

                zipperTile = createZipperNorthOrSouth(EarthHemisphere.SOUTH, longitude, currentTile);
                break;

            case WEST: // pointLocation such as longitudeIndex < 0

                zipperTile = createZipperWestOrEast(EarthHemisphere.WEST, latitude, currentTile);
                break;

            case EAST: // pointLocation such as longitudeIndex > longitudeColumns - 2

                zipperTile = createZipperWestOrEast(EarthHemisphere.EAST, latitude, currentTile);
                break;

                // One must create a corner zipper tile between this tile and the 3 surrounding tiles
                // according to the ground point location
            case NORTH_WEST:

                zipperTile = createCornerZipper(EarthHemisphere.NORTH, EarthHemisphere.WEST, latitude, longitude, currentTile);
                break;

            case NORTH_EAST:

                zipperTile = createCornerZipper(EarthHemisphere.NORTH, EarthHemisphere.EAST, latitude, longitude, currentTile);
                break;

            case SOUTH_WEST:

                zipperTile = createCornerZipper(EarthHemisphere.SOUTH, EarthHemisphere.WEST, latitude, longitude, currentTile);
                break;

            case SOUTH_EAST:

                zipperTile = createCornerZipper(EarthHemisphere.SOUTH, EarthHemisphere.EAST, latitude, longitude, currentTile);
                break;

            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);

        } // end switch

        // again make some room in the cache, possibly evicting the 2nd least recently used one
        for (int i = tiles.length - 1; i > 0; --i) {
            tiles[i] = tiles[i - 1];
        }

        tiles[1] = currentTile;
        tiles[0] = zipperTile;

        return (T) zipperTile;
    }

    /** Initialize the zipper tile for a given geometry and the full set of elevations.
     * @param zipperLatMin zipper min latitude (ra)
     * @param zipperLonMin zipper min longitude (rad)
     * @param zipperLatStep zipper latitude step (rad)
     * @param zipperLonStep zipper longitude step (rad)
     * @param zipperLatRows zipper latitude rows
     * @param zipperLonCols zipper longitude columns
     * @param zipperElevations zipper elevations
     * @return the zipper tile
     * @since 4.0
     */
    private T initializeZipperTile(final double zipperLatMin, final double zipperLonMin,
                                   final double zipperLatStep, final double zipperLonStep,
                                   final int zipperLatRows, final int zipperLonCols,
                                   final double[][] zipperElevations) {

        // Create an empty tile
        final T zipperTile = factory.createTile();

        // Set the tile geometry
        zipperTile.setGeometry(zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep,
                               zipperLatRows, zipperLonCols);

        // Fill in the tile with the relevant elevations
        for (int iLat = 0; iLat < zipperLatRows; iLat++) {
            for (int jLon = 0; jLon < zipperLonCols; jLon++) {
                zipperTile.setElevation(iLat, jLon, zipperElevations[iLat][jLon]);
            }
        }

        // Last step in order to create the MinMax kd tree
        zipperTile.tileUpdateCompleted();

        return zipperTile;
    }

    /** Create the zipper tile between the current tile and the Northern or Southern tile of the current tile.
     * for a given longitude. The Northern or Southern tiles of the current tile may have a change in
     * resolution either in longitude or in latitude wrt the current tile.
     * The zipper has 4 rows in latitude.
     * @param latitudeHemisphere hemisphere for latitude: NORTH / SOUTH
     * @param longitude given longitude (rad)
     * @param currentTile current tile
     * @return Northern or Southern zipper tile of the current tile (according to latitudeHemisphere)
     * @since 4.0
     */
    private T createZipperNorthOrSouth(final EarthHemisphere latitudeHemisphere, final double longitude, final T currentTile) {

        final int currentTileLatRows = currentTile.getLatitudeRows();
        final int currentTileLonCols = currentTile.getLongitudeColumns();
        final double currentTileLatStep = currentTile.getLatitudeStep();
        final double currentTileLonStep = currentTile.getLongitudeStep();
        final double currentTileMinLat = currentTile.getMinimumLatitude();
        final double currentTileMinLon = currentTile.getMinimumLongitude();

        // Get the Northern or Southern tile
        final T tileNorthOrSouth = createNorthOrSouthTile(latitudeHemisphere, longitude, currentTileMinLat, currentTileLatStep, currentTileLatRows);

        // If NORTH hemisphere:
        // Create zipper tile between the current tile and the Northern tile;
        // 2 rows belong to the North part of the current tile
        // 2 rows belong to the South part of the Northern tile

        // If SOUTH hemisphere:
        // Create zipper tile between the current tile and the Southern tile;
        // 2 rows belong to the South part of the current tile
        // 2 rows belong to the North part of the Southern tile

        // Initialize the zipper latitude step and number of rows (4)
        final int zipperLatRows = 4;
        double zipperLatStep = currentTileLatStep;

        // Check if latitude steps are the same between the current tile and the Northern or Southern tile
        boolean isSameStepLat = true;
        final double northSouthLatitudeStep = tileNorthOrSouth.getLatitudeStep();

        if (!(Math.abs(currentTileLatStep - northSouthLatitudeStep) < STEP_EQUALITY)) {
            // Steps are not the same
            isSameStepLat = false;
            // Recompute zipper latitude step if the North or South tile latitude step is smaller
            if (northSouthLatitudeStep < currentTileLatStep) {
                zipperLatStep = northSouthLatitudeStep;
            }
        }

        // Initialize the zipper longitude step and number of columns with current tile step and columns
        double zipperLonStep = currentTileLonStep;
        int zipperLonCols = currentTileLonCols;

        // Check if longitude steps are the same
        boolean isSameStepLon = true;
        final double northSouthLongitudeStep = tileNorthOrSouth.getLongitudeStep();

        if (!(Math.abs(currentTileLonStep - northSouthLongitudeStep) < STEP_EQUALITY)) {
            // Steps are not the same
            isSameStepLon = false;
            // Recompute zipper longitude step and columns if the North or South tile longitude step is smaller
            if (northSouthLongitudeStep < currentTileLonStep) {
                zipperLonStep = northSouthLongitudeStep;
                zipperLonCols = tileNorthOrSouth.getLongitudeColumns();
            }
        }

        final double zipperLatMin;
        final double zipperLonMin;
        final double[][] elevations;

        switch (latitudeHemisphere) {
            case NORTH:
                // Defines the zipper min latitude (center of the cell) wrt the current tile Northern latitude
                // Explanation: we want the 2 first rows belongs to the current tile and 2 last rows to the Northern tile
                //    If zipperLatStep = currentTileLatStep, the 2 first rows = exactly lines of the current tile
                //    otherwise (currentTileLatStep > North tile step), the 2 first rows will be smaller (in latitude) than the 2 lines of the current tile
                final double currentTileNorthernLatitude = currentTileMinLat - 0.5 * currentTileLatStep + currentTileLatRows * currentTileLatStep;
                // Zipper min latitude = center of the Southern zipper cell in latitude
                // In case of resolution change zipperLatStep may be different from currentTileLatStep
                zipperLatMin = currentTileNorthernLatitude - 2 * zipperLatStep + 0.5 * zipperLatStep;

                // Define the zipper min longitude = current tile min longitude
                zipperLonMin = currentTileMinLon;

                // Fill in the zipper elevations
                elevations = getZipperNorthSouthElevations(zipperLonCols, tileNorthOrSouth, currentTile,
                                                           isSameStepLat, isSameStepLon,
                                                           zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep);
                break;

            case SOUTH:
                // Defines the zipper min latitude wrt the current tile Southern latitude
                // Explanation: we want the 2 last rows belongs to the current tile and 2 first rows to the Southern tile
                //    If zipperLatStep = currentTileLatStep, the 2 last rows = exactly lines of the current tile
                //    otherwise (currentTileLatStep > South tile step), the 2 last rows will be smaller (in latitude) than the 2 lines of the current tile
                final double currentTileSouthernLatitude = currentTileMinLat - 0.5 * currentTileLatStep;
                // Zipper min latitude = center of the Southern zipper cell in latitude
                // In case of resolution change zipperLatStep may be different from currentTileLatStep
                zipperLatMin = currentTileSouthernLatitude - 2 * zipperLatStep + 0.5 * zipperLatStep;

                // Define the zipper min longitude = current tile min longitude
                zipperLonMin = currentTileMinLon;

                // Fill in the zipper elevations
                elevations = getZipperNorthSouthElevations(zipperLonCols, currentTile, tileNorthOrSouth,
                                                          isSameStepLat, isSameStepLon,
                                                          zipperLatMin, zipperLonMin, zipperLatStep, zipperLonStep);
                break;

            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        final T zipperNorthOrSouth = initializeZipperTile(zipperLatMin, zipperLonMin,
                                                          zipperLatStep, zipperLonStep,
                                                          zipperLatRows, zipperLonCols, elevations);
        return zipperNorthOrSouth;
    }

    /** Create the zipper tile between the current tile and the Western or Eastern tile of the current tile.
     * for a given latitude. The Western or Eastern tiles of the current tile have the same
     * resolution in longitude and in latitude as the current tile (for known DEMs).
     * The zipper has 4 columns in longitude.
     * @param longitudeHemisphere hemisphere for longitude: WEST / EAST
     * @param latitude given latitude (rad)
     * @param currentTile current tile
     * @return Western or Eastern zipper tile of the current tile (according to longitudeHemisphere)
     * @since 4.0
     */
    private T createZipperWestOrEast(final EarthHemisphere longitudeHemisphere, final double latitude, final T currentTile) {

        final int currentTileLatRows = currentTile.getLatitudeRows();
        final int currentTileLonCols = currentTile.getLongitudeColumns();
        final double currentTileLatStep = currentTile.getLatitudeStep();
        final double currentTileLonStep = currentTile.getLongitudeStep();
        final double currentTileMinLon = currentTile.getMinimumLongitude();

        // Get the West or East Tile
        final T tileWestOrEast = createEastOrWestTile(longitudeHemisphere, latitude, currentTileMinLon, currentTileLonStep, currentTileLonCols);

        if (!(Math.abs(currentTileLatStep - tileWestOrEast.getLatitudeStep()) < STEP_EQUALITY) ||
            !(Math.abs(currentTileLonStep - tileWestOrEast.getLongitudeStep()) < STEP_EQUALITY)) {
            // Steps are not the same.
            // Along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
            throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        // If WEST hemisphere
        // Create zipper tile between the current tile and the Western tile;
        // 2 cols belong to the West part of the current tile
        // 2 cols belong to the East part of the West tile

        // If EAST hemisphere
        // Create zipper tile between the current tile and the Eastern tile;
        // 2 cols belong to the East part of the current tile
        // 2 cols belong to the West part of the East tile

        final double zipperLonStep = currentTileLonStep;
        final int zipperLatRows = currentTileLatRows;
        final int zipperLonCols = 4;

        final double zipperLonMin;
        final double[][] elevations;

        switch (longitudeHemisphere) {
            case WEST:
                zipperLonMin = currentTileMinLon - 2 * currentTileLonStep;
                elevations = getZipperEastWestElevations(zipperLatRows, currentTile, tileWestOrEast);
                break;

            case EAST:
                zipperLonMin = currentTileMinLon + (currentTileLonCols - 2) * currentTileLonStep;
                elevations = getZipperEastWestElevations(zipperLatRows, tileWestOrEast, currentTile);
                break;

            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        final T zipperWestOrEast = initializeZipperTile(currentTile.getMinimumLatitude(), zipperLonMin,
                                                        currentTileLatStep, zipperLonStep,
                                                        zipperLatRows, zipperLonCols, elevations);
        return zipperWestOrEast;
    }

    /** Get the latitude index of a point.
     * @param latitude geodetic latitude (rad)
     * @param latitudeMin min latitude of the tile (rad)
     * @param latitudeStep latitude step of the tile (rad)
     * @param latitudeRows number of rows in latitude
     * @return latitude index (it may lie outside of the tile!)
     * @since 4.0
     */
    private int computeLatitudeIndex(final double latitude, final double latitudeMin, final double latitudeStep, final int latitudeRows) {
        // Compute the difference in latitude wrt the Southern edge latitude of the tile
        // TBN: latitude min is at the center of the Southern cell tiles.
        final double doubleLatitudeIndex =  (latitude  - (latitudeMin - 0.5 * latitudeStep))  / latitudeStep;
        return FastMath.max(0, FastMath.min(latitudeRows - 1, (int) FastMath.floor(doubleLatitudeIndex)));
    }

    /** Get the longitude index of a point.
     * @param longitude geodetic longitude (rad)
     * @param longitudeMin min longitude of the tile (rad)
     * @param longitudeStep longitude step of the tile (rad)
     * @param longitudeColumns number of columns in longitude
     * @return longitude index (it may lie outside of the tile!)
     * @since 4.0
     */
    private int computeLongitudeIndex(final double longitude, final double longitudeMin, final double longitudeStep, final int longitudeColumns) {
        // Compute the difference in longitude wrt the Western edge longitude of the tile
        // TBN: longitude min is at the center of the Western cell tiles.
        final double doubleLongitudeIndex = (longitude - (longitudeMin - 0.5 * longitudeStep)) / longitudeStep;
        return FastMath.max(0, FastMath.min(longitudeColumns - 1, (int) FastMath.floor(doubleLongitudeIndex)));
    }

    /** Get the elevations for the zipper tile between a northern and a southern tiles with 4 rows in latitude.
     * @param zipperLonCols number of column in longitude
     * @param northernTile the tile which is the northern
     * @param southernTile the tile which is the southern
     * @param isSameStepLat flag to tell is latitude steps are the same (= true)
     * @param isSameStepLon flag to tell is longitude steps are the same (= true)
     * @param zipperLatMin zipper tile min latitude (rad)
     * @param zipperLonMin zipper tile min longitude (rad)
     * @param zipperLatStep zipper tile latitude step (rad)
     * @param zipperLonStep zipper tile longitude step (rad)
     * @return the elevations to fill in the zipper tile between a northern and a southern tiles
     * @since 4.0
     */
    private double[][] getZipperNorthSouthElevations(final int zipperLonCols,
                                                     final T northernTile, final T southernTile,
                                                     final boolean isSameStepLat, final boolean isSameStepLon,
                                                     final double zipperLatMin, final double zipperLonMin,
                                                     final double zipperLatStep, final double zipperLonStep) {

        final double[][] elevations = new double[4][zipperLonCols];

        if (isSameStepLat && isSameStepLon) { // tiles with same steps in latitude and longitude

            for (int jLon = 0; jLon < zipperLonCols; jLon++) {
                // Part from the northern tile
                final int lat3 = 1;
                elevations[3][jLon] = northernTile.getElevationAtIndices(lat3, jLon);
                final int lat2 = 0;
                elevations[2][jLon] = northernTile.getElevationAtIndices(lat2, jLon);

                // Part from the southern tile
                final int lat1 = southernTile.getLatitudeRows() - 1;
                elevations[1][jLon] = southernTile.getElevationAtIndices(lat1, jLon);
                final int lat0 = southernTile.getLatitudeRows() - 2;
                elevations[0][jLon] = southernTile.getElevationAtIndices(lat0, jLon);
            }

        } else { // tiles with different steps

            // To cover every cases and as zipper with such characteristics are rare,
            // we will use conversion from (latitude, longitude) to tile indices

            // We assure to be inside a cell by adding a delta step (to avoid inappropriate index computation)
            // TBN: zipperLatStep/zipperLonStep are the smallest steps vs Northern and Southern tiles
            final double deltaLon = 0.1 * zipperLonStep;
            double zipperLonCurrent = zipperLonMin + deltaLon;

            // Assure that the longitude belongs to [-180, + 180]
            final double zipperLonMax = MathUtils.normalizeAngle(zipperLonMin + (zipperLonCols - 1) * zipperLonStep, 0.0);

            // Northern Tile
            final double northernMinLat = northernTile.getMinimumLatitude();
            final double northernLatStep = northernTile.getLatitudeStep();
            final int northernLatRows = northernTile.getLatitudeRows();
            final double northernMinLon = northernTile.getMinimumLongitude();
            final double northernLonStep = northernTile.getLongitudeStep();
            final int northernLonCols = northernTile.getLongitudeColumns();

            // Southern Tile
            final double southernMinLat = southernTile.getMinimumLatitude();
            final double southernLatStep = southernTile.getLatitudeStep();
            final int southernLatRows = southernTile.getLatitudeRows();
            final double southernMinLon = southernTile.getMinimumLongitude();
            final double southernLonStep = southernTile.getLongitudeStep();
            final int southernLonCols = southernTile.getLongitudeColumns();

            while (zipperLonCurrent <= zipperLonMax + 2 * deltaLon) {

                // Compute zipper tile longitude index
                final int zipperLonIndex = computeLongitudeIndex(zipperLonCurrent, zipperLonMin, zipperLonStep, zipperLonCols);

                // Part from the northern tile
                // ---------------------------
                // Compute northern longitude
                final int northenLongitudeIndex = computeLongitudeIndex(zipperLonCurrent, northernMinLon, northernLonStep, northernLonCols);

                final double zipperLat3 = zipperLatMin + (3 + 0.1) * zipperLatStep;
                // lat3 would be 1 if Northern latitude step smallest; could be 0 if biggest
                final int lat3Index = computeLatitudeIndex(zipperLat3, northernMinLat, northernLatStep, northernLatRows);
                elevations[3][zipperLonIndex] = northernTile.getElevationAtIndices(lat3Index, northenLongitudeIndex);

                // lat2 is 0 whatever Northern latitude step
                final int lat2Index = 0;
                elevations[2][zipperLonIndex] = northernTile.getElevationAtIndices(lat2Index, northenLongitudeIndex);

                // Part from the southern tile
                // ---------------------------
                // Compute southern longitude
                final int southernLongitudeIndex = computeLongitudeIndex(zipperLonCurrent, southernMinLon, southernLonStep, southernLonCols);

                // lat1 is Southern latitude rows - 1  whatever Southern latitude step
                final int lat1Index = southernTile.getLatitudeRows() - 1;
                elevations[1][zipperLonIndex] = southernTile.getElevationAtIndices(lat1Index, southernLongitudeIndex);

                final double zipperLat0 = zipperLatMin + 0.1 * zipperLatStep;
                // lat1 would be Southern latitude rows - 2 if Southern latitude step smallest; could be Southern latitude rows - 1 if biggest
                final int lat0Index = computeLatitudeIndex(zipperLat0, southernMinLat, southernLatStep, southernLatRows);
                elevations[0][zipperLonIndex] = southernTile.getElevationAtIndices(lat0Index, southernLongitudeIndex);

                // Next longitude
                // --------------
                zipperLonCurrent += zipperLonStep;

            } // end loop on zipperLonCurrent
        }
        return elevations;
    }

    /** Get the elevations for the zipper tile between a eastern and a western tiles with 4 columns in longitude.
     * @param zipperLatRows number of rows in latitude
     * @param easternTile the tile which is the eastern
     * @param westernTile the tile which is the western
     * @return the elevations to fill in the zipper tile between a eastern and a western tiles
     * @since 4.0
     */
    private double[][] getZipperEastWestElevations(final int zipperLatRows,
                                                   final T easternTile, final T westernTile) {

        final double[][] elevations = new double[zipperLatRows][4];

        for (int iLat = 0; iLat < zipperLatRows; iLat++) {
            // Part from the eastern tile
            final int lon3 = 1;
            elevations[iLat][3] = easternTile.getElevationAtIndices(iLat, lon3);
            final int lon2 = 0;
            elevations[iLat][2] = easternTile.getElevationAtIndices(iLat, lon2);

            // Part from the western tile
            final int lon1 = westernTile.getLongitudeColumns() - 1;
            elevations[iLat][1] = westernTile.getElevationAtIndices(iLat, lon1);
            final int lon0 = westernTile.getLongitudeColumns() - 2;
            elevations[iLat][0] = westernTile.getElevationAtIndices(iLat, lon0);
        }
        return elevations;
    }

    /** Create the corner zipper tile.
     * Hypothesis: along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
     * @param latitudeHemisphere latitude hemisphere (North or South)
     * @param longitudeHemisphere longitude hemisphere (West or East)
     * @param latitude ground point latitude (rad)
     * @param longitude ground point longitude (rad)
     * @param currentTile current tile
     * @return the corner zipper tile
     * @since 4.0
     */
    private T createCornerZipper(final EarthHemisphere latitudeHemisphere, final EarthHemisphere longitudeHemisphere,
                                 final double latitude, final double longitude, final T currentTile) {

        final int currentTileLatRows = currentTile.getLatitudeRows();
        final int currentTileLonCols = currentTile.getLongitudeColumns();
        final double currentTileLatStep = currentTile.getLatitudeStep();
        final double currentTileLonStep = currentTile.getLongitudeStep();
        final double currentTileLonMin = currentTile.getMinimumLongitude();
        final double currentTileLatMin = currentTile.getMinimumLatitude();

        final T belowLeftTile;
        final T belowRightTile;
        final T aboveLeftTile;
        final T aboveRightTile;

        switch (latitudeHemisphere) {
            case NORTH:

                switch (longitudeHemisphere) {
                    case WEST:

                        // Get the West Tile
                        final T tileWest = createEastOrWestTile(EarthHemisphere.WEST, latitude,
                                                                currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        // Get the North Tile
                        T tileNorth = createNorthOrSouthTile(EarthHemisphere.NORTH, longitude,
                                                             currentTileLatMin, currentTileLatStep, currentTileLatRows);
                        // Get the North-West Tile
                        final T tileNorthWest = createIntercardinalTile(EarthHemisphere.NORTH,
                                                                        currentTileLatMin, currentTileLatStep, currentTileLatRows,
                                                                        EarthHemisphere.WEST,
                                                                        currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        belowLeftTile = tileWest;
                        belowRightTile = currentTile;
                        aboveLeftTile = tileNorthWest;
                        aboveRightTile = tileNorth;

                        break;

                    case EAST:

                        // Get the East Tile
                        final T tileEast = createEastOrWestTile(EarthHemisphere.EAST, latitude,
                                                                currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        // Get the North Tile
                        tileNorth = createNorthOrSouthTile(EarthHemisphere.NORTH, longitude,
                                                           currentTileLatMin, currentTileLatStep, currentTileLatRows);
                        // Get the North-East Tile
                        final T tileNorthEast = createIntercardinalTile(EarthHemisphere.NORTH,
                                                                        currentTileLatMin, currentTileLatStep, currentTileLatRows,
                                                                        EarthHemisphere.EAST,
                                                                        currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        belowLeftTile = currentTile;
                        belowRightTile = tileEast;
                        aboveLeftTile = tileNorth;
                        aboveRightTile = tileNorthEast;

                        break;

                    default:
                        // impossible to reach
                        throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
                } // end switch longitudeHemisphere

                break;

            case SOUTH:

                switch (longitudeHemisphere) {
                    case WEST:

                        // Get the West Tile
                        final T tileWest = createEastOrWestTile(EarthHemisphere.WEST, latitude,
                                                                currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        // Get the South Tile
                        T tileSouth = createNorthOrSouthTile(EarthHemisphere.SOUTH, longitude,
                                                             currentTileLatMin, currentTileLatStep, currentTileLatRows);
                        // Get the South-West Tile
                        final T tileSouthhWest = createIntercardinalTile(EarthHemisphere.SOUTH,
                                                                         currentTileLatMin, currentTileLatStep, currentTileLatRows,
                                                                         EarthHemisphere.WEST,
                                                                         currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        belowLeftTile = tileSouthhWest;
                        belowRightTile = tileSouth;
                        aboveLeftTile = tileWest;
                        aboveRightTile = currentTile;

                        break;

                    case EAST:

                        // Get the East Tile
                        final T tileEast = createEastOrWestTile(EarthHemisphere.EAST, latitude,
                                                                currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        // Get the South Tile
                        tileSouth = createNorthOrSouthTile(EarthHemisphere.SOUTH, longitude,
                                                           currentTileLatMin, currentTileLatStep, currentTileLatRows);
                        // Get the South-East Tile
                        final T tileSouthhEast = createIntercardinalTile(EarthHemisphere.SOUTH,
                                                                         currentTileLatMin, currentTileLatStep, currentTileLatRows,
                                                                         EarthHemisphere.EAST,
                                                                         currentTileLonMin, currentTileLonStep, currentTileLonCols);
                        belowLeftTile = tileSouth;
                        belowRightTile = tileSouthhEast;
                        aboveLeftTile = currentTile;
                        aboveRightTile = tileEast;

                        break;

                    default:
                        // case impossible to reach
                        throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
                } // end switch longitudeHemisphere

                break;

            default:
                // case impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        } // end switch latitudeHemisphere


        // Check that tiles at same latitude have same steps in longitude and latitude
        // Along Western or Eastern edges of the tiles: no change of resolution may occurs in known DEMs.
        if (!(Math.abs(belowLeftTile.getLatitudeStep() - belowRightTile.getLatitudeStep()) < STEP_EQUALITY) ||
            !(Math.abs(belowLeftTile.getLongitudeStep() - belowRightTile.getLongitudeStep()) < STEP_EQUALITY) ||
            !(Math.abs(aboveLeftTile.getLatitudeStep() - aboveRightTile.getLatitudeStep()) < STEP_EQUALITY) ||
            !(Math.abs(aboveLeftTile.getLongitudeStep() - aboveRightTile.getLongitudeStep()) < STEP_EQUALITY)) {
            // Steps are not the same.
            throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }


        // Compute the zipper steps in latitude and longitude.
        // If steps are different, the zipper will have the smallest steps.

        // at this stage the latitude steps of the right and left tiles are the same
        final double belowLatitudeStep = belowRightTile.getLatitudeStep();
        final double aboveLatitudeStep = aboveRightTile.getLatitudeStep();

        // initialize the zipper latitude step
        double zipperLatStep = belowLatitudeStep;

        // check if latitude steps are the same between the above and below tiles
        boolean isSameStepLat = true;

        if (!(Math.abs(belowLatitudeStep - aboveLatitudeStep) < STEP_EQUALITY)) {

            // Latitude steps are not the same
            isSameStepLat = false;

            // Recompute zipper latitude step if the above tiles latitude step is smaller
            if (aboveLatitudeStep < belowLatitudeStep) {
                zipperLatStep = aboveLatitudeStep;
            }
        }

        // at this stage the longitude steps of the right and left tiles are the same
        final double belowLongitudeStep = belowRightTile.getLongitudeStep();
        final double aboveLongitudeStep = aboveRightTile.getLongitudeStep();

        // initialize the zipper longitude step
        double zipperLonStep = belowLongitudeStep;

        // check if longitude steps are the same between the above and below tiles
        boolean isSameStepLon = true;

        if (!(Math.abs(belowLongitudeStep - aboveLongitudeStep) < STEP_EQUALITY)) {

            // Longitude steps are not the same
            isSameStepLon = false;

            // Recompute zipper longitude step if the above tiles longitude step is smaller
            if (aboveLongitudeStep < belowLongitudeStep) {
                zipperLonStep = aboveLongitudeStep;
            }
        }

        // Define the zipper min latitude and min longitude.
        // Explanation:
        // We want the zipper 2 first rows belongs to the below tiles and the zipper 2 last rows to the above tiles.
        // We want the zipper 2 first columns belongs to the left tiles and the zipper 2 last columns to the right tiles.
        // We use the current tile origin AND the zipper steps to compute the zipper origin
        final GeodeticPoint zipperCorner = computeCornerZipperOrigin(zipperLatStep, zipperLonStep,
                                                                     latitudeHemisphere, currentTileLatMin,
                                                                     currentTileLatStep, currentTileLatRows,
                                                                     longitudeHemisphere, currentTileLonMin,
                                                                     currentTileLonStep, currentTileLonCols);

        // Initialize corner tile
        return initializeCornerZipperTile(zipperCorner.getLatitude(), zipperCorner.getLongitude(), zipperLatStep, zipperLonStep,
                                          belowLeftTile, aboveLeftTile, belowRightTile, aboveRightTile,
                                          isSameStepLat, isSameStepLon);

    }

    /** Initialize a corner zipper tile (with 4 rows in latitude and 4 columns in longitude).
     * @param zipperLatMin zipper min latitude (ra)
     * @param zipperLonMin zipper min longitude (rad)
     * @param zipperLatStep zipper latitude step (rad)
     * @param zipperLonStep zipper longitude step (rad)
     * @param belowLeftTile below left tile
     * @param aboveLeftTile above left tile
     * @param belowRightTile below right tile
     * @param aboveRightTile above right tile
     * @param isSameStepLat flag to tell if latitude steps are the same (true)
     * @param isSameStepLon flag to tell if longitude steps are the same (true)
     * @return corner zipper tile
     * @since 4.0
     */
    private T initializeCornerZipperTile(final double zipperLatMin, final double zipperLonMin,
                                         final double zipperLatStep, final double zipperLonStep,
                                         final T belowLeftTile, final T aboveLeftTile, final T belowRightTile, final T aboveRightTile,
                                         final boolean isSameStepLat, final boolean isSameStepLon) {

        // Defines with 4 cells from each of the 4 tiles at the corner
        final int zipperLatRows = 4;
        final int zipperLonCols = 4;
        final double[][] elevations = new double[zipperLatRows][zipperLonCols];

        if (isSameStepLat && isSameStepLon) { // tiles with same steps in latitude and longitude

            // Rows 0 and 1 of zipper:
            // 2 first cells belong to the below left tile and 2 last cells to the below right tile

            // row 0
            elevations[0][0] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 2, belowLeftTile.getLongitudeColumns() - 2);
            elevations[0][1] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 2, belowLeftTile.getLongitudeColumns() - 1);

            elevations[0][2] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 2, 0);
            elevations[0][3] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 2, 1);

            // row 1
            elevations[1][0] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 1, belowLeftTile.getLongitudeColumns() - 2);
            elevations[1][1] = belowLeftTile.getElevationAtIndices(belowLeftTile.getLatitudeRows() - 1, belowLeftTile.getLongitudeColumns() - 1);

            elevations[1][2] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 1, 0);
            elevations[1][3] = belowRightTile.getElevationAtIndices(belowRightTile.getLatitudeRows() - 1, 1);

            // Rows 2 and 3 of zipper:
            // 2 first cells belong to the above left tile and 2 last cells to the above right tile

            // row 2
            elevations[2][0] = aboveLeftTile.getElevationAtIndices(0, aboveLeftTile.getLongitudeColumns() - 2);
            elevations[2][1] = aboveLeftTile.getElevationAtIndices(0, aboveLeftTile.getLongitudeColumns() - 1);

            elevations[2][2] = aboveRightTile.getElevationAtIndices(0, 0);
            elevations[2][3] = aboveRightTile.getElevationAtIndices(0, 1);

            // row 3
            elevations[3][0] = aboveLeftTile.getElevationAtIndices(1, aboveLeftTile.getLongitudeColumns() - 2);
            elevations[3][1] = aboveLeftTile.getElevationAtIndices(1, aboveLeftTile.getLongitudeColumns() - 1);

            elevations[3][2] = aboveRightTile.getElevationAtIndices(1, 0);
            elevations[3][3] = aboveRightTile.getElevationAtIndices(1, 1);


        } else { // tiles with different steps

            // To cover every cases and as zipper with such characteristics are rare,
            // we will use conversion from (latitude, longitude) to tile indices

            // We assure to be inside a cell by adding a delta step (to avoid inappropriate index computation)
            // TBN: zipperLatStep/zipperLonStep are the smallest steps vs below and above tiles

            // Compute latitude and longitude of each zipper cells
            final double zipperLat0 = zipperLatMin + 0.1 * zipperLatStep;
            final double zipperLat1 = zipperLat0 + zipperLatStep;
            final double zipperLat2 = zipperLat1 + zipperLatStep;
            final double zipperLat3 = zipperLat2 + zipperLatStep;

            final double zipperLon0 = zipperLonMin + 0.1 * zipperLonStep;
            final double zipperLon1 = zipperLon0 + zipperLonStep;
            final double zipperLon2 = zipperLon1 + zipperLonStep;
            final double zipperLon3 = zipperLon2 + zipperLonStep;

            // Compute the tiles index in latitude
            final int belowLeftLatitudeIndex0 = computeLatitudeIndex(zipperLat0, belowLeftTile.getMinimumLatitude(), belowLeftTile.getLatitudeStep(), belowLeftTile.getLatitudeRows());
            final int belowLeftLatitudeIndex1 = computeLatitudeIndex(zipperLat1, belowLeftTile.getMinimumLatitude(), belowLeftTile.getLatitudeStep(), belowLeftTile.getLatitudeRows());

            final int belowRightLatitudeIndex0 = computeLatitudeIndex(zipperLat0, belowRightTile.getMinimumLatitude(), belowRightTile.getLatitudeStep(), belowRightTile.getLatitudeRows());
            final int belowRightLatitudeIndex1 = computeLatitudeIndex(zipperLat1, belowRightTile.getMinimumLatitude(), belowRightTile.getLatitudeStep(), belowRightTile.getLatitudeRows());

            final int aboveLeftLatitudeIndex2 = computeLatitudeIndex(zipperLat2, aboveLeftTile.getMinimumLatitude(), aboveLeftTile.getLatitudeStep(), aboveLeftTile.getLatitudeRows());
            final int aboveLeftLatitudeIndex3 = computeLatitudeIndex(zipperLat3, aboveLeftTile.getMinimumLatitude(), aboveLeftTile.getLatitudeStep(), aboveLeftTile.getLatitudeRows());

            final int aboveRightLatitudeIndex2 = computeLatitudeIndex(zipperLat2, aboveRightTile.getMinimumLatitude(), aboveRightTile.getLatitudeStep(), aboveRightTile.getLatitudeRows());
            final int aboveRightLatitudeIndex3 = computeLatitudeIndex(zipperLat3, aboveRightTile.getMinimumLatitude(), aboveRightTile.getLatitudeStep(), aboveRightTile.getLatitudeRows());

            // Compute the tiles index in longitude
            final int belowLeftLongitudeIndex0 = computeLongitudeIndex(zipperLon0, belowLeftTile.getMinimumLongitude(), belowLeftTile.getLongitudeStep(), belowLeftTile.getLongitudeColumns());
            final int belowLeftLongitudeIndex1 = computeLongitudeIndex(zipperLon1, belowLeftTile.getMinimumLongitude(), belowLeftTile.getLongitudeStep(), belowLeftTile.getLongitudeColumns());

            final int belowRightLongitudeIndex2 = computeLongitudeIndex(zipperLon2, belowRightTile.getMinimumLongitude(), belowRightTile.getLongitudeStep(), belowRightTile.getLongitudeColumns());
            final int belowRightLongitudeIndex3 = computeLongitudeIndex(zipperLon3, belowRightTile.getMinimumLongitude(), belowRightTile.getLongitudeStep(), belowRightTile.getLongitudeColumns());

            final int aboveLeftLongitudeIndex0 = computeLongitudeIndex(zipperLon0, aboveLeftTile.getMinimumLongitude(), aboveLeftTile.getLongitudeStep(), aboveLeftTile.getLongitudeColumns());
            final int aboveLeftLongitudeIndex1 = computeLongitudeIndex(zipperLon1, aboveLeftTile.getMinimumLongitude(), aboveLeftTile.getLongitudeStep(), aboveLeftTile.getLongitudeColumns());

            final int aboveRightLongitudeIndex2 = computeLongitudeIndex(zipperLon2, aboveRightTile.getMinimumLongitude(), aboveRightTile.getLongitudeStep(), aboveRightTile.getLongitudeColumns());
            final int aboveRightLongitudeIndex3 = computeLongitudeIndex(zipperLon3, aboveRightTile.getMinimumLongitude(), aboveRightTile.getLongitudeStep(), aboveRightTile.getLongitudeColumns());

            // Rows 0 and 1 of zipper:
            // 2 first cells belong to the below left tile and 2 last cells to the below right tile

            // row 0
            elevations[0][0] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex0, belowLeftLongitudeIndex0);
            elevations[0][1] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex0, belowLeftLongitudeIndex1);

            elevations[0][2] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex0, belowRightLongitudeIndex2);
            elevations[0][3] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex0, belowRightLongitudeIndex3);

            // row 1
            elevations[1][0] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex1, belowLeftLongitudeIndex0);
            elevations[1][1] = belowLeftTile.getElevationAtIndices(belowLeftLatitudeIndex1, belowLeftLongitudeIndex1);

            elevations[1][2] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex1, belowRightLongitudeIndex2);
            elevations[1][3] = belowRightTile.getElevationAtIndices(belowRightLatitudeIndex1, belowRightLongitudeIndex3);

            // Rows 2 and 3 of zipper:
            // 2 first cells belong to the above left tile and 2 last cells to the above right tile

            // row 2
            elevations[2][0] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex2, aboveLeftLongitudeIndex0);
            elevations[2][1] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex2, aboveLeftLongitudeIndex1);

            elevations[2][2] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex2, aboveRightLongitudeIndex2);
            elevations[2][3] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex2, aboveRightLongitudeIndex3);

            // row 3
            elevations[3][0] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex3, aboveLeftLongitudeIndex0);
            elevations[3][1] = aboveLeftTile.getElevationAtIndices(aboveLeftLatitudeIndex3, aboveLeftLongitudeIndex1);

            elevations[3][2] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex3, aboveRightLongitudeIndex2);
            elevations[3][3] = aboveRightTile.getElevationAtIndices(aboveRightLatitudeIndex3, aboveRightLongitudeIndex3);

        } // end test isSameStepLat && isSameStepLon

        // Initialize the corner zipper tile
        final T cornerZipperTile = initializeZipperTile(zipperLatMin, zipperLonMin,
                                                        zipperLatStep, zipperLonStep,
                                                        zipperLatRows, zipperLonCols, elevations);

        return cornerZipperTile;
    }

    /**
     * Create the tile in intercardinal direction of the current Tile i.e. NW, NE, SW, SE.
     * @param latitudeHemisphere hemisphere for latitude: NORTH / SOUTH
     * @param latitudeMin latitude minimum for the tile (rad)
     * @param latitudeStep latitude step (rad)
     * @param latitudeRows latitude rows
     * @param longitudeHemisphere hemisphere for longitude : WEST / EAST
     * @param longitudeMin longitude minimum for the tile (rad)
     * @param longitudeStep longitude step (rad)
     * @param longitudeCols longitude columns
     * @return the tile in intercardinal direction
     * @since 4.0
     */
    private T createIntercardinalTile(final EarthHemisphere latitudeHemisphere,
            final double latitudeMin, final double latitudeStep, final int latitudeRows,
            final EarthHemisphere longitudeHemisphere,
            final double longitudeMin, final double longitudeStep, final int longitudeCols) {

        // longitudeHemisphere = +1 : East or = -1 : West
        final int lonHemisphere;
        switch (longitudeHemisphere) {
            case EAST:
                lonHemisphere = +1;
                break;
            case WEST:
                lonHemisphere = -1;
                break;
            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        // latitudeHemisphere = +1 : North or = -1 : South
        final int latHemisphere;
        switch (latitudeHemisphere) {
            case NORTH:
                latHemisphere = +1;
                break;
            case SOUTH:
                latHemisphere = -1;
                break;
            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        final double latToGetIntercardinalTile = latitudeMin + latHemisphere * latitudeRows * latitudeStep;
        final double lonToGetIntercardinalTile = longitudeMin + lonHemisphere * longitudeCols * longitudeStep;
        final T intercardinalTile = createTile(latToGetIntercardinalTile, lonToGetIntercardinalTile);
        return intercardinalTile;
    }

    /** Compute the corner zipper tile origin (min latitude and min longitude).
     * @param zipperLatStep zipper latitude step (rad)
     * @param zipperLonStep zipper longitude step (rad)
     * @param latitudeHemisphere latitude hemisphere of the zipper vs the current tile
     * @param currentTileLatMin current tile latitude origin (rad)
     * @param currentTileLatStep current tile latitude step (rad)
     * @param currentTileLatRows current tile latitude rows
     * @param longitudeHemisphere longitude hemisphere of the zipper vs the current tile
     * @param currentTileLonMin current tile tile longitude origin (rad)
     * @param currentTileLonStep current tile longitude step (rad)
     * @param currentTileLonCols current tile longitude columns
     * @return corner zipper tile origin point
     * @since 4.0
     */
    private GeodeticPoint computeCornerZipperOrigin(final double zipperLatStep, final double zipperLonStep,
                                                    final EarthHemisphere latitudeHemisphere,
                                                    final double currentTileLatMin, final double currentTileLatStep, final int currentTileLatRows,
                                                    final EarthHemisphere longitudeHemisphere,
                                                    final double currentTileLonMin, final double currentTileLonStep, final int currentTileLonCols) {
        final double zipperLatMin;
        final double zipperLonMin;

        // Explanation:
        // We want the zipper 2 first rows belongs to the below tiles and the zipper 2 last rows to the above tiles.
        // We want the zipper 2 first columns belongs to the left tiles and the zipper 2 last columns to the right tiles.
        // We use the current tile origin AND the zipper steps to compute the zipper origin

        switch (latitudeHemisphere) {
            case NORTH:

                switch (longitudeHemisphere) {
                    case WEST:
                        zipperLatMin = currentTileLatMin + currentTileLatRows * currentTileLatStep - 2 * zipperLatStep;
                        zipperLonMin = currentTileLonMin - 2 * zipperLonStep;
                        break;

                    case EAST:
                        zipperLatMin = currentTileLatMin + currentTileLatRows * currentTileLatStep - 2 * zipperLatStep;
                        zipperLonMin = currentTileLonMin + currentTileLonCols * currentTileLonStep - 2 * zipperLonStep;
                        break;

                    default:
                        // case impossible to reach
                        throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
                } // end switch longitudeHemisphere

                break;

            case SOUTH:

                switch (longitudeHemisphere) {
                    case WEST:
                        zipperLatMin = currentTileLatMin - 2 * zipperLatStep;
                        zipperLonMin = currentTileLonMin - 2 * zipperLonStep;
                        break;

                    case EAST:
                        zipperLatMin = currentTileLatMin - 2 * zipperLatStep;
                        zipperLonMin = currentTileLonMin + currentTileLonCols * currentTileLonStep - 2 * zipperLonStep;
                        break;

                    default:
                        // case impossible to reach
                        throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
                } // end switch longitudeHemisphere

                break;

            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        } // end switch latitudeHemisphere

        return new GeodeticPoint(zipperLatMin, zipperLonMin, 0);
    }


    /** Create the Northern or Southern tile of a given tile defined by minLat, longitude, latitudeRows and latStep.
     * @param latitudeHemisphere latitude hemisphere
     * @param longitude longitude to define the tile (rad)
     * @param latitudeMin minimum latitude to define the tile (rad)
     * @param latitudeStep latitude step (rad)
     * @param latitudeRows latitude rows
     * @return North or South tile according to the Earth hemisphere
     * @since 4.0
     */
    private T createNorthOrSouthTile(final EarthHemisphere latitudeHemisphere, final double longitude,
                                     final double latitudeMin, final double latitudeStep, final int latitudeRows) {
        // hemisphere = +1 : North or = -1 : South
        final int hemisphere;
        switch (latitudeHemisphere) {
            case NORTH:
                hemisphere = +1;
                break;
            case SOUTH:
                hemisphere = -1;
                break;
            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        final double latToGetNewTile = latitudeMin + hemisphere * latitudeRows * latitudeStep;
        return createTile(latToGetNewTile, longitude);
    }


    /** Create the Eastern or Western tile of a given tile defined by latitude, minLon, longitudeCols and lonStep.
     * @param longitudeHemisphere longitude hemisphere
     * @param latitude latitude to define the tile (rad)
     * @param longitudeMin minimum longitude to define the tile (rad)
     * @param longitudeStep longitude step (rad)
     * @param longitudeCols longitude columns
     * @return East or West tile  tile according to the Earth hemisphere
     * @since 4.0
     */
    private T createEastOrWestTile(final EarthHemisphere longitudeHemisphere, final double latitude,
                                   final double longitudeMin, final double longitudeStep, final int longitudeCols) {
        // hemisphere = +1 : East or = -1 : West
        final int hemisphere;
        switch (longitudeHemisphere) {
            case EAST:
                hemisphere = +1;
                break;
            case WEST:
                hemisphere = -1;
                break;
            default:
                // impossible to reach
                throw new RuggedException(RuggedMessages.INTERNAL_ERROR);
        }

        final double lonToGetNewTile = longitudeMin + hemisphere * longitudeCols * longitudeStep;
        return createTile(latitude, lonToGetNewTile);
    }
}