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

io.data2viz.math.Matrix.kt Maven / Gradle / Ivy

/*
 * 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.math

import io.data2viz.geom.*

/**
 * An affine transformation matrix performs a linear mapping from 2D
 * coordinates to other 2D coordinates that preserves the "straightness" and
 * "parallelness" of lines.
 *
 * Such a coordinate transformation can be represented by a 3 row by 3
 * column matrix with an implied last row of `[ 0 0 1 ]`. This matrix
 * transforms source coordinates `(x, y)` into destination coordinates `(x',y')`
 * by considering them to be a column vector and multiplying the coordinate
 * vector by the matrix according to the following process:
 *
 *     [ x ]   [ a  c  tx ] [ x ]   [ a * x + c * y + tx ]
 *     [ y ] = [ b  d  ty ] [ y ] = [ b * x + d * y + ty ]
 *     [ 1 ]   [ 0  0  1  ] [ 1 ]   [         1          ]
 *
 * Note the locations of b and c.
 *
 * This class is optimized for speed and minimizes calculations based on its
 * knowledge of the underlying matrix (as opposed to say simply performing
 * matrix multiplication).
 *
 * todo should we accept non invertible matrix (scale = .0 or infinite transformation params)?
 * https://math.stackexchange.com/questions/2875241/what-does-a-non-invertible-affine-transformation-look-like-geometrically-in-term
 *
 * todo make it immutable?
 */
data class Matrix(
    internal var a: Double = 1.0,
    internal var b: Double = 0.0,
    internal var c: Double = 0.0,
    internal var d: Double = 1.0,
    internal var tx: Double = 0.0,
    internal var ty: Double = 0.0
) {

    /**
     * Change transformation parameters to make the transformation an identity.
     */
    fun reset(): Matrix {
        a = 1.0
        d = 1.0
        b = .0
        c = .0
        tx = .0
        ty = .0
        return this
    }

    fun isIdentity() = (a == 1.0
                    && b == .0
                    && c == .0
                    && d == 1.0
                    && tx == .0
                    && ty == .0)


    /**
     * Appends the specified matrix to this matrix. This is the equivalent of
     * multiplying `(this matrix) * (specified matrix)`.
     *
     * @param {Matrix} matrix the matrix to append
     * @return {Matrix} this matrix, modified
     */
    fun append (other: Matrix): Matrix {
        val a1 = a
        val b1 = b
        val c1 = c
        val d1 = d
        val a2 = other.a
        val b2 = other.b
        val c2 = other.c
        val d2 = other.d
        val tx2 = other.tx
        val ty2 = other.ty

        a = a2 * a1 + c2 * c1
        c = b2 * a1 + d2 * c1
        b = a2 * b1 + c2 * d1
        d = b2 * b1 + d2 * d1
        tx += tx2 * a1 + ty2 * c1
        ty += tx2 * b1 + ty2 * d1
        return this
    }

    /**
     * Prepends the specified matrix to this matrix. This is the equivalent of
     * multiplying `(specified matrix) * (this matrix)`.
     *
     * @param {Matrix} matrix the matrix to prepend
     * @return {Matrix} this matrix, modified
     */
    fun prepend(mx: Matrix): Matrix {
            val a1 = a
            val b1 = b
            val c1 = c
            val d1 = d
            val tx1 = tx
            val ty1 = ty
            val a2 = mx.a
            val b2 = mx.c
            val c2 = mx.b
            val d2 = mx.d
            val tx2 = mx.tx
            val ty2 = mx.ty
            a = a2 * a1 + b2 * b1
            c = a2 * c1 + b2 * d1
            b = c2 * a1 + d2 * b1
            d = c2 * c1 + d2 * d1
            tx = a2 * tx1 + b2 * ty1 + tx2
            ty = c2 * tx1 + d2 * ty1 + ty2
        return this
    }

    /**
     * Add a translation to the current transformation.
     */
    fun translate(pt: Point) = translate(pt.x, pt.y)

    /**
     * Add a translation to the current transformation.
     */
    fun translate(x: Double, y: Double): Matrix {
        tx += x * a + y * c
        ty += x * b + y * d
        return this
    }

    /**
     * Add a scale transformation to the current one, using the same scale
     * factor for X and Y and an optional point f
     */
    fun scale(scaleXY: Double, center: Point? = null) = scale(scaleXY, scaleXY, center)

    /**
     * Add a scale transformation to the current one, using the same scale
     * factor for X and Y
     */
    fun scale(scaleX: Double, scaleY: Double, center: Point? = null): Matrix {

        require(scaleX != .0) { "$scaleX should be different than 0.0 to ensure the matrix is invertible "}
        require(scaleY != .0) { "$scaleY should be different than 0.0 to ensure the matrix is invertible "}

        center?.let {
            translate(it)
        }
        a *= scaleX
        b *= scaleX
        c *= scaleY
        d *= scaleY
        center?.let {
            translate(-it)
        }
        return this
    }

    /**
     * Add a rotation to the current transformation. The rotation is done
     * using the optional center point as pivot. Without a center parameter,
     * the rotation uses the
     */
    fun rotate(angle: Angle, center: Point? = null): Matrix {
        val cos = angle.cos
        val sin = angle.sin
        val tempA = a
        val tempB = b
        val tempC = c
        val tempD = d

        a = cos * tempA + sin * tempC
        b = cos * tempB + sin * tempD
        c = -sin * tempA + cos * tempC
        d = -sin * tempB + cos * tempD

        center?.let {
            val x = it.x
            val y = it.y
            val tempTx = x - x * cos + y * sin
            val tempTy = y - x * sin - y * cos
            tx += tempTx * tempA + tempTy * tempC
            ty += tempTx * tempB + tempTy * tempD
        }
        return this
    }

    /**
     * Apply the current transformation matrix to a point.
     * @return the point coordinates after apply the transformation.
     */
    fun transform(point: Point) = point(
        point.x * a + point.y * c + tx,
        point.x * b + point.y * d + ty
    )

    /**
     * Apply the inverse transformations
     * @return the point coordinates after apply the inverse transformation.
     */
    fun inverseTransform(point: Point):Point {
        //matrix should always be invertible
        val x = point.x - tx
        val y = point.y - ty
        val det = a * d  - b * c
        return point(
            (x * d - y * c) / det,
            (y * a - x * b) / det)
    }

    /**
     * Checks whether the matrix is invertible. A matrix is not invertible if
     * the determinant is 0 or any value is infinite or NaN.
     *
     * @return {Boolean} whether the matrix is invertible
     */
    inline internal fun isInvertible(): Boolean{
        val det = a * d - c * b
        return det != .0
                && !det.isNaN()
                && tx.isFinite()
                && ty.isFinite()
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy