(* ETH Oberon, Copyright 2001 ETH Zuerich Institut fuer Computersysteme, ETH Zentrum, CH-8092 Zuerich.
Refer to the "General ETH Oberon System Source License" contract available at: http://www.oberon.ethz.ch/ *)

MODULE GfxMatrix; (** portable *)	(* eos  *)
(** AUTHOR "eos"; PURPOSE "Affine transformations in 2D"; *)

	(*
		21.02.2000 - bugfix in Scaled: didn't recognize downscaling
		15.04.2000 - added Atan2
	*)

	IMPORT
		Streams, Math;


	CONST
		Eps = 1.0E-5;


	TYPE
		(**
			Transformation matrix
				3x2_matrices can represent any combination of affine transformations, i.e. of translation, rotation, scaling and
				shearing.

			Translate by tx, ty:
				[  1  0 ]
				[  0  1 ]
				[ tx ty ]

			Scale by sx, sy:
				[ sx   0 ]
				[  0  sy ]
				[  0   0 ]

			Rotate counter-clockwise by angle phi:
				[  cos(phi)   sin(phi) ]
				[ -sin(phi)  cos(phi) ]
				[       0             0       ]

			Shear along x_axis by factor f:
				[ 1   0 ]
				[ f    1 ]
				[ 0   0 ]
		**)

		Matrix* = ARRAY 3, 2 OF REAL;


	VAR
		Identity*: Matrix;	(** identity matrix (read_only) **)


	(**--- Matrix Computation ---**)

	(** initialize matrix with given values **)
	PROCEDURE Init* (VAR m: Matrix; m00, m01, m10, m11, m20, m21: REAL);
	BEGIN
		m[0, 0] := m00; m[0, 1] := m01;
		m[1, 0] := m10; m[1, 1] := m11;
		m[2, 0] := m20; m[2, 1] := m21
	END Init;

	(**
		Procedures Get3PointTransform, Get2PointTransform and Invert may not be able to find a solution. In that case,
		they return a singular matrix with all elements set to zero.
	**)

	(** calculate matrix that maps p0 to p1, q0 to q1, and r0 to r1 **)
	PROCEDURE Get3PointTransform* (px0, py0, px1, py1, qx0, qy0, qx1, qy1, rx0, ry0, rx1, ry1: REAL; VAR res: Matrix);
		VAR m: ARRAY 6, 7 OF REAL; i, j, k: LONGINT; max, t: REAL; v: ARRAY 6 OF REAL;
	BEGIN
		(* initialize set of linear equations for matrix coefficients *)
		m[0, 0] := px0; m[0, 1] := py0; m[0, 2] := 1.0; m[0, 3] := 0.0; m[0, 4] := 0.0; m[0, 5] := 0.0; m[0, 6] := px1;
		m[1, 0] := qx0; m[1, 1] := qy0; m[1, 2] := 1.0; m[1, 3] := 0.0; m[1, 4] := 0.0; m[1, 5] := 0.0; m[1, 6] := qx1;
		m[2, 0] := rx0; m[2, 1] := ry0; m[2, 2] := 1.0; m[2, 3] := 0.0; m[2, 4] := 0.0;  m[2, 5] := 0.0; m[2, 6] := rx1;
		m[3, 0] := 0.0; m[3, 1] := 0.0; m[3, 2] := 0.0; m[3, 3] := px0; m[3, 4] := py0; m[3, 5] := 1.0; m[3, 6] := py1;
		m[4, 0] := 0.0; m[4, 1] := 0.0; m[4, 2] := 0.0; m[4, 3] := qx0; m[4, 4] := qy0; m[4, 5] := 1.0; m[4, 6] := qy1;
		m[5, 0] := 0.0; m[5, 1] := 0.0; m[5, 2] := 0.0; m[5, 3] := rx0; m[5, 4] := ry0; m[5, 5] := 1.0; m[5, 6] := ry1;

		(* Gaussian elimination with pivoting *)
		FOR i := 0 TO 5 DO
			k := i; max := ABS(m[i, i]);
			FOR j := i+1 TO 5 DO
				IF ABS(m[j, i]) > max THEN
					k := j; max := ABS(m[j, i])
				END
			END;
			IF max < Eps THEN	(* matrix is singular *)
				Init(res, 0, 0, 0, 0, 0, 0);
				RETURN
			END;
			IF k # i THEN	(* swap rows to bring largest element up *)
				FOR j := i TO 6 DO
					t := m[i, j]; m[i, j] := m[k, j]; m[k, j] := t
				END
			END;
			FOR k := i+1 TO 5 DO
				t := m[k, i]/m[i, i];
				FOR j := i+1 TO 6 DO
					m[k, j] := m[k, j] - t * m[i, j]
				END
			END
		END;

		(* solve equations *)
		FOR i := 5 TO 0 BY -1 DO
			t := m[i, 6];
			FOR j := i+1 TO 5 DO
				t := t - v[j] * m[i, j]
			END;
			v[i] := t/m[i, i]
		END;

		Init(res, v[0], v[3], v[1], v[4], v[2], v[5])
	END Get3PointTransform;

	(** calculate matrix that maps p0 to p1 and q0 to q1 **)
	PROCEDURE Get2PointTransform* (px0, py0, px1, py1, qx0, qy0, qx1, qy1: REAL; VAR res: Matrix);
		VAR rx0, ry0, rx1, ry1: REAL;
	BEGIN
		rx0 := px0 + py0 - qy0; ry0 := py0 + qx0 - px0;
		rx1 := px1 + py1 - qy1; ry1 := py1 + qx1 - px1;
		Get3PointTransform(px0, py0, px1, py1, qx0, qy0, qx1, qy1, rx0, ry0, rx1, ry1, res)
	END Get2PointTransform;

	(** calculate inverse of matrix **)
	PROCEDURE Invert* (m: Matrix; VAR res: Matrix);
		VAR det, inv: REAL;
	BEGIN
		det := m[0, 0] * m[1, 1] - m[0, 1] * m[1, 0];
		IF ABS(det) >= Eps THEN	(* matrix can be inverted; use Cramer's rule *)
			inv := 1/det;
			res[0, 0] := +inv * m[1, 1];
			res[0, 1] := -inv * m[0, 1];
			res[1, 0] := -inv * m[1, 0];
			res[1, 1] := +inv * m[0, 0];
			res[2, 0] := +inv * (m[1, 0] * m[2, 1] - m[1, 1] * m[2, 0]);
			res[2, 1] := +inv * (m[0, 1] * m[2, 0] - m[0, 0] * m[2, 1])
		ELSE
			Init(res, 0, 0, 0, 0, 0, 0)
		END
	END Invert;


	(**--- Detection of Special Cases ---**)

	(** return determinant of matrix **)
	PROCEDURE Det* (VAR m: Matrix): REAL;
	BEGIN
		RETURN m[0, 0] * m[1, 1] - m[0, 1] * m[1, 0]
	END Det;

	(** return whether matrix is singular **)
	PROCEDURE Singular* (VAR m: Matrix): BOOLEAN;
	BEGIN
		RETURN ABS(m[0, 0] * m[1, 1] - m[0, 1] * m[1, 0]) < Eps
	END Singular;

	(** return whether matrix changes vector lengths **)
	PROCEDURE Scaled* (VAR m: Matrix): BOOLEAN;
	BEGIN
		RETURN ABS(ABS(m[0, 0] * m[1, 1] - m[0, 1] * m[1, 0]) - 1) > Eps
	END Scaled;

	(** return whether matrix includes rotation, shear, or mirror transformation **)
	PROCEDURE Rotated* (VAR m: Matrix): BOOLEAN;
	BEGIN
		RETURN (m[0, 0] < -Eps) OR (m[1, 1] < -Eps) OR (ABS(m[0, 1]) > Eps) OR (ABS(m[1, 0]) > Eps)
	END Rotated;

	(** return whether matrices should be considered equal **)
	PROCEDURE Equal* (VAR m, n: Matrix): BOOLEAN;
	BEGIN
		RETURN
			(ABS(m[0, 0] - n[0, 0]) < Eps) & (ABS(m[0, 1] - n[0, 1]) < Eps) &
			(ABS(m[1, 0] - n[1, 0]) < Eps) & (ABS(m[1, 1] - n[1, 1]) < Eps) &
			(ABS(m[2, 0] - n[2, 0]) < Eps) & (ABS(m[2, 1] - n[2, 1]) < Eps)
	END Equal;


	(**--- Matrix Concatenation ---**)

	(**
		Combinations of single transformations are evaluated from left to right. Executing Translate, Rotate or Scale
		pre-concatenates a corresponding matrix to the left of the given matrix parameter. This has the effect that
		the new transformation is applied before all previously accumulated transformations. Every transformation is
		therefore executed in the context of the coordinate system defined by the concatenation of all transformations
		to its right.
	**)

	(** translation by (dx, dy) **)
	PROCEDURE Translate* (m: Matrix; dx, dy: REAL; VAR res: Matrix);
	BEGIN
		res[0, 0] := m[0, 0]; res[0, 1] := m[0, 1];
		res[1, 0] := m[1, 0]; res[1, 1] := m[1, 1];
		res[2, 0] := m[2, 0] + dx * m[0, 0] + dy * m[1, 0];
		res[2, 1] := m[2, 1] + dx * m[0, 1] + dy * m[1, 1]
	END Translate;

	(** scale by (sx, sy) **)
	PROCEDURE Scale* (m: Matrix; sx, sy: REAL; VAR res: Matrix);
	BEGIN
		res[0, 0] := sx * m[0, 0]; res[0, 1] := sx * m[0, 1];
		res[1, 0] := sy * m[1, 0]; res[1, 1] := sy * m[1, 1];
		res[2, 0] := m[2, 0]; res[2, 1] := m[2, 1]
	END Scale;

	(** scale at (ox, oy) by (sx, sy) **)
	PROCEDURE ScaleAt* (m: Matrix; ox, oy, sx, sy: REAL; VAR res: Matrix);
		VAR tx, ty: REAL;
	BEGIN
		res[0, 0] := sx * m[0, 0]; res[0, 1] := sx * m[0, 1];
		res[1, 0] := sy * m[1, 0]; res[1, 1] := sy * m[1, 1];
		tx := ox * (1-sx); ty := oy * (1-sy);
		res[2, 0] := tx * m[0, 0] + ty * m[1, 0] + m[2, 0];
		res[2, 1] := tx * m[0, 1] + ty * m[1, 1] + m[2, 1]
	END ScaleAt;

	(** rotate counter-clockwise by angle specified by its sine and cosine **)
	PROCEDURE Rotate* (m: Matrix; sin, cos: REAL; VAR res: Matrix);
	BEGIN
		res[0, 0] := cos * m[0, 0] + sin * m[1, 0]; res[0, 1] := cos * m[0, 1] + sin * m[1, 1];
		res[1, 0] := -sin * m[0, 0] + cos * m[1, 0]; res[1, 1] := -sin * m[0, 1] + cos * m[1, 1];
		res[2, 0] := m[2, 0]; res[2, 1] := m[2, 1]
	END Rotate;

	(** rotate counter-clockwise around (ox, oy) by angle specified by its sine and cosine **)
	PROCEDURE RotateAt* (m: Matrix; ox, oy, sin, cos: REAL; VAR res: Matrix);
		VAR tx, ty: REAL;
	BEGIN
		res[0, 0] := cos * m[0, 0] + sin * m[1, 0]; res[0, 1] := cos * m[0, 1] + sin * m[1, 1];
		res[1, 0] := -sin * m[0, 0] + cos * m[1, 0]; res[1, 1] := -sin * m[0, 1] + cos * m[1, 1];
		tx := ox * (1-cos) + oy * sin; ty := oy * (1-cos) - ox * sin;
		res[2, 0] := tx * m[0, 0] + ty * m[1, 0] + m[2, 0];
		res[2, 1] := tx * m[0, 1] + ty * m[1, 1] + m[2, 1]
	END RotateAt;

	(** concatenate matrices **)
	PROCEDURE Concat* (m, n: Matrix; VAR res: Matrix);
	BEGIN
		res[0, 0] := m[0, 0] * n[0, 0] + m[0, 1] * n[1, 0];
		res[0, 1] := m[0, 0] * n[0, 1] + m[0, 1] * n[1, 1];
		res[1, 0] := m[1, 0] * n[0, 0] + m[1, 1] * n[1, 0];
		res[1, 1] := m[1, 0] * n[0, 1] + m[1, 1] * n[1, 1];
		res[2, 0] := m[2, 0] * n[0, 0] + m[2, 1] * n[1, 0] + n[2, 0];
		res[2, 1] := m[2, 0] * n[0, 1] + m[2, 1] * n[1, 1] + n[2, 1]
	END Concat;


	(**--- Arctan of Vector ---**)

	PROCEDURE Atan2* (x, y: REAL): REAL;
		VAR phi: REAL;
	BEGIN
		IF (ABS(x) < 1.0) & (ABS(y) >= ABS(x * MAX(REAL))) THEN	(* y/x would overflow *)
			IF y >= 0.0 THEN phi := Math.pi/2
			ELSE phi := -Math.pi/2
			END
		ELSIF x > 0.0 THEN	(* 1st or 4th quadrant *)
			phi := Math.arctan(y/x)
		ELSIF x < 0.0 THEN	(* 2nd or 3rd quadrant *)
			phi := Math.arctan(y/x) + Math.pi
		END;
		RETURN phi
	END Atan2;


	(**--- Matrix Application ---**)

	(** apply transformation matrix to point **)
	PROCEDURE Apply* (VAR m: Matrix; xin, yin: REAL; VAR xout, yout: REAL);
	BEGIN
		xout := xin * m[0, 0] + yin * m[1, 0] + m[2, 0];
		yout := xin * m[0, 1] + yin * m[1, 1] + m[2, 1]
	END Apply;

	(** apply transformation matrix to vector (ignoring translation) **)
	PROCEDURE ApplyToVector* (VAR m: Matrix; xin, yin: REAL; VAR xout, yout: REAL);
	BEGIN
		xout := xin * m[0, 0] + yin * m[1, 0];
		yout := xin * m[0, 1] + yin * m[1, 1]
	END ApplyToVector;

	(** apply transformation matrix to distance **)
	PROCEDURE ApplyToDist* (VAR m: Matrix; din: REAL; VAR dout: REAL);
		VAR x, y: REAL;
	BEGIN
		x := din * m[0, 0]; y := din * m[0, 1];
		IF ABS(y) < 1.0E-3 THEN dout := x
		ELSE dout := Math.sqrt(x * x + y * y)
		END
	END ApplyToDist;

	(** apply transformation matrix to axis-aligned rectangle; result is enclosing axis-aligned rectangle **)
	PROCEDURE ApplyToRect* (VAR m: Matrix; ilx, ily, irx, iuy: REAL; VAR olx, oly, orx, ouy: REAL);
		VAR l, h: REAL;
	BEGIN
		olx := m[2, 0]; orx := m[2, 0];
		l := ilx * m[0, 0]; h := irx * m[0, 0];
		IF l <= h THEN olx := olx + l; orx := orx + h ELSE olx := olx + h; orx := orx + l END;
		l := ily * m[1, 0]; h := iuy * m[1, 0];
		IF l <= h THEN olx := olx + l; orx := orx + h ELSE olx := olx + h; orx := orx + l END;
		oly := m[2, 1]; ouy := m[2, 1];
		l := ilx * m[0, 1]; h := irx * m[0, 1];
		IF l <= h THEN oly := oly + l; ouy := ouy + h ELSE oly := oly + h; ouy := ouy + l END;
		l := ily * m[1, 1]; h := iuy * m[1, 1];
		IF l <= h THEN oly := oly + l; ouy := ouy + h ELSE oly := oly + h; ouy := ouy + l END
	END ApplyToRect;

	(** apply inverse of matrix to point **)
	PROCEDURE Solve* (VAR m: Matrix; u, v: REAL; VAR x, y: REAL);
		VAR det: REAL;
	BEGIN
		det := m[0, 0] * m[1, 1] - m[0, 1] * m[1, 0];
		IF ABS(det) >= Eps THEN	(* matrix can be inverted *)
			u := u - m[2, 0]; v := v - m[2, 1];
			x := (m[1, 1] * u - m[1, 0] * v)/det;
			y := (m[0, 0] * v - m[0, 1] * u)/det
		END
	END Solve;


	(**--- Matrix I/O ---**)

	PROCEDURE Write* (VAR w: Streams.Writer; VAR m: Matrix);
		VAR i: LONGINT;
	BEGIN
		FOR i := 0 TO 2 DO
			w.RawReal(m[i, 0]); w.RawReal(m[i, 1])
		END
	END Write;

	PROCEDURE Read* (VAR r: Streams.Reader; VAR m: Matrix);
		VAR i: LONGINT;
	BEGIN
		FOR i := 0 TO 2 DO
			r.RawReal(m[i, 0]); r.RawReal(m[i, 1])
		END
	END Read;


BEGIN
	Init(Identity, 1, 0, 0, 1, 0, 0)
END GfxMatrix.
