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

commonMain.jetbrains.datalore.plot.base.stat.ContourStatUtil.kt Maven / Gradle / Ivy

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2019. JetBrains s.r.o.
 * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
 */

package jetbrains.datalore.plot.base.stat

import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.interval.DoubleSpan
import jetbrains.datalore.plot.base.DataFrame
import jetbrains.datalore.plot.base.data.TransformVar
import jetbrains.datalore.plot.common.data.SeriesUtil
import kotlin.math.max
import kotlin.math.min

object ContourStatUtil {
    private val xLoc = doubleArrayOf(0.0, 1.0, 1.0, 0.0, .5)
    private val yLoc = doubleArrayOf(0.0, 0.0, 1.0, 1.0, .5)

    /**
     * @param xs - series defining x-coordinates of data-points
     * @return Pair(col, row) or fail if not a regular grid
     */
    internal fun estimateRegularGridShape(xs: List): Pair {
        // num of columns
        // regular X/Y grid is expected
        var colCount = 0
        var x0: Double? = null
        for (x in xs) {
            if (x0 == null) {
                x0 = x
            } else if (x == x0) {
                break
            }
            colCount++
        }

        if (colCount <= 1) {
            throw IllegalArgumentException("Data grid must be at least 2 columns wide (was $colCount)")
        }
        val rowCount = xs.size / colCount
        if (rowCount <= 1) {
            throw IllegalArgumentException("Data grid must be at least 2 rows tall (was $rowCount)")
        }

        return Pair(colCount, rowCount)
    }

    fun computeLevels(data: DataFrame, binOptions: BinStatUtil.BinOptions): List? {
        if (!(data.has(TransformVar.X) && data.has(TransformVar.Y) && data.has(
                TransformVar.Z
            ))
        ) {
            return null
        }
        val zRange = data.range(TransformVar.Z)
        return computeLevels(zRange, binOptions)
    }

    fun computeLevels(zRange: DoubleSpan?, binOptions: BinStatUtil.BinOptions): List? {
        if (zRange == null || SeriesUtil.isBeyondPrecision(zRange)) return null

        val b = BinStatUtil.binCountAndWidth(zRange.length, binOptions)
        val levels = ArrayList()
        for (i in 0 until b.count) {
            var level = i * b.width + zRange.lowerEnd
            level += b.width / 2   // shift all levels half-step to make 1-st level contour visible
            levels.add(level)
        }
        return levels
    }

    internal fun computeContours(data: DataFrame, levels: List): Map>> {
        val xVector = data.getNumeric(TransformVar.X)
        val zVector = data.getNumeric(TransformVar.Z)

        val shape = estimateRegularGridShape(xVector)
        val colCount = shape.first
        val rowCount = shape.second

        val xRange = data.range(TransformVar.X)!!
        val yRange = data.range(TransformVar.Y)!!

        return computeContours(
            xRange,
            yRange,
            colCount,
            rowCount,
            zVector,
            levels
        )
    }

    fun computeContours(
        xRange: DoubleSpan,
        yRange: DoubleSpan,
        colCount: Int,
        rowCount: Int,
        data: List,
        levels: List
    ): Map>> {

        val xStep = xRange.length / (colCount - 1)
        val yStep = yRange.length / (rowCount - 1)
        val origin = DoubleVector(xRange.lowerEnd, yRange.lowerEnd)

        val pathListByLevel = HashMap>>()
        for (level in levels) {
            val levelSegments =
                computeSegments(colCount, rowCount, data, level)
            val paths = joinSegments(levelSegments)
            // checkEdges(paths, colCount, rowCount); // debug use
            var pathsReal =
                convertPaths(paths, xStep, yStep, origin, level)
            pathsReal = confirmPaths(pathsReal)
            pathListByLevel[level] = pathsReal
        }

        return pathListByLevel
    }

    private fun joinSegments(segments: List>): List> {
        val pathList = ArrayList>()

        val pathByEndPoint = HashMap>()

        for (segment in segments) {
            val p0 = segment.first
            val p1 = segment.second

            if (pathByEndPoint.containsKey(p0) || pathByEndPoint.containsKey(p1)) {
                //remove need to be modified paths
                val path0 = pathByEndPoint[p0]
                val path1 = pathByEndPoint[p1]

                if (path0 != null) {
                    pathByEndPoint.remove(path0[0])
                    pathByEndPoint.remove(path0[path0.size - 1])
                }
                if (path1 != null) {
                    pathByEndPoint.remove(path1[0])
                    pathByEndPoint.remove(path1[path1.size - 1])
                }

                val longPath: MutableList
                if (path0 === path1) {
                    //a cycle is formed
                    path0!!.add(segment.second)
                    pathList.add(path0)
                    continue
                } else if (path0 != null && path1 != null) {
                    longPath = path0
                    longPath.addAll(path1)
                } else if (path0 == null) { //path1!=null
                    longPath = path1!!
                    longPath.add(0, segment.first)
                } else { // path0!=null
                    longPath = path0
                    longPath.add(segment.second)
                }

                pathByEndPoint[longPath[0]] = longPath
                pathByEndPoint[longPath[longPath.size - 1]] = longPath
            } else {
                val path = ArrayList()
                path.add(segment.first)
                path.add(segment.second)
                pathByEndPoint[segment.first] = path
                pathByEndPoint[segment.second] = path
            }
        }

        // collect paths (don't duplicate)
        val pathSet = HashSet(pathByEndPoint.values)
        for (path in pathSet) {
            pathList.add(path)
        }
        return pathList
    }

    private fun convertPaths(
        paths: List>,
        xStep: Double,
        yStep: Double,
        origin: DoubleVector,
        level: Double?
    ): List> {
        val result = ArrayList>()

        for (path in paths) {
            val temp = ArrayList()
            var lastPoint: DoubleVector? = null
            for (edge in path) {
                var intersect = edge.intersect(level!!)
                intersect = DoubleVector(xStep * intersect.x, yStep * intersect.y).add(origin)
                // in case two adjacent double vectors are the same
                if (intersect == lastPoint) {
                    continue
                }
                temp.add(intersect)
                lastPoint = intersect
            }
            // in case of slight computational error in loops
            if (path[0] == path[path.size - 1] && temp[0] != temp[temp.size - 1]) {
                temp[temp.size - 1] = temp[0]
            }
            if (temp.size > 1) {
                result.add(temp)
            }
        }

        return result
    }

    private fun confirmPaths(paths: List>): List> {
        // join paths that share the same end points
        val newPaths = ArrayList>()

        val pathByEndPoint = HashMap>()

        for (path in paths) {
            val p0 = path[0]
            val p1 = path[path.size - 1]
            //if cycle
            if (p0 == p1) {
                newPaths.add(path)
                continue
            }

            if (pathByEndPoint.containsKey(p0) || pathByEndPoint.containsKey(p1)) {
                //remove need to be modified paths
                val path0 = pathByEndPoint[p0]
                val path1 = pathByEndPoint[p1]
                removePathByEndpoints(path0, pathByEndPoint)
                removePathByEndpoints(path1, pathByEndPoint)

                val longPath = ArrayList()
                if (path0 === path1) {
                    //a cycle is formed
                    longPath.addAll(path0!!)
                    longPath.addAll(path.subList(1, path.size))
                    newPaths.add(longPath)
                    continue
                } else if (path0 != null && path1 != null) {
                    longPath.addAll(path0)
                    longPath.addAll(path.subList(1, path.size - 1))
                    longPath.addAll(path1)
                } else if (path0 == null) { //path1!=null
                    longPath.addAll(path1!!)
                    longPath.addAll(0, path.subList(0, path.size - 1))
                } else { //path0!=null
                    longPath.addAll(path0)
                    longPath.addAll(path.subList(1, path.size))
                }

                pathByEndPoint[longPath[0]] = longPath
                pathByEndPoint[longPath[longPath.size - 1]] = longPath
            } else {
                pathByEndPoint[path[0]] = path
                pathByEndPoint[path[path.size - 1]] = path
            }
        }

        val pathSet = HashSet(pathByEndPoint.values)
        for (path in pathSet) {
            newPaths.add(path)
        }

        // prevent the degenerated polygons from providing weird outputs
        val result = ArrayList>()
        for (path in newPaths) {
            result.addAll(pathSeparator(path))
        }
        return result
    }

    internal fun > removePathByEndpoints(
        path: List?,
        pathByEndPoint: MutableMap
    ) {
        if (path != null) {
            pathByEndPoint.remove(path[0])
            pathByEndPoint.remove(path[path.size - 1])
        }
    }

    private fun pathSeparator(path: List): List> {
        val result = ArrayList>()
        var startIndex = 0
        for (nextIndex in 1 until path.size - 1) {
            if (path[startIndex] == path[nextIndex]) {
                result.add(path.subList(startIndex, nextIndex + 1))
                startIndex = nextIndex
            }
        }
        if (startIndex == 0) {
            return listOf(path)
        } else {
            result.add(path.subList(startIndex, path.size))
            return result
        }
    }

    private fun computeSegments(
        colCount: Int,
        rowCount: Int,
        data: List,
        level: Double
    ): List> {

        val segments = ArrayList>()
        for (row in 0 until rowCount - 1) {
            for (col in 0 until colCount - 1) {
                val i0 = row * colCount + col
                val i1 = row * colCount + col + 1 // next right
                val i2 = (row + 1) * colCount + col + 1 // next right & up
                val i3 = (row + 1) * colCount + col // next up

                val v = DoubleArray(5)
                v[0] = data[i0]!!
                v[1] = data[i1]!!
                v[2] = data[i2]!!
                v[3] = data[i3]!!

                var min = v[0]
                var max = v[0]
                for (i in 1..3) {
                    min = min(min, v[i])
                    max = max(max, v[i])
                }

                if (min == max) {
                    // special case: all points are in the same plane - drop
                    continue
                }

                // v[4] = (v[0] + v[1] + v[2] + v[3]) / 4;   // center  (average of corners)
                v[4] = (v[0] + v[1] + v[2] + v[3] - min - max) / 2   // center  (average of corners)

                if (level > min && level <= max) {

                    val cellSegments =
                        computeSegmentsForGridCell(level, v, col, row)
                    segments.addAll(cellSegments)
                }
            }
        }

        return segments
    }

    private fun computeSegmentsForGridCell(
        level: Double?,
        value5: DoubleArray,
        col: Int,
        row: Int
    ): List> {
        // triangles:
        // [0] 0-1-4
        // [1] 1-2-4
        // [2] 2-3-4
        // [3] 3-0-4
        // check each edge of each triangle - find edges that cross the level 'plane'

        val result = ArrayList>()
        val points = ArrayList()
        var tempVector: MutableList
        for (i in 0..4) {
            points.add(
                TripleVector(
                    col + xLoc[i],
                    row + yLoc[i],
                    value5[i]
                )
            )
        }

        // All triangles are counterclockwise
        for (i in 0..3) {
            val i1 = (i + 1) % 4
            tempVector = ArrayList()
            tempVector.add(points[i])
            tempVector.add(points[i1])
            tempVector.add(points[4])
            val temp = intersectionSegment(tempVector, level)
            if (temp != null) {
                result.add(temp)
            }
        }
        return result
    }

    private fun intersectionSegment(vectors: List, level: Double?): Pair? {
        // input: a counterclockwise triangle
        // output: a segment, such that the higher part is always to the right
        val type = vectors[0].getType(level!!) * 100 + vectors[1].getType(level) * 10 + vectors[2].getType(level)
        val start: Edge
        val end: Edge
        when (type) {
            100 -> {
                start = Edge(vectors[2], vectors[0])
                end = Edge(vectors[0], vectors[1])
            }
            10 -> {
                start = Edge(vectors[0], vectors[1])
                end = Edge(vectors[1], vectors[2])
            }
            1 -> {
                start = Edge(vectors[1], vectors[2])
                end = Edge(vectors[2], vectors[0])
            }
            110 -> {
                start = Edge(vectors[0], vectors[2])
                end = Edge(vectors[2], vectors[1])
            }
            101 -> {
                start = Edge(vectors[2], vectors[1])
                end = Edge(vectors[1], vectors[0])
            }
            11 -> {
                start = Edge(vectors[1], vectors[0])
                end = Edge(vectors[0], vectors[2])
            }
            else -> return null
        }
        return Pair(start, end)
    }

    // to check if all the side edges are on the boarder **Debug Use**
    private fun checkEdges(paths: List>, colCount: Int, rowCount: Int) {
        for (path in paths) {
            if (path[0] != path[path.size - 1]) {
                checkEdge(path[0], colCount, rowCount)
                checkEdge(path[path.size - 1], colCount, rowCount)
            }
        }
    }

    private fun checkEdge(edge: Edge, colCount: Int, rowCount: Int) {
        val a = edge.myA
        val b = edge.myB
        if (a.myX == 0 && b.myX == 0) {
            return
        }
        if (a.myY == 0 && b.myY == 0) {
            return
        }
        if (a.myX == colCount - 1 && b.myX == colCount - 1) {
            return
        }
        if (a.myY == rowCount - 1 && b.myY == rowCount - 1) {
            return
        }
        throw IllegalArgumentException("Check Edge Failed")
    }

    private class TripleVector internal constructor(x: Double, y: Double, val z: Double) {
        val myX: Int
        val myY: Int
        private val myIsCenter: Int

        val coord: DoubleVector
            get() = DoubleVector(x, y)

        val x: Double
            get() = this.myX + 0.5 * myIsCenter

        val y: Double
            get() = this.myY + 0.5 * myIsCenter

        init {
            this.myX = x.toInt()
            this.myY = y.toInt()
            this.myIsCenter = if (x % 1 == 0.0) 0 else 1
        }

        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other == null || this::class != other::class) return false
            val that = other as TripleVector?
            return myX == that!!.myX && myY == that.myY && myIsCenter == that.myIsCenter
        }

        override fun hashCode(): Int {
            return arrayOf(myX, myY, myIsCenter).hashCode()
        }

        fun getType(level: Double): Int {
            return if (z >= level) {
                1
            } else {
                0
            }
        }
    }

    private class Edge internal constructor(val myA: TripleVector, val myB: TripleVector) {

        override fun equals(other: Any?): Boolean {
            if (other !is Edge) {
                return false
            }
            val obj = other as Edge?
            return this.myA == obj!!.myA && this.myB == obj.myB || this.myA == obj.myB && this.myB == obj.myA
        }

        override fun hashCode(): Int {
            return myA.coord.hashCode() + myB.coord.hashCode()
        }

        fun intersect(level: Double): DoubleVector {
            val z0 = myA.z
            val z1 = myB.z

            if (level == z0) {
                return myA.coord
            }
            if (level == z1) {
                return myB.coord
            }

            val ratio = (z1 - z0) / (level - z0)
            val x0 = myA.x
            val y0 = myA.y
            val x1 = myB.x
            val y1 = myB.y
            val x = x0 + (x1 - x0) / ratio
            val y = y0 + (y1 - y0) / ratio
            return DoubleVector(x, y)
        }
    }
}// **Debug Use**




© 2015 - 2025 Weber Informatics LLC | Privacy Policy