# 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 = numarray.Float64,
                    output_type = None):
    """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. An output array can optionally be provided. The
    order of the spline must be >= 2 and <= 5.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    input = numarray.asarray(input)
    if output_type == None:
        output_type = input.type()
    output, return_value = _ni_support._get_output(output, input.shape, 
                                                   output_type)
    if order in [0, 1]:
        output[...] = numarray.array(input)
    else:
        _nd_image.spline_filter1d(input, order, axis, output)
    return return_value


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

    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.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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"
    output, return_value = _ni_support._get_output(output, input.shape,
                                                   output_type) 
    if order not in [0, 1] and input.rank > 0:
        for axis in range(input.rank):
            spline_filter1d(input, order, axis, output = output)
            input = output
    else:
        output[...] = input[...]
    return return_value

def geometric_transform(input, mapping, output_shape = None,
                        output_type = None, output = None, order = 3,
                        mode = 'constant', cval = 0.0,
                        prefilter = True,
                     extra_arguments = (), extra_keywords = {}):
    """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 can optionally be given. If
    not given is equal to the input shape. Optionally
    an output array can be provided that must match the requested
    output shape. The parameter prefilter determines if the
    input is pre-filtered before interpolation, if False it is assumed
    that the input is already filtered.  The extra_arguments and
    extra_keywords arguments can be used to pass extra arguments and
    keywords that are passed to the mapping function at each call.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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 = numarray.Float64)
    else:
        filtered = input
    output, return_value = _ni_support._get_output(output, output_shape, 
                                                   output_type)
    _nd_image.geometric_transform(filtered, mapping, output, order, mode,
                                  cval, extra_arguments, extra_keywords)
    return return_value


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. Optionally an output array can be provided that must match the 
    array of coordinates. The parameter prefilter determines if the input 
    is pre-filtered before interpolation, if False it is assumed that the
    input is already filtered.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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 = numarray.Float64)
    else:
        filtered = input
    output, return_value = _ni_support._get_output(output, output_shape, 
                                                   output_type)
    _nd_image.map_coordinates(filtered, coordinates, output,
                              order, mode, cval)
    return return_value


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 can optionally be given. If not given it 
    us equal to the input shape. Optionally an output array can be 
    provided that must match the requested output shape. 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.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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 = numarray.Float64)
    else:
        filtered = input
    output, return_value = _ni_support._get_output(output, output_shape, 
                                                   output_type)
    _nd_image.affine_transform(filtered, matrix, offset, output, order,
                               mode, cval)
    return return_value
    

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. Optionally an output array can be provided 
    that must match the requested output shape. The parameter prefilter 
    determines if the input is pre-filtered before interpolation, if False 
    it is assumed that the input is already filtered.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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 = numarray.Float64)
    else:
        filtered = input
    output, return_value = _ni_support._get_output(output, input.shape, 
                                                   output_type)
    _nd_image.shift(filtered, shift, output, order, mode, cval)
    return return_value


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. Optionally an output array can be provided 
    that must match the requested output shape. The parameter prefilter 
    determines if the input is pre-filtered before interpolation, if False 
    it is assumed that the input is already filtered.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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 = [int(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 = numarray.Float64)
    else:
        filtered = input
    output, return_value = _ni_support._get_output(output, output_shape, 
                                                   output_type)
    _nd_image.zoom(filtered, zoom,  output, order, mode, cval)
    return return_value


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. 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.
    """
    output, output_type = _ni_support.decrepate(output, output_type)
    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)

