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

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

Go to download

The WorldWind Kotlin SDK (WWK) includes the library, examples and tutorials for building multiplatform 3D virtual globe applications for Android, Web and Java.

The newest version!
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 - 2024 Weber Informatics LLC | Privacy Policy