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

io.data2viz.voronoi.Diagram.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2018-2019. data2viz sàrl.
 *
 *  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.
 *
 */

package io.data2viz.voronoi

import io.data2viz.geom.Point
import kotlin.math.abs


fun  MutableList.pop(): T? = if(isEmpty()) null else removeAt(lastIndex)

class Diagram(initialSites: Array, clipStart: Point = Point(.0, .0), clipEnd: Point? = null) {

    private var x: Double? = null
    private var y: Double? = null
    private var site: Site? = null
    private var circle: RedBlackNode? = null
    private val sites: MutableList = with(initialSites) { sort(); toMutableList() }

    var edges: MutableList
    var cells: Array?

    init {
        wCells = arrayOfNulls(initialSites.size)
        site = sites.pop()

        while (true) {
            circle = firstCircle

            if (site != null &&
                    (circle == null
                            || site!!.y < circle!!.y
                            || (site!!.y == circle!!.y && site!!.x < circle!!.x))) {
                if (site!!.x != x || site!!.y != y) {
                    addBeach(site!!)
                    x = site!!.x
                    y = site!!.y
                }
                site = sites.pop()
            } else if (circle != null) {
                removeBeach(circle!!)
            } else {
                break
            }
        }

        sortCellHalfedges()

        if (clipEnd != null) {
            clipEdges(clipStart, clipEnd)
            clipCells(clipStart, clipEnd)
        }

        this.edges = wEdges
        this.cells = wCells

        beaches.root = null
        circles.root = null

        wEdges = mutableListOf()
        wCells = null

    }

    private fun clipCells(clipStart: Point, clipEnd: Point) {
        val nCells = wCells!!.size
        var cell:Cell?
        var site:Site?
        var iHalfedge:Int
        var halfedges:MutableList
        var nHalfedges:Int
        var start: Point
        var end: Point
        var cover = true

        val x0 = clipStart.x
        val y0 = clipStart.y
        val x1 = clipEnd.x
        val y1 = clipEnd.y

        wCells!!.forEach { cel ->
                site = cel!!.site
                halfedges = cel.halfedges
                iHalfedge = halfedges.size

                // Remove any dangling clipped edges.
                while (iHalfedge-- > 0) {
                    if (wEdges[halfedges[iHalfedge]] == null) {
                        halfedges.removeAt(iHalfedge)
                    }
                }

                // Insert any border edges as necessary.
                iHalfedge = 0
                nHalfedges = halfedges.size
                while (iHalfedge < nHalfedges) {
                    end = cel.cellHalfedgeEnd(wEdges[halfedges[iHalfedge]]!!)!!
                    start = cel.cellHalfedgeStart(wEdges[halfedges[++iHalfedge % nHalfedges]]!!)!!
                    val startX = start.x
                    val startY = start.y
                    val endX = end.x
                    val endY = end.y

                    if (abs(end.x - start.x) > epsilon || abs(end.y - start.y) > epsilon) {

                        val edge = createBorderEdge(site!!, end,
                                when {
                                    abs(endX - x0) < epsilon && y1 - endY > epsilon -> Point(x0, if(abs(startX - x0) < epsilon) startY else y1)
                                    abs(endY - y1) < epsilon && x1 - endX > epsilon -> Point(if (abs(startY - y1) < epsilon) startX else x1, y1)
                                    abs(endX - x1) < epsilon && endY - y0 > epsilon -> Point(x1, if(abs(startX - x1) < epsilon) startY else y0)
                                    abs(endY - y0) < epsilon && endX - x0 > epsilon -> Point(if(abs(startY - y0) < epsilon) startX else x0, y0)
                                    else -> null
                                })

                        wEdges.add(edge)
                        halfedges.add(iHalfedge, wEdges.size -1 )
                        ++nHalfedges
                    }
                }
                if (nHalfedges > 0) cover = false
//            }
        }

        // If there weren’t any edges, have the closest site cover the extent.
        // It doesn’t matter which corner of the extent we measure!
        if (cover) {
            var dx:Double
            var dy:Double
            var d2:Double
            var dc = Double.POSITIVE_INFINITY

            var coverCel: Cell? = null

            for (iCell in 0..nCells-1) {
                cell = wCells!![iCell]
                if (cell != null) {
                    site = cell.site
                    dx = site!!.x - x0
                    dy = site!!.y - y0
                    d2 = dx * dx + dy * dy
                    if (d2 < dc) {
                        dc = d2
                        coverCel = cell
                    }
                }
            }

            if (coverCel != null) {
                val v00 = Point(x0, y0)
                val v01 = Point(x0, y1)
                val v11 = Point(x1, y1)
                val v10 = Point(x1, y0)

                site = coverCel.site
                wEdges.add(createBorderEdge(site!!, v00, v01))
                wEdges.add(createBorderEdge(site!!, v01, v11))
                wEdges.add(createBorderEdge(site!!, v11, v10))
                wEdges.add(createBorderEdge(site!!, v10, v00))

                (wEdges.size-5..wEdges.size-1).forEach { coverCel.halfedges.add(it) }
            }
        }

        // Lastly delete any cells with no edges; these were entirely clipped.
        wCells!!.forEachIndexed { index, cel ->
            if (cel?.halfedges?.size == 0) { wEdges[index] = null }
        }
    }

    private fun clipEdges(start: Point, end: Point) {
        var i = wEdges.size
        var edge:Edge

        while (i-- > 0) {
            edge = wEdges[i]!!
            if (!edge.connect(start, end)
                    || !edge.clip(start, end)
                    || !(abs(edge.start!!.x - edge.end!!.x) > epsilon
                    ||  abs(edge.start!!.y - edge.end!!.y) > epsilon)) {
                wEdges[i] = null
            }
        }
    }

    private var _found:Int? = null

    fun Point.squareDistance(point: Point): Double {
        val vx = x - point.x
        val vy = y - point.y
        return vx * vx + vy * vy
    }


    fun find(point: Point, radius: Double? = null): Site? {
        val that = this
        var i0:Int
        var i1:Int? = _found ?: 0
        val n = that.cells?.size ?: 0
        var cell: Cell? = cells!![i1!!]

        // Use the previously-found cell, or start with an arbitrary one.
        while (cell == null) {
            if (++i1 >= n) return null
            cell = that.cells?.get(i1)
        }
        var d2 = point.squareDistance(cell.site.pos)

        fun Edge.opposite(site:Site): Site? {
            if (right == null) return null
            return if (left == site) right else left
        }

        // Traverse the half-edges to find a closer cell, if any.
        do {
            i0 = i1!!
            cell = that.cells!![i0]
            i1 = null
            cell!!.halfedges.forEach { e ->
                val edge = that.edges[e]
                val opposite = edge?.opposite(cell.site) ?: return@forEach
                val v2 = point.squareDistance(opposite.pos)
                if (v2 < d2) {
                    d2 = v2
                    i1 = opposite.index
                }
            }
        } while (i1 != null)

        that._found = i0

        return if (radius == null || d2 <= radius * radius) cell?.site else null
    }

    fun polygons()  = cells?.asSequence()
            ?.filterNotNull()
            ?.map { cell ->
            cell.halfedges.map { edges[it]?.let { e -> cell.halfedgeStart(e) } }.filterNotNull()
        }?.toList() ?: emptyList()

    data class Link(val source: Point, val target: Point)

    fun links() =
        edges.filter { it?.right != null}
                .map { Link(it!!.left.pos, it.right!!.pos)}


    fun triangles(): MutableList {
        val triangles = mutableListOf()
        cells!!.filterNotNull()
                .forEachIndexed { i, cell ->

            val halfedges = cell.halfedges
            val m = halfedges.size
            if (m == 0) return@forEachIndexed
            val site = cell.site
            var e1 = edges[halfedges[m-1]]
            var s0: Site?
            var s1 = if (e1?.left === site) e1.right else e1?.left

            var j = -1
            while (++j < m){
                 s0 = s1
                e1 = edges[halfedges[j]]
                s1 = if (e1?.left === site) e1.right else e1?.left
                if (s0 != null && s1 != null && i < s0.index && i < s1.index && triangleArea( site.pos, s0.pos, s1.pos) < 0) {
                    triangles.add(Triangle(site, s0, s1))
                }
            }
        }
        return triangles
    }


    fun triangleArea(a: Point, b: Point, c: Point) = (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y)

}

data class Triangle(val a:Site, val b: Site, val c: Site)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy