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

commonMain.io.data2viz.geo.geometry.clip.Clip.kt Maven / Gradle / Ivy

There is a newer version: 10.0.4
Show newest version
/*
 * Copyright (c) 2018-2021. 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.geo.geometry.clip

import io.data2viz.geo.geometry.polygonContains
import io.data2viz.geo.stream.Stream
import io.data2viz.math.EPSILON
import io.data2viz.math.HALFPI

/**
 * Default clipping. Install no Clip Stream and just returns current
 * output Stream.
 */
public val NoClip: ClipStreamBuilder = object : ClipStreamBuilder {
    override fun bindTo(downstream: Stream) = downstream
}

/**
 * Installs a ClipStream into the chain of Stream.
 */
public interface ClipStreamBuilder {

    /**
     * Adds a ClipStream in front of the [downstream]
     */
    public fun bindTo(downstream: Stream): Stream
}

/**
 * // TODO: refactor in sealed/enum or refactor all stream API
 * Takes a line and cuts into visible segments. Values for clean:
 *  0 - there were intersections or the line was empty;
 *  1 - no intersections;
 *  2 - there were intersections, and the first and last segments should be rejoined.
 */
public interface ClipStream : Stream {

    public var clean: Int
}


public interface Clipper {

    /**
     * Indicates if the point will be visible after clipping.
     */
    public fun pointVisible(x: Double, y: Double): Boolean

    /**
     * In
     */
    public fun clipLine(downstream: Stream): ClipStream

    public fun interpolate(
        from: DoubleArray?,
        to: DoubleArray?,
        direction: Int,
        stream: Stream
    )
}


internal interface ClipperWithStart : Clipper {
    val start: DoubleArray
}

internal class ClippableStream(
    val clipper: ClipperWithStart,
    val downstream: Stream
) : Stream {

    // context of execution of stream
    // a line can be projected in the context of a polygon or not
    enum class LineStartContext { DEFAULT, RING }
    enum class LineEndContext { DEFAULT, RING }

    //a point can be projected in the context of a polygon, a line, or nothing
    enum class PointContext { DEFAULT, RING, LINE }

    var pointContext = PointContext.DEFAULT
    var lineStartContext = LineStartContext.DEFAULT
    var lineEndContext = LineEndContext.DEFAULT

    internal var polygonStarted = false


    internal val clipStream: ClipStream = clipper.clipLine(downstream)

    internal val ringBuffer = BufferStream()
    internal val ringSink = clipper.clipLine(ringBuffer)


    internal val segments: MutableList>> = mutableListOf()

    internal val polygon: MutableList> = mutableListOf()
    internal var ring: MutableList? = null


    override fun polygonStart() {
        pointContext = PointContext.RING
        lineStartContext = LineStartContext.RING
        lineEndContext = LineEndContext.RING
    }

    override fun lineStart() {
        when (lineStartContext) {
            LineStartContext.DEFAULT -> {
                pointContext = PointContext.LINE
                clipStream.lineStart()
            }
            LineStartContext.RING -> {
                ringSink.lineStart()
                ring = mutableListOf()
            }
        }
    }

    override fun point(x: Double, y: Double, z: Double) {
        when (pointContext) {
            PointContext.RING -> pointRing(x, y, z)
            PointContext.LINE -> pointLine(x, y, z)
            PointContext.DEFAULT -> pointDefault(x, y, z)
        }
    }

    private fun pointRing(x: Double, y: Double, z: Double) {
        ring!!.add(doubleArrayOf(x, y))
        ringSink.point(x, y, z)
    }


    private fun pointLine(x: Double, y: Double, z: Double) {
        clipStream.point(x, y, z)
    }

    private fun pointDefault(x: Double, y: Double, z: Double) {
        if (clipper.pointVisible(x, y))
            downstream.point(x, y, z)
    }

    override fun lineEnd() {
        when (lineEndContext) {
            LineEndContext.DEFAULT -> lineEndDefault()
            LineEndContext.RING -> lineEndRing()
        }
    }

    private fun lineEndDefault() {
        pointContext = PointContext.DEFAULT
        clipStream.lineEnd()
    }

    private fun lineEndRing() {
        requireNotNull(ring, { "Error on ClippableStream.ringEnd, ring can't be null." })

        val ringList = ring!!

        pointRing(ringList[0][0], ringList[0][1], 0.0)

        ringSink.lineEnd()

        val clean = ringSink.clean
        val ringSegments: MutableList> = ringBuffer.result()


        ringList.removeAt(ringList.lastIndex)

        polygon.add(ringList)
        this.ring = null

        if (ringSegments.isEmpty()) return

        // No intersections
        if ((clean and 1) != 0) {
            val segment = ringSegments[0]
            val m = segment.lastIndex
            if (m > 0) {
                if (!polygonStarted) {
                    downstream.polygonStart()
                    polygonStarted = true
                }
                downstream.lineStart()
                (0 until m).forEach {
                    val currentSegmentPiece = segment[it]
                    val x = currentSegmentPiece[0]
                    val y = currentSegmentPiece[1]
                    downstream.point(x, y, 0.0)
                }
                downstream.lineEnd()
            }
            return
        }

        // Rejoin connected segments
        // TODO reuse ringBuffer.rejoin()?
        if (ringSegments.size > 1 && (clean and 2) != 0) {
            val concat = ringSegments.removeAt(ringSegments.lastIndex).toMutableList()
            concat.addAll(ringSegments.removeAt(0))
            ringSegments.add(concat)
        }

        segments.add(ringSegments.filter { it.size > 1 })
    }

    override fun polygonEnd() {
        pointContext = PointContext.DEFAULT
        lineStartContext = LineStartContext.DEFAULT
        lineEndContext = LineEndContext.DEFAULT

        val startInside = polygonContains(polygon, clipper.start)

        if (segments.isNotEmpty()) {
            if (!polygonStarted) {
                downstream.polygonStart()
                polygonStarted = true
            }
            rejoin(segments.flatten(), compareIntersection, startInside, clipper, downstream)
        } else if (startInside) {
            if (!polygonStarted) {
                downstream.polygonStart()
                polygonStarted = true
            }
            downstream.lineStart()
            clipper.interpolate(null, null, 1, downstream)
            downstream.lineEnd()
        }

        if (polygonStarted) {
            downstream.polygonEnd()
            polygonStarted = false
        }

        segments.clear()
        polygon.clear()
    }


    override fun sphere() {
        downstream.polygonStart()
        downstream.lineStart()
        clipper.interpolate(null, null, 1, downstream)
        downstream.lineEnd()
        downstream.polygonEnd()
    }


    private val compareIntersection = Comparator { i1, i2 ->
        val a = i1.point
        val b = i2.point
        val ca = if (a[0] < 0) a[1] - HALFPI - EPSILON else HALFPI - a[1]
        val cb = if (b[0] < 0) b[1] - HALFPI - EPSILON else HALFPI - b[1]
        ca.compareTo(cb)
    }
}


internal class BufferStream : Stream {
    private var lines: MutableList> = mutableListOf()
    private lateinit var line: MutableList

    override fun lineStart() {
        line = mutableListOf()
        lines.add(line)
    }

    override fun point(x: Double, y: Double, z: Double) {
        line.add(doubleArrayOf(x, y))
    }

    fun rejoin() {
        if (lines.size > 1) {
            val l = mutableListOf>()
            l.add(lines.removeAt(lines.lastIndex))
            l.add(lines.removeAt(0))
            lines.addAll(l)
        }
    }

    fun result(): MutableList> {
        val oldLines = lines
        lines = mutableListOf()
        return oldLines
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy