content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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 org.apache.hadoop.chukwa.hicc;
  
  import java.awt.Graphics;
  import java.awt.geom.AffineTransform;
  import java.awt.image.AffineTransformOp;
  import java.awt.image.BufferedImage;
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import javax.imageio.ImageIO;
  
  import org.apache.commons.logging.Log;
  import org.apache.commons.logging.LogFactory;
  import org.apache.hadoop.chukwa.util.ExceptionUtil;
  
  public class ImageSlicer {
    private BufferedImage src = null;
    private Log log = LogFactory.getLog(ImageSlicer.class);
    private String sandbox = System.getenv("CHUKWA_HOME")+File.separator+"webapps"+File.separator+"sandbox"+File.separator;
    private int maxLevel = 0;
    
    public ImageSlicer() {
    }
    
    /*
     * Prepare a large image for tiling.
     * 
     * Load an image from a file. Resize the image so that it is square,
     * with dimensions that are an even power of two in length (e.g. 512,
     * 1024, 2048, ...). Then, return it.
     * 
     */
    public BufferedImage prepare(String filename) {
      try {
        src = ImageIO.read(new File(filename));
      } catch (IOException e) {
        log.error("Image file does not exist:"+filename+", can not render image.");
      }
      XYData fullSize = new XYData(1, 1);
      while(fullSize.getX()<src.getWidth() || fullSize.getY()<src.getHeight()) {
        fullSize.set(fullSize.getX()*2, fullSize.getY()*2);
      }
      float scaleX = (float)fullSize.getX()/src.getWidth();
      float scaleY = (float)fullSize.getY()/src.getHeight();
      log.info("Image size: ("+src.getWidth()+","+src.getHeight()+")");
      log.info("Scale size: ("+scaleX+","+scaleY+")");
      
      AffineTransform at = 
        AffineTransform.getScaleInstance(scaleX,scaleY);
  
        //       AffineTransform.getScaleInstance((fullSize.getX()-src.getWidth())/2,(fullSize.getY()-src.getHeight())/2);
      AffineTransformOp op = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
      BufferedImage dest = op.filter(src, null);
      return dest;
    }
    
    /*
     * Extract a single tile from a larger image.
     * 
     * Given an image, a zoom level (int), a quadrant (column, row tuple;
     * ints), and an output size, crop and size a portion of the larger
     * image. If the given zoom level would result in scaling the image up,
     * throw an error - no need to create information where none exists.
     * 
     */
    public BufferedImage tile(BufferedImage image, int level, XYData quadrant, XYData size, boolean efficient) throws Exception {
      double scale = Math.pow(2, level);
      if(efficient) {
        /* efficient: crop out the area of interest first, then scale and copy it */
        XYData inverSize = new XYData((int)(image.getWidth(null)/(size.getX()*scale)), 
            (int)(image.getHeight(null)/(size.getY()*scale)));
        XYData topLeft = new XYData(quadrant.getX()*size.getX()*inverSize.getX(), 
            quadrant.getY()*size.getY()*inverSize.getY());
        XYData newSize = new XYData((size.getX()*inverSize.getX()), 
            (size.getY()*inverSize.getY()));
        if(inverSize.getX()<1.0 || inverSize.getY() < 1.0) {
          throw new Exception("Requested zoom level ("+level+") is too high.");
        }
        image = image.getSubimage(topLeft.getX(), topLeft.getY(), newSize.getX(), newSize.getY());
        BufferedImage zoomed = new BufferedImage(size.getX(), size.getY(), BufferedImage.TYPE_INT_RGB);
        zoomed.getGraphics().drawImage(image, 0, 0, size.getX(), size.getY(), null);
        if(level>maxLevel) {
          maxLevel = level;
        }
        return zoomed;
      } else {
        /* inefficient: copy the whole image, scale it and then crop out the area of interest */
        XYData newSize = new XYData((int)(size.getX()*scale), (int)(size.getY()*scale));
        XYData topLeft = new XYData(quadrant.getX()*size.getX(), quadrant.getY()*size.getY());
        if(newSize.getX() > image.getWidth(null) || newSize.getY() > image.getHeight(null)) {
          throw new Exception("Requested zoom level ("+level+") is too high.");
        }
        AffineTransform tx = new AffineTransform();
        AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
        tx.scale(scale, scale);
        image = op.filter(image, null);
        BufferedImage zoomed = image.getSubimage(topLeft.getX(), topLeft.getY(), newSize.getX(), newSize.getY());
        if(level>maxLevel) {
          maxLevel = level;
        }
        return zoomed;
      }
    }
    
    /*
     * Recursively subdivide a large image into small tiles.
     * 
     * Given an image, a zoom level (int), a quadrant (column, row tuple;
     * ints), and an output size, cut the image into even quarters and
     * recursively subdivide each, then generate a combined tile from the
     * resulting subdivisions. If further subdivision would result in
     * scaling the image up, use tile() to turn the image itself into a
     * tile.
     */
    public BufferedImage subdivide(BufferedImage image, int level, XYData quadrant, XYData size, String prefix) {
      if(image.getWidth()<=size.getX()*Math.pow(2, level)) {
        try {
          BufferedImage outputImage = tile(image, level, quadrant, size, true);
          write(outputImage, level, quadrant, prefix);
          return outputImage;
        } catch (Exception e) {
          log.error(ExceptionUtil.getStackTrace(e));
        }
      }
      
      BufferedImage zoomed = new BufferedImage(size.getX()*2, size.getY()*2, BufferedImage.TYPE_INT_RGB);
      Graphics g = zoomed.getGraphics();
      XYData newQuadrant = new XYData(quadrant.getX() * 2 + 0, quadrant.getY() * 2 + 0);
      g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), 0, 0, null);
      newQuadrant = new XYData(quadrant.getX()*2 + 0, quadrant.getY()*2 + 1);
      g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), 0, size.getY(), null);
      newQuadrant = new XYData(quadrant.getX()*2 + 1, quadrant.getY()*2 + 0);
      g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), size.getX(), 0, null);
      newQuadrant = new XYData(quadrant.getX()*2 + 1, quadrant.getY()*2 + 1);
      g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), size.getX(), size.getY(), null);
      BufferedImage outputImage = new BufferedImage(size.getX(), size.getY(), BufferedImage.TYPE_INT_RGB);
      outputImage.getGraphics().drawImage(zoomed, 0, 0, size.getX(), size.getY(), null);
      write(outputImage, level, quadrant, prefix);
      return outputImage;    
    }
    
    /*
     * Write image file.
     */
    public void write(BufferedImage image, int level, XYData quadrant, String prefix) {
      StringBuilder outputFile = new StringBuilder();
      outputFile.append(sandbox);
      outputFile.append(File.separator);
      outputFile.append(prefix);
      outputFile.append("-");
      outputFile.append(level);
      outputFile.append("-");
      outputFile.append(quadrant.getX());
      outputFile.append("-");
      outputFile.append(quadrant.getY());
      outputFile.append(".png");
      FileOutputStream fos;
      try {
        fos = new FileOutputStream(outputFile.toString());
        ImageIO.write(image, "PNG", fos);
        fos.close();   
      } catch (IOException e) {
        log.error(ExceptionUtil.getStackTrace(e));
      }
    }
    
    public int process(String filename) {
      Pattern p = Pattern.compile("(.*)\\.(.*)");
      Matcher m = p.matcher(filename);
      if(m.matches()) {
        String prefix = m.group(1);
        String fullPath = sandbox + File.separator + filename;
        subdivide(prepare(fullPath), 0, new XYData(0, 0), new XYData(256, 256), prefix);
        return maxLevel;
      }
      return 0;
    }
  
  }
  
  class XYData {
    private int x = 0;
    private int y = 0;
    
    public XYData(int x, int y) {
      this.x=x;
      this.y=y;
    }
  
    public void set(int x, int y) {
      this.x=x;
      this.y=y;
    }
    
    public int getX() {
      return x;
    }
  
    public int getY() {
      return y;
    }
    
  }
  