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

commonMain.me.piruin.geok.LatLng.kt Maven / Gradle / Ivy

/*
 * Copyright (c) 2018 Piruin Panichphol
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package me.piruin.geok

import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.math.tan

data class LatLng(val latitude: Double, val longitude: Double, val elevation: Double? = null) {

    val x get() = longitude
    val y get() = latitude
    val z get() = elevation

    constructor(xyPair: Pair) : this(xyPair.second.toDouble(), xyPair.first.toDouble())

    init {
        require(latitude between (-90.0 and 90.0)) { "latitude should between -90.0 and 90 [$latitude]" }
        require(longitude between (-180.0 and 180.0)) { "longitude should between -180.0 and 180 [$longitude]" }
    }

    /**
     * @return distance in meter (m)
     */
    infix fun distanceTo(latlng: LatLng) = distanceCalculator().between(this, latlng)

    override fun toString(): String {
        return "[$longitude, $latitude" + if (elevation != null) ", $elevation]" else "]"
    }

    /**
     * This method port from Professor Steven Dutch's JavaScript "Convert Between Geographic and UTM Coordinates"
     *
     * @see Convert Between Geographic and UTM Coordinates
     * @see Converting UTM to Latitude and Longitude (Or Vice Versa)
     *
     */
    fun toUtm(datum: Datum = Datum.WSG84): Utm {
        require(latitude between (-80.0 to 84.0)) { "latitude $latitude is outside utm grid" }

        val a = datum.equatorialRad
        val f = 1.0 / datum.flat // polar flattening.
        val b = datum.polarRad
        val e = sqrt(1 - (b / a) * (b / a)) // eccentricity

        val scale = 0.9996 // scale on central meridian

        val latRad = latitude.toRadians() // Convert latitude to radians
        val utmZone = 1 + floor((longitude + 180) / 6) // calculate utm zone
        var latZone = when { // Latitude zone: A-B S of -80, C-W -80 to +72, X 72-84, Y,Z N of 84
            latitude > -80 && latitude < 72 -> (floor((latitude + 80) / 8) + 2).toInt()
            latitude > 72 && latitude < 84 -> 21
            latitude > 84 -> 23
            else -> 0
        }

        val zcm = 3 + 6 * (utmZone - 1) - 180 // Central meridian of zone
        val esq = (1 - (b / a) * (b / a)) // e squared for use in expansions
        val e0sq = e * e / (1 - e * e) // e0 squared - always even powers
        val N = a / sqrt(1 - (e * sin(latRad)).pow(2.0))

        val T = tan(latRad).pow(2.0)
        val C = e0sq * cos(latRad).pow(2.0)
        val A = (longitude - zcm).toRadians() * cos(latRad)

        var easting = scale * N * A * (
            1 + A * A * (
                (1 - T + C) / 6 + A * A
                    * (5 - 18 * T + T * T + 72 * C - 58 * e0sq) / 120
                )
            ) // Easting relative to Central meridian
        easting += 500000

        var M = latRad * (1.0 - esq * (1.0 / 4.0 + esq * (3.0 / 64.0 + 5 * esq / 256)))
        M -= sin(2.0 * latRad) * (esq * (3.0 / 8.0 + esq * (3.0 / 32.0 + 45 * esq / 1024)))
        M += sin(4.0 * latRad) * (esq * esq * (15.0 / 256.0 + esq * 45.0 / 1024))
        M -= sin(6.0 * latRad) * (esq * esq * esq * (35.0 / 3072))
        M *= a // Arc length along standard meridian

        var northing = scale * (
            M + N * tan(latRad) *
                (
                    A * A * (
                        1.0 / 2.0 + A * A * (
                            (5.0 - T + (9.0 * C) + (4.0 * C * C)) / 24.0 + A * A
                                * (61.0 - (58.0 * T) + (T * T) + (600.0 * C) - (330.0 * e0sq)) / 720
                            )
                        )
                    )
            ) // Northing from equator
        if (this.latitude < 0) {
            northing += 10000000.0
        }
        return Utm(utmZone.toInt(), if (latRad > 0) 'N' else 'S', easting.round(1), northing.round(1))
    }

    /**
     * @see PNPOLY - Point Inclusion in Polygon Test
     * @return Determines whether this LatLng are inside of given Polygon.
     */
    infix fun insideOf(polygon: List): Boolean {
        if (polygon.contains(this)) // Point of Polygon consider to be inside
            return true

        var i = 0
        var j = polygon.size - 1
        var result = false

        while (i < polygon.size) {
            if (polygon[i].y > this.y != polygon[j].y > this.y &&
                this.x < (polygon[j].x - polygon[i].x) * (this.y - polygon[i].y) /
                (polygon[j].y - polygon[i].y) + polygon[i].x
            ) result = !result
            j = i++
        }
        return result
    }

    override fun hashCode(): Int {
        var result = latitude.hashCode()
        result = 31 * result + longitude.hashCode()
        result = 31 * result + (elevation?.hashCode() ?: 0)
        return result
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as LatLng

        if (latitude != other.latitude) return false
        if (longitude != other.longitude) return false
        if (elevation != other.elevation) return false

        return true
    }

    companion object {
        /**
         * Default precision value for comparing LatLng data,
         * 6 decimal should be enough for more case (accurate at 11 cm).
         *
         * @see Accuracy of digit
         * @see 




© 2015 - 2025 Weber Informatics LLC | Privacy Policy