MapArea.java

/*
 * *************************************************************************************************************************************************************
 *
 * MapView: a JavaFX map renderer for tile-based servers
 * http://tidalwave.it/projects/mapview
 *
 * Copyright (C) 2024 - 2025 by Tidalwave s.a.s. (http://tidalwave.it)
 *
 * *************************************************************************************************************************************************************
 *
 * Licensed 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.
 *
 * *************************************************************************************************************************************************************
 *
 * git clone https://bitbucket.org/tidalwave/mapview-src
 * git clone https://github.com/tidalwave-it/mapview-src
 *
 * *************************************************************************************************************************************************************
 */
package it.tidalwave.mapviewer;

import jakarta.annotation.Nonnull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import static lombok.AccessLevel.PRIVATE;

/***************************************************************************************************************************************************************
 *
 * A rectangular area on the map.
 *
 * @author  Fabrizio Giudici
 *
 **************************************************************************************************************************************************************/
@RequiredArgsConstructor(access = PRIVATE) @Getter @EqualsAndHashCode
public class MapArea
  {
    private final double north;

    private final double east;

    private final double south;

    private final double west;

    /***********************************************************************************************************************************************************
     * {@return a new area}.
     * @param   north     the north limit
     * @param   east      the east limit
     * @param   south     the south limit
     * @param   west      the west limit
     * @throws            IllegalArgumentException when the values are unfeasible
     **********************************************************************************************************************************************************/
    public static MapArea of (final double north, final double east, final double south, final double west)
      {
        checkLatitude(north, "north");
        checkLatitude(south, "south");
        checkLongitude(east, "east");
        checkLongitude(west, "west");

        if (north < south)
          {
            throw new IllegalArgumentException(String.format("north (%f) must be greater on equal than south (%f)", north, south));
          }

        return new MapArea(north, east, south, west);
      }

    /***********************************************************************************************************************************************************
     * {@return the center of this area}.
     **********************************************************************************************************************************************************/
    @Nonnull
    public MapCoordinates getCenter()
      {
        var longitudeCenter = (west + east) / 2;

        if (isAcrossGreenwichAntimeridian())
          {
            longitudeCenter = longitudeCenter - 180;

            if (longitudeCenter <= -180)
              {
                longitudeCenter = 360 + longitudeCenter;
              }
          }

        return MapCoordinates.of((north + south) / 2, longitudeCenter);
      }

    /***********************************************************************************************************************************************************
     * {@return {@code true} if this area spans across the Greenwich antimeridian (180° W)}.
     **********************************************************************************************************************************************************/
    public boolean isAcrossGreenwichAntimeridian()
      {
        return east < west;
      }

    /***********************************************************************************************************************************************************
     * {@return {@code true} if this area contains the given coordinates}.
     * @param   coordinates   the coordinates
     **********************************************************************************************************************************************************/
    public boolean contains (@Nonnull final MapCoordinates coordinates)
      {
        return coordinates.latitude() <= north && coordinates.latitude() >= south
               && isAcrossGreenwichAntimeridian() ? coordinates.longitude() >= west && coordinates.longitude() <= east
                                                  :  coordinates.longitude() >= east && coordinates.longitude() <= west;
      }

    /***********************************************************************************************************************************************************
     * {@return {@code true} if this area contains the given area}.
     * @param   that   the area to compare
     **********************************************************************************************************************************************************/
    public boolean contains (@Nonnull final MapArea that)
      {
        return north >= that.north && south <= that.south && west <= that.west && east >= that.east;
      }

    /***********************************************************************************************************************************************************
     * {@inheritDoc}
     **********************************************************************************************************************************************************/
    @Override @Nonnull
    public String toString()
      {
        return String.format("(n=%.6f, e=%.6f, s=%.6f, w=%.6f)", north, east, south, west);
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    private static void checkLatitude (final double latitude, @Nonnull final String name)
      {
        if (latitude < -90 || latitude > 90)
          {
            throw new IllegalStateException("Latitude must be in range [-90, 90]: " + name + "=" + latitude);
          }
      }

    /***********************************************************************************************************************************************************
     *
     **********************************************************************************************************************************************************/
    private static void checkLongitude (final double longitude, @Nonnull final String name)
      {
        if (longitude <= -180 || longitude > 180)
          {
            throw new IllegalStateException("Longitude must be in range (-180, 180]: " + name + "=" + longitude);
          }
      }
  }