All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.earth.worldwind.geom.Sector.kt Maven / Gradle / Ivy

package earth.worldwind.geom

import earth.worldwind.geom.Angle.Companion.NEG180
import earth.worldwind.geom.Angle.Companion.NEG90
import earth.worldwind.geom.Angle.Companion.POS180
import earth.worldwind.geom.Angle.Companion.POS90
import earth.worldwind.geom.Angle.Companion.ZERO
import earth.worldwind.geom.Angle.Companion.average
import earth.worldwind.geom.Angle.Companion.clampLatitude
import earth.worldwind.geom.Angle.Companion.clampLongitude
import earth.worldwind.geom.Angle.Companion.fromDegrees
import earth.worldwind.geom.Angle.Companion.fromRadians
import earth.worldwind.geom.Angle.Companion.max
import earth.worldwind.geom.Angle.Companion.min
import earth.worldwind.util.Logger.ERROR
import earth.worldwind.util.Logger.logMessage
import kotlin.jvm.JvmStatic
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor

/**
 * Geographic rectangular region.
 */
open class Sector(
    /**
     * The sector's minimum latitude.
     */
    var minLatitude: Angle,
    /**
     * The sector's maximum latitude.
     */
    var maxLatitude: Angle,
    /**
     * The sector's minimum longitude.
     */
    var minLongitude: Angle,
    /**
     * The sector's maximum longitude.
     */
    var maxLongitude: Angle
) {
    /**
     * Indicates whether this sector has no dimensions.
     */
    val isEmpty get() = minLatitude == ZERO && maxLatitude == ZERO && minLongitude == ZERO && maxLongitude == ZERO
    /**
     * Indicates whether this sector contains the full range of latitude [90 to +90] and longitude [-180 to +180].
     */
    val isFullSphere get() = minLatitude == NEG90 && maxLatitude == POS90 && minLongitude == NEG180 && maxLongitude == POS180
    /**
     * Returns the angle between this sector's minimum and maximum latitudes.
     */
    val deltaLatitude get() = maxLatitude - minLatitude
    /**
     * Returns the angle between this sector's minimum and maximum longitudes.
     */
    val deltaLongitude get() = maxLongitude - minLongitude
    /**
     * Returns the angle midway between this sector's minimum and maximum latitudes.
     */
    val centroidLatitude get() = average(minLatitude, maxLatitude)
    /**
     * Returns the angle midway between this sector's minimum and maximum longitudes.
     */
    val centroidLongitude get() = average(minLongitude, maxLongitude)

    /**
     * Constructs an empty sector with minimum and maximum latitudes and longitudes all 0.
     */
    constructor(): this(minLatitude = ZERO, maxLatitude = ZERO, minLongitude = ZERO, maxLongitude = ZERO)

    /**
     * Constructs a sector with the minimum and maximum latitudes and longitudes of a specified sector.
     *
     * @param sector the sector specifying the coordinates
     */
    constructor(sector: Sector): this(sector.minLatitude, sector.maxLatitude, sector.minLongitude, sector.maxLongitude)

    companion object {
        @JvmStatic
        fun fromDegrees(minLatDegrees: Double, minLonDegrees: Double, deltaLatDegrees: Double, deltaLonDegrees: Double): Sector {
            val maxLatDegrees = if (deltaLatDegrees > 0)
                clampLatitude(minLatDegrees + deltaLatDegrees) else minLatDegrees
            val maxLonDegrees = if (deltaLonDegrees > 0)
                clampLongitude(minLonDegrees + deltaLonDegrees) else minLonDegrees
            return Sector(
                fromDegrees(minLatDegrees), fromDegrees(maxLatDegrees),
                fromDegrees(minLonDegrees), fromDegrees(maxLonDegrees)
            )
        }

        @JvmStatic
        fun fromRadians(minLatRadians: Double, minLonRadians: Double, deltaLatRadians: Double, deltaLonRadians: Double): Sector {
            val maxLatRadians = if (deltaLatRadians > 0)
                clampLatitude(minLatRadians + deltaLatRadians) else minLatRadians
            val maxLonRadians = if (deltaLonRadians > 0)
                clampLongitude(minLonRadians + deltaLonRadians) else minLonRadians
            return Sector(
                fromRadians(minLatRadians), fromRadians(maxLatRadians),
                fromRadians(minLonRadians), fromRadians(maxLonRadians)
            )
        }
    }

    /**
     * Computes the location of the angular center of this sector, which is the mid-angle of each of this sector's
     * latitude and longitude dimensions.
     *
     * @param result a pre-allocated [Location] in which to return the computed centroid
     *
     * @return the specified result argument containing the computed centroid
     */
    fun centroid(result: Location): Location {
        result.latitude = centroidLatitude
        result.longitude = centroidLongitude
        return result
    }

    /**
     * Sets this sector to the specified latitude, longitude and dimension.
     *
     * @param minLatitude    the minimum latitude, i.e., the latitude at the southwest corner of the sector.
     * @param minLongitude   the minimum longitude, i.e., the longitude at the southwest corner of the sector.
     * @param deltaLatitude  the width of the sector; must equal to or greater than zero.
     * @param deltaLongitude the height of the sector; must equal to or greater than zero.
     *
     * @return this sector with its coordinates set to the specified values
     */
    fun set(minLatitude: Angle, minLongitude: Angle, deltaLatitude: Angle, deltaLongitude: Angle) = apply {
        this.minLatitude = minLatitude
        this.minLongitude = minLongitude
        maxLatitude = if (deltaLatitude.inDegrees > 0.0) (minLatitude + deltaLatitude).clampLatitude() else minLatitude
        maxLongitude = if (deltaLongitude.inDegrees > 0.0) (minLongitude + deltaLongitude).clampLongitude() else minLongitude
    }

    /**
     * Sets this sector to the specified latitude, longitude and dimension in degrees.
     *
     * @param minLatitude    the minimum latitude in degrees, i.e., the latitude at the southwest corner of the sector.
     * @param minLongitude   the minimum longitude in degrees, i.e., the longitude at the southwest corner of the sector.
     * @param deltaLatitude  the width of the sector in degrees; must equal to or greater than zero.
     * @param deltaLongitude the height of the sector in degrees; must equal to or greater than zero.
     *
     * @return this sector with its coordinates set to the specified values
     */
    fun setDegrees(minLatitude: Double, minLongitude: Double, deltaLatitude: Double, deltaLongitude: Double) = set(
        fromDegrees(minLatitude), fromDegrees(minLongitude),
        fromDegrees(deltaLatitude), fromDegrees(deltaLongitude),
    )

    /**
     * Sets this sector to the specified latitude, longitude and dimension in radians.
     *
     * @param minLatitude    the minimum latitude in radians, i.e., the latitude at the southwest corner of the sector.
     * @param minLongitude   the minimum longitude in radians, i.e., the longitude at the southwest corner of the sector.
     * @param deltaLatitude  the width of the sector in radians; must equal to or greater than zero.
     * @param deltaLongitude the height of the sector in radians; must equal to or greater than zero.
     *
     * @return this sector with its coordinates set to the specified values
     */
    fun setRadians(minLatitude: Double, minLongitude: Double, deltaLatitude: Double, deltaLongitude: Double) = set(
        fromRadians(minLatitude), fromRadians(minLongitude),
        fromRadians(deltaLatitude), fromRadians(deltaLongitude),
    )

    /**
     * Sets this sector to the minimum and maximum latitudes and longitudes of a specified sector.
     *
     * @param sector the sector specifying the new coordinates
     *
     * @return this sector with its coordinates set to that of the specified sector
     */
    fun copy(sector: Sector) = apply {
        minLatitude = sector.minLatitude
        maxLatitude = sector.maxLatitude
        minLongitude = sector.minLongitude
        maxLongitude = sector.maxLongitude
    }

    /**
     * Sets this sector to an empty sector.
     *
     * @return this sector with its coordinates set to an empty sector
     */
    fun setEmpty() = apply {
        minLatitude = ZERO
        maxLatitude = ZERO
        minLongitude = ZERO
        maxLongitude = ZERO
    }

    /**
     * Sets this sector to the full range of latitude [90 to +90] and longitude [-180 to +180].
     *
     * @return this sector with its coordinates set to the full range of latitude and longitude
     */
    fun setFullSphere() = apply {
        minLatitude = NEG90
        maxLatitude = POS90
        minLongitude = NEG180
        maxLongitude = POS180
    }

    /**
     * Indicates whether this sector intersects a specified sector. Two sectors intersect when both the latitude
     * boundaries and the longitude boundaries overlap by a non-zero amount. An empty sector never intersects another
     * sector.
     * 
* The sectors are assumed to have normalized angles (angles within the range [-90, +90] latitude and [-180, +180] * longitude). * * @param sector the sector to test intersection with * * @return true if the specified sector intersections this sector, false otherwise */ fun intersects(sector: Sector) = minLatitude.inDegrees < sector.maxLatitude.inDegrees && maxLatitude.inDegrees > sector.minLatitude.inDegrees && minLongitude.inDegrees < sector.maxLongitude.inDegrees && maxLongitude.inDegrees > sector.minLongitude.inDegrees /** * Indicates if this sector is next to, or intersects, a specified sector. Two sectors intersect when the conditions * of the [Sector.intersects] methods have been met, and if the boundary or corner is shared with the * specified sector. This is a temporary implementation and will be deprecated in future releases. *
* The sectors are assumed to have normalized angles (angles within the range [-90, +90] latitude and [-180, +180] * longitude). * * @param sector the sector to test intersection with * * @return true if the specified sector intersects or is next to this sector, false otherwise */ fun intersectsOrNextTo(sector: Sector) = minLatitude.inDegrees <= sector.maxLatitude.inDegrees && maxLatitude.inDegrees >= sector.minLatitude.inDegrees && minLongitude.inDegrees <= sector.maxLongitude.inDegrees && maxLongitude.inDegrees >= sector.minLongitude.inDegrees /** * Computes the intersection of this sector and a specified sector, storing the result in this sector and returning * whether or not the sectors intersect. Two sectors intersect when both the latitude boundaries and the longitude * boundaries overlap by a non-zero amount. An empty sector never intersects another sector. When there is no * intersection, this returns false and leaves this sector unchanged. *
* The sectors are assumed to have normalized angles (angles within the range [-90, +90] latitude and [-180, +180] * longitude). * * @param sector the sector to intersect with * * @return this true if this sector intersects the specified sector, false otherwise */ fun intersect(sector: Sector): Boolean { if (minLatitude.inDegrees < sector.maxLatitude.inDegrees && maxLatitude.inDegrees > sector.minLatitude.inDegrees && minLongitude.inDegrees < sector.maxLongitude.inDegrees && maxLongitude.inDegrees > sector.minLongitude.inDegrees ) { if (minLatitude.inDegrees < sector.minLatitude.inDegrees) minLatitude = sector.minLatitude if (maxLatitude.inDegrees > sector.maxLatitude.inDegrees) maxLatitude = sector.maxLatitude if (minLongitude.inDegrees < sector.minLongitude.inDegrees) minLongitude = sector.minLongitude if (maxLongitude.inDegrees > sector.maxLongitude.inDegrees) maxLongitude = sector.maxLongitude return true } return false // the two sectors do not intersect } /** * Indicates whether this sector contains a specified geographic location. * An empty sector never contains a location. * Assumes normalized angles: [-90, +90], [-180, +180] * * @param latitude the location's latitude * @param longitude the location's longitude * * @return true if this sector contains the location, false otherwise */ fun contains(latitude: Angle, longitude: Angle) = latitude.inDegrees in minLatitude.inDegrees..maxLatitude.inDegrees && longitude.inDegrees in minLongitude.inDegrees..maxLongitude.inDegrees /** * Indicates whether this sector contains a specified geographic location. An empty sector never contains a * location. * * @param location the location * * @return true if this sector contains the location, false otherwise */ fun contains(location: Location) = contains(location.latitude, location.longitude) /** * Indicates whether this sector fully contains a specified sector. This sector contains the specified sector when * the specified sector's boundaries are completely contained within this sector's boundaries, or are equal to this * sector's boundaries. An empty sector never contains another sector. *
* The sectors are assumed to have normalized angles (angles within the range [-90, +90] latitude and [-180, +180] * longitude). * * @param sector the sector to test containment with * * @return true if the specified sector contains this sector, false otherwise */ fun contains(sector: Sector) = minLatitude.inDegrees <= sector.minLatitude.inDegrees && maxLatitude.inDegrees >= sector.maxLatitude.inDegrees && minLongitude.inDegrees <= sector.minLongitude.inDegrees && maxLongitude.inDegrees >= sector.maxLongitude.inDegrees /** * Sets this sector to the union of itself and a specified location. * Assumes normalized angles: [-90, +90], [-180, +180] * * @param latitude the location's latitude * @param longitude the location's longitude * * @return this sector, set to its union with the specified location */ fun union(latitude: Angle, longitude: Angle) = apply { if (!isEmpty) { minLatitude = min(minLatitude, latitude) maxLatitude = max(maxLatitude, latitude) minLongitude = min(minLongitude, longitude) maxLongitude = max(maxLongitude, longitude) } else { minLatitude = latitude maxLatitude = latitude minLongitude = longitude maxLongitude = longitude } } /** * Sets this sector to the union of itself and a specified location. * * @param location the location * * @return this sector, set to its union with the specified location */ fun union(location: Location) = union(location.latitude, location.longitude) /** * Sets this sector to the union of itself and an array of specified locations. If this sector is empty, it bounds * the specified locations. The array is understood to contain location of at least two coordinates organized as * (longitude, latitude, ...), where stride indicates the number of coordinates between longitude values. * * @param array the array of locations to consider * @param count the number of array elements to consider * @param stride the number of coordinates between the first coordinate of adjacent locations - must be at least 2 * * @return This bounding box set to contain the specified array of locations. * * @throws IllegalArgumentException If the array is empty, if the count is less than 0, or if the stride is * less than 2 */ fun union(array: FloatArray, count: Int, stride: Int) = apply { require(array.size >= stride) { logMessage(ERROR, "Sector", "union", "missingArray") } require(count >= 0) { logMessage(ERROR, "Sector", "union", "invalidCount") } require(stride >= 2) { logMessage(ERROR, "Sector", "union", "invalidStride") } val empty = isEmpty var minLat = if (empty) Double.MAX_VALUE else minLatitude.inDegrees var maxLat = if (empty) -Double.MAX_VALUE else maxLatitude.inDegrees var minLon = if (empty) Double.MAX_VALUE else minLongitude.inDegrees var maxLon = if (empty) -Double.MAX_VALUE else maxLongitude.inDegrees for (idx in 0 until count step stride) { val lon = array[idx].toDouble() val lat = array[idx + 1].toDouble() if (maxLat < lat) maxLat = lat if (minLat > lat) minLat = lat if (maxLon < lon) maxLon = lon if (minLon > lon) minLon = lon } if (minLat < Double.MAX_VALUE) minLatitude = fromDegrees(minLat) if (maxLat > -Double.MAX_VALUE) maxLatitude = fromDegrees(maxLat) if (minLon < Double.MAX_VALUE) minLongitude = fromDegrees(minLon) if (maxLon > -Double.MAX_VALUE) maxLongitude = fromDegrees(maxLon) } /** * Sets this sector to the union of itself and a specified sector. * This has no effect if the specified sector is empty. * If this sector is empty, it is set to the specified sector. * Assumes normalized angles: [-90, +90], [-180, +180] * * @param sector the sector to union with * * @return this sector, set to its union with the specified sector */ fun union(sector: Sector) = apply { if (!sector.isEmpty) { // specified sector not empty if (!isEmpty) { // this sector not empty, make a union if (minLatitude.inDegrees > sector.minLatitude.inDegrees) minLatitude = sector.minLatitude if (maxLatitude.inDegrees < sector.maxLatitude.inDegrees) maxLatitude = sector.maxLatitude if (minLongitude.inDegrees > sector.minLongitude.inDegrees) minLongitude = sector.minLongitude if (maxLongitude.inDegrees < sector.maxLongitude.inDegrees) maxLongitude = sector.maxLongitude } else { // this sector is empty, set to the specified sector minLatitude = sector.minLatitude maxLatitude = sector.maxLatitude minLongitude = sector.minLongitude maxLongitude = sector.maxLongitude } } } /** * Translates this sector by a specified geographic increment. *
* The translated sector is assumed to have normalized angles (angles within the range [-90, +90] latitude and * [-180, +180] longitude). * * @param deltaLatitudeDegrees the translation's latitude increment in degrees * @param deltaLongitudeDegrees the translation's longitude increment in degrees * * @return this sector, translated by the specified increment */ fun translate(deltaLatitudeDegrees: Double, deltaLongitudeDegrees: Double) = apply { minLatitude = minLatitude.plusDegrees(deltaLatitudeDegrees) maxLatitude = maxLatitude.plusDegrees(deltaLatitudeDegrees) minLongitude = minLongitude.plusDegrees(deltaLongitudeDegrees) maxLongitude = maxLongitude.plusDegrees(deltaLongitudeDegrees) } /** * Determines minimal level number relevant for sector of the specified size * @param maxLevelNumber Maximum available level number * @return Minimal relevant level number for sector */ fun minLevelNumber(maxLevelNumber: Int): Int { val relWidth = abs(maxLongitude.relativeLongitude - minLongitude.relativeLongitude) val relHeight = abs(maxLatitude.relativeLatitude - minLatitude.relativeLatitude) val delta = 0.00001 for (minLevelNumber in 0 until maxLevelNumber) { val tileSize = 1.0 / (1 shl (minLevelNumber + 1)) if (relWidth - delta > tileSize && relHeight - delta > tileSize) return minLevelNumber } return maxLevelNumber } fun equals(other: Sector, tolerance: Double): Boolean { // if (this === other) return true // Empty sector is not equal self if (isEmpty && other.isEmpty) return false // Two empty sectors are not equal return abs(minLatitude.inDegrees - other.minLatitude.inDegrees) < tolerance && abs(maxLatitude.inDegrees - other.maxLatitude.inDegrees) < tolerance && abs(minLongitude.inDegrees - other.minLongitude.inDegrees) < tolerance && abs(maxLongitude.inDegrees - other.maxLongitude.inDegrees) < tolerance } /** * Computes a row number for a tile within a level given the tile's latitude. * * @param tileDelta the level's tile delta * @param latitude the tile's minimum latitude * * @return the computed row number */ open fun computeRow(tileDelta: Angle, latitude: Angle): Int { var row = floor((latitude.inDegrees - minLatitude.inDegrees) / tileDelta.inDegrees).toInt() // if latitude is at the end of the grid, subtract 1 from the computed row to return the last row if (latitude.inDegrees - minLatitude.inDegrees == 180.0) row -= 1 return row } /** * Computes a column number for a tile within a level given the tile's longitude. * * @param tileDelta the level's tile delta * @param longitude the tile's minimum longitude * * @return The computed column number */ open fun computeColumn(tileDelta: Angle, longitude: Angle): Int { var col = floor((longitude.inDegrees - minLongitude.inDegrees) / tileDelta.inDegrees).toInt() // if longitude is at the end of the grid, subtract 1 from the computed column to return the last column if (longitude.inDegrees - minLongitude.inDegrees == 360.0) col -= 1 return col } /** * Computes the last row number for a tile within a level given the tile's maximum latitude. * * @param tileDelta the level's tile delta * @param latitude the tile's maximum latitude * * @return the computed row number */ open fun computeLastRow(tileDelta: Angle, latitude: Angle): Int { var row = ceil((latitude.inDegrees - minLatitude.inDegrees) / tileDelta.inDegrees - 1).toInt() // if max latitude is in the first row, set the max row to 0 if (latitude.inDegrees - minLatitude.inDegrees < tileDelta.inDegrees) row = 0 return row } /** * Computes the last column number for a tile within a level given the tile's maximum longitude. * * @param tileDelta the level's tile delta * @param longitude the tile's maximum longitude * * @return The computed column number */ open fun computeLastColumn(tileDelta: Angle, longitude: Angle): Int { var col = ceil((longitude.inDegrees - minLongitude.inDegrees) / tileDelta.inDegrees - 1).toInt() // if max longitude is in the first column, set the max column to 0 if (longitude.inDegrees - minLongitude.inDegrees < tileDelta.inDegrees) col = 0 return col } override fun equals(other: Any?): Boolean { // if (this === other) return true // Empty sector is not equal self if (other !is Sector) return false if (isEmpty && other.isEmpty) return false // Two empty sectors are not equal return minLatitude == other.minLatitude && maxLatitude == other.maxLatitude && minLongitude == other.minLongitude && maxLongitude == other.maxLongitude } override fun hashCode(): Int { var result = minLatitude.hashCode() result = 31 * result + maxLatitude.hashCode() result = 31 * result + minLongitude.hashCode() result = 31 * result + maxLongitude.hashCode() return result } override fun toString() = "Sector(minLatitude=$minLatitude, maxLatitude=$maxLatitude, minLongitude=$minLongitude, maxLongitude=$maxLongitude)" }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy