# Copyright (C) 2003-2005 Peter J. Verveer
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met: 
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following
#    disclaimer in the documentation and/or other materials provided
#    with the distribution.
#
# 3. The name of the author may not be used to endorse or promote
#    products derived from this software without specific prior
#    written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.      

import types
import math
import numarray
import _ni_support
import _nd_image

def spline_filter1d(input, order = 3, axis = -1, output = None,
                    output_type = numarray.Float64):
    """Calculates a one-dimensional spline filter along the given axis.

    The lines of the array along the given axis are filtered by a
    spline filter. The type of the output can be forced by the type
    parameter. If equal to None, the output array type is equal to the
    input array type. An output array can optionally be provided. The
    order of the spline must be >= 2 and <= 5.
    """
    inplace = isinstance(output, numarray.NumArray)
    if order in [0, 1]:
        if inplace:
            output[...] = numarray.array(input).astype(output_type)[...]
        else:
            output = numarray.array(input).copy().astype(output_type)
    else:
        output = _nd_image.spline_filter1d(input, order, axis, output,
                                     _ni_support._type_to_num[output_type])
    if not inplace:
        return output
        


def spline_filter(input, order = 3, output = None,
                  output_type = numarray.Float64):
    """Multi-dimensional spline filter.

    The data type of the output array can be specified. The data type
    of the output is equal to the input type if the output_type
    argument is equal to None. Optionally an output array can be
    provided.

    Note: The multi-dimensional filter is implemented as a sequence of
    one-dimensional spline filters. The intermediate arrays are stored
    in the same data type as the output. Therefore, for output types
    with a limited precision, the results may be imprecise because
    intermediate results may be stored with insufficient precision.
    """
    inplace = isinstance(output, numarray.NumArray)
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    done = False
    # loop over the dimensions
    for axis in range(input.rank):
        if order in [0, 1]:
            continue
        # zero filter sizes are skipped:
        if done:
            # if one or more axes are already processed, work in-place:
            spline_filter1d(output, order, axis, output_type = None,
                            output = output)
        else:
            if inplace:
                spline_filter1d(input, order, axis,
                               output_type = output_type, output = output)
            else:
                output = spline_filter1d(input, order, axis,
                                         output_type = output_type)
        done = True
    if not done: # if no filter was applied, make a copy:
        if inplace:
            output[...] = numarray.array(input).astype(output_type)[...]
        else:
            output = numarray.array(input).copy().astype(output_type)
    if not inplace:
        return output
    

def geometric_transform(input, mapping, output_shape = None,
                        output_type = None, output = None, order = 3,
                        mode = 'constant', cval = 0.0,
                        prefilter = True):
    """Apply an arbritrary geometric transform.

    The given mapping function is used to find for each point in the
    output the corresponding coordinates in the input. The value of
    the input at those coordinates is determined by spline
    interpolation of the requested order. Points outside the
    boundaries of the input are filled according to the given
    mode. The output shape and output type can optionally be given. If
    not given they are equal to the input shape and type. Optionally
    an output array can be provided that must match the requested
    output shape and type. The parameter prefilter determines if the
    input is pre-filtered before interpolation, if False it is assumed
    that the input is already filtered.
    """
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    if output_shape == None:
        output_shape = input.shape
    mode = _ni_support._extend_mode_to_code(mode)
    if prefilter:
        filtered = spline_filter(input, order,
                                 output_type = numarray.Float64)
    else:
        filtered = input
    return _nd_image.geometric_transform(filtered, mapping, output_shape,
          _ni_support._type_to_num[output_type], output, order, mode, cval)


def map_coordinates(input, coordinates, output_type = None, output = None,
                    order = 3, mode = 'constant', cval = 0.0,
                    prefilter = True):
    """Apply an arbritrary coordinate transformation.

    The array of coordinates is used to find for each point in the
    output the corresponding coordinates in the input. The value of
    the input at that coordinates is determined by spline
    interpolation of the requested order. Points outside the
    boundaries of the input are filled according to the given
    mode. The output type can optionally be given. If not given it is
    equal to the input type. Optionally an output array can be
    provided that must match the array of coordinates and the
    requested type. The parameter prefilter determines if the input is
    pre-filtered before interpolation, if False it is assumed that the
    input is already filtered.
    """
    input = numarray.asarray(input)
    coordinates = numarray.asarray(coordinates)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    output_shape = coordinates.shape[1:]
    mode = _ni_support._extend_mode_to_code(mode)
    if prefilter:
        filtered = spline_filter(input, order,
                                 output_type = numarray.Float64)
    else:
        filtered = input
    return _nd_image.map_coordinates(filtered, coordinates, output_shape,
          _ni_support._type_to_num[output_type], output, order, mode, cval)


def affine_transform(input, matrix, offset = 0.0, output_shape = None,
                     output_type = None, output = None, order = 3,
                     mode = 'constant', cval = 0.0, prefilter = True):
    """Apply an affine transformation.

    The given matrix and offset are used to find for each point in the
    output the corresponding coordinates in the input by an affine
    transformation. The value of the input at those coordinates is
    determined by spline interpolation of the requested order. Points
    outside the boundaries of the input are filled according to the
    given mode. The output shape and output type can optionally be
    given. If not given they are equal to the input shape and
    type. Optionally an output array can be provided that must match
    the requested output shape and type. The parameter prefilter
    determines if the input is pre-filtered before interpolation, if
    False it is assumed that the input is already filtered.

    The matrix must be two-dimensional or can also be given as a
    one-dimensional sequence or array. In the latter case, it is
    assumed that the matrix is diagonal. A more efficient algorithms
    is then applied that exploits the separability of the problem.
    """
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    if output_shape == None:
        output_shape = input.shape
    mode = _ni_support._extend_mode_to_code(mode)
    offset = _ni_support._normalize_sequence(offset, input)
    if prefilter:
        filtered = spline_filter(input, order,
                                 output_type = numarray.Float64)
    else:
        filtered = input
    return _nd_image.affine_transform(filtered, matrix, offset,
                     output_shape, _ni_support._type_to_num[output_type],
                     output, order, mode, cval)

def shift(input, shift, output_type = None, output = None, order = 3,
          mode = 'constant', cval = 0.0, prefilter = True):
    """Shift an array.

    The array is shifted using spline interpolation of the requested
    order. Points outside the boundaries of the input are filled
    according to the given mode. The output type can optionally be
    given. If not given it is equal to the input type. Optionally an
    output array can be provided that must match the requested output
    shape and type. The parameter prefilter determines if the input is
    pre-filtered before interpolation, if False it is assumed that the
    input is already filtered.
    """
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    mode = _ni_support._extend_mode_to_code(mode)
    shift = _ni_support._normalize_sequence(shift, input)
    shift = [-ii for ii in shift]
    if prefilter:
        filtered = spline_filter(input, order,
                                 output_type = numarray.Float64)
    else:
        filtered = input
    return _nd_image.shift(filtered, shift, input.shape, 
                           _ni_support._type_to_num[output_type],
                           output, order, mode, cval)


def zoom(input, zoom, output_type = None, output = None, order = 3,
         mode = 'constant', cval = 0.0, prefilter = True):
    """Zoom an array.

    The array is zoomed using spline interpolation of the requested
    order. Points outside the boundaries of the input are filled
    according to the given mode. The output type can optionally be
    given. If not given it is equal to the input type. Optionally an
    output array can be provided that must match the requested output
    shape and type. The parameter prefilter determines if the input is
    pre-filtered before interpolation, if False it is assumed that the
    input is already filtered.
    """
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    if isinstance(output_type, numarray.ComplexType):
        raise RuntimeError, "complex array types not supported"
    mode = _ni_support._extend_mode_to_code(mode)
    zoom = _ni_support._normalize_sequence(zoom, input)
    output_shape = [ii[0] * ii[1] for ii in zip(input.shape, zoom)]
    zoom = [1.0 / ii for ii in zoom]
    if prefilter:
        filtered = spline_filter(input, order,
                                 output_type = numarray.Float64)
    else:
        filtered = input
    return _nd_image.zoom(filtered, zoom, output_shape,
                          _ni_support._type_to_num[output_type],
                          output, order, mode, cval)

def _minmax(coor, minc, maxc):
    if coor[0] < minc[0]:
        minc[0] = coor[0]
    if coor[0] > maxc[0]:
        maxc[0] = coor[0]
    if coor[1] < minc[1]:
        minc[1] = coor[1]
    if coor[1] > maxc[1]:
        maxc[1] = coor[1]
    return minc, maxc

def rotate(input, angle, axes = (-1, -2), reshape = True,
           output_type = None, output = None, order = 3,
           mode = 'constant', cval = 0.0, prefilter = True):
    """Rotate an array.

    The array is rotated in the plane definde by the two axes given
    by the axes parameter using spline interpolation of the requested
    order. The angle is given in degrees. Points outside the
    boundaries of the input are filled according to the given
    mode. The output type can optionally be given. If not given it is
    equal to the input type. If reshape is true, the output shape is
    adapted so that the input array is contained completely in the
    output. Optionally an output array can be provided that must
    match the requested output shape and type. The parameter
    prefilter determines if the input is pre-filtered before
    interpolation, if False it is assumed that the input is already
    filtered.
    """
    input = numarray.asarray(input)
    angle = numarray.pi / 180 * angle
    if axes[0] < axes[1]:
        a1 = axes[0]
        a2 = axes[1]
    else:
        a1 = axes[1]
        a2 = axes[0]
    m11 = math.cos(angle)
    m12 = math.sin(angle)
    m21 = -math.sin(angle)
    m22 = math.cos(angle)
    matrix = numarray.identity(input.rank, type = numarray.Float64)
    matrix[a1, a1] = m11
    matrix[a1, a2] = m12
    matrix[a2, a1] = m21
    matrix[a2, a2] = m22
    oshape = list(input.shape)
    ix = input.shape[a1]
    iy = input.shape[a2]
    if reshape:
        mtrx = [[m11, -m21], [-m12, m22]]
        minc = [0, 0]
        maxc = [0, 0]
        coor = numarray.matrixmultiply(mtrx, [0, ix])
        minc, maxc = _minmax(coor, minc, maxc)
        coor = numarray.matrixmultiply(mtrx, [iy, 0])
        minc, maxc = _minmax(coor, minc, maxc)
        coor = numarray.matrixmultiply(mtrx, [iy, ix])
        minc, maxc = _minmax(coor, minc, maxc)
        oy = int(maxc[0] - minc[0] + 0.5)
        ox = int(maxc[1] - minc[1] + 0.5)
        oshape[a1] = ox
        oshape[a2] = oy
    else:
        ox = oshape[a1]
        oy = oshape[a2]
    offset = numarray.zeros((input.rank,), type = numarray.Float64)
    offset[a1] = float(ox) / 2.0 - 0.5
    offset[a2] = float(oy) / 2.0 - 0.5
    offset = numarray.matrixmultiply(matrix, offset)
    tmp = numarray.zeros((input.rank,), type = numarray.Float64)
    tmp[a1] = float(ix) / 2.0 - 0.5
    tmp[a2] = float(iy) / 2.0 - 0.5
    offset = tmp - offset
    return affine_transform(input, matrix, offset, oshape, output_type,
                            output, order, mode, cval, prefilter)

