/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2003, 2ie Technologie
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.image.jai;

// J2SE dependencies
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.Map;

// JAI dependencies
import javax.media.jai.ImageLayout;
import javax.media.jai.UntiledOpImage;
import javax.media.jai.iterator.RandomIter;
import javax.media.jai.iterator.RandomIterFactory;


/**
 * Effectue un seuillage par hysteresis sur une image.
 * L'opération de seuillage s'effectue de la manière suivante:
 * <p>
 * On dispose d'un seuil haut, <var>sh</var>, et d'un seuil bas, <var>sb</var>.
 * Si la valeur d'un pixel est supérieur à <var>sh</var>, on la conserve, elle
 * nous interesse. Si cette valeur est inférieure à <var>sb</var>, on la supprime.
 * Si elle est entre les deux on dit le pixel indeterminé et on ne le conserve que
 * s'il est proche d'un pixel dont la valeur est au dessus de <var>sh</var>, ou
 * proche d'un pixel indéterminé que l'on a précédement trouvé proche d'un pixel
 * de valeur supérieure à <var>sh</var>. Cette recherche se fait de manière itérative,
 * jusqu'à ce que le point indéterminé n'est plus de voisins satisfaisants.
 * 
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/coverage/src/main/java/org/geotools/image/jai/Hysteresis.java $
 * @version $Id: Hysteresis.java 20970 2006-08-11 07:53:22Z jgarnett $
 * @author Lionel Flahaut
 * @author Martin Desruisseaux
 */
public class Hysteresis extends UntiledOpImage {
    /**
     * The low threshold value, inclusive.
     */
    private final double low;

    /**
     * The high threshold value, inclusive.
     */
    private final double high;

    /**
     * The value to give to filtered pixel.
     */
    private final double padValue;

    /**
     * Constructs a new Hysterisis filter for the given image.
     *
     * @param source   The source image.
     * @param layout   The image layout.
     * @param map      The rendering hints and image properties.
     * @param low      The low threshold value, inclusive.
     * @param high     The high threshold value, inclusive.
     * @param padValue The value to give to filtered pixel.
     */
    protected Hysteresis(final RenderedImage source,
                         final ImageLayout   layout,
                         final Map           map,
                         final double        low,
                         final double        high,
                         final double        padValue)
    {
        super(source, map, layout);
        this.low      = low;
        this.high     = high;
        this.padValue = padValue;
    }

    /**
     * Computes the whole image.
     */
    protected void computeImage(final Raster[]    sources,
                                final WritableRaster dest,
                                final Rectangle  destRect)
    {
        assert sources.length == 1;
        final Raster source = sources[0];
        Rectangle sourceRect = mapDestRect(destRect, 0);
        sourceRect = sourceRect.intersection(source.getBounds());
        final RandomIter iter = RandomIterFactory.create(source, sourceRect);
        final int minX = destRect.x;              // Minimum inclusive
        final int minY = destRect.y;              // Minimum inclusive
        final int maxX = destRect.width  + minX;  // Maximum exclusive
        final int maxY = destRect.height + minY;  // Maximum exclusive
        final int w    = width -1;
        final int h    = height-1;
        final boolean[] accepted = new boolean[destRect.width * destRect.height];
        final boolean[] rejected = new boolean[destRect.width * destRect.height];
        for (int band=source.getNumBands(); --band>=0;) {
            /*
             * Find immediately all accepted values (above the high threshold) and rejected values
             * (below the low threshold).    NaN values are both accepted and rejected ("accepted"
             * since they are going to be copied in the destination image, and "rejected" since
             * they do not cause the acceptation of neighbor values).
             */
            int index = 0;
            for (int y=minY; y<maxY; y++) {
                for (int x=minX; x<maxX; x++) {
                    final double current = iter.getSampleDouble(x, y, band);
                    accepted[index] = !(current < high); // Accept NaN values
                    rejected[index] = !(current >= low); // Accept NaN values
                    index++;
                }
            }
            assert index == accepted.length;
            /*
             * Complete the mask of "accepted" values. Unknow values (those which are neither
             * accepted or rejected) are tested for their proximity with an accepted value.
             * This loop will be run until there is no change.
             */
            int sign = +1;
            boolean changed;
            do {
                changed = false;
                final int stop;
                if (sign >= 0) {
                    index = 0;
                    stop  = accepted.length;
                } else {
                    index = accepted.length-1;
                    stop  = -1;
                }
                while (index != stop) {
                    if (!accepted[index] && !rejected[index]) {
                        int check;
                        final int y = index / width;
                        final int x = index % width;
                        if ((x!=0 && ((accepted[check=index-1      ] && !rejected[check])   ||
                            (y!=0 &&   accepted[check=index-1-width] && !rejected[check])   ||
                            (y!=h &&   accepted[check=index-1+width] && !rejected[check]))) ||
                            (x!=w && ((accepted[check=index+1      ] && !rejected[check])   ||
                            (y!=0 &&   accepted[check=index+1-width] && !rejected[check])   ||
                            (y!=h &&   accepted[check=index+1+width] && !rejected[check]))) ||
                            (y!=0 && ((accepted[check=index  -width] && !rejected[check]))) ||
                            (y!=w && ((accepted[check=index  +width] && !rejected[check]))))
                        {
                            accepted[index] = true;
                            changed = true;
                        }
                    }
                    index += sign;
                }
                sign = -sign;
            } while (changed);
            /*
             * Copy all accepted values in the destination raster.
             * Other values are replaced by NaN.
             */
            index = 0;
            for (int y=minY; y<maxY; y++) {
                for (int x=minX; x<maxX; x++) {
                    dest.setSample(x, y, band,
                                   accepted[index++] ? iter.getSampleDouble(x, y, band) : padValue);
                }
            }
            assert index == accepted.length;
        }
        iter.done();
    }
}
