/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2012, Geomatys * * 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; * version 2.1 of the License. * * 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.geotoolkit.image.relief; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.NullArgumentException; import org.geotoolkit.image.io.large.WritableLargeRenderedImage; import org.geotoolkit.image.iterator.PixelIterator; import org.geotoolkit.image.iterator.PixelIteratorFactory; import org.opengis.referencing.cs.AxisDirection; /** * Travel a source image and a dem (Digital Elevation Model) image to compute shadow.<br/> * * To Do : replace destination {@link BufferedImage} to destination {@link WritableLargeRenderedImage}. * @author Rémi Marechal (Geomatys). */ public final class ReliefShadow { private static final double PI = Math.PI; /** * Table which contain (X, Y) or (Y, X) couple pixel ordinate position in source image. * * @see ReliefShadow#computeShadow(double) table use case. */ private final static double[] TABV = new double[2]; /** * Cosinus PI/4.<br/> * Permit to define along which axis we compute shadow. * @see ReliefShadow#getRelief(java.awt.image.RenderedImage, java.awt.image.RenderedImage, double) use case. */ private final static double COS45 = Math.cos(PI/4); /** * Coefficient to attenuate pixel values. */ private final double shadowDimming; /** * Coefficient to increase pixel values. */ private final double brightness; /** * {@link PixelIterator} which travel on dem image. */ private PixelIterator mntIter; /** * {@link PixelIterator} which travel on destination image which is resulting image from shadow computing. */ private PixelIterator destIter; /** * Source image minimum position on X axis. */ private int minX; /** * Source image maximum position on X axis. */ private int maxX; /** * Source image minimum position on Y axis. */ private int minY; /** * Source image maximum position on Y axis. */ private int maxY; /** * Angle in radian define by alpha = PI/2 - azimuth. */ private double alpha; /** * Alpha cosinus. */ private final double cosAlpha; /** * Alpha sinus. */ private final double sinAlpha; /** * Alpha tangente. */ private final double tanAlpha; /** * Source light tangente altitude. */ private double tanAltitude; /** * Define for one pixel, how much the elevation go down. */ private final double pash; /** * Define X axis ordinate position in {@link ReliefShadow#TABV}. * * @see ReliefShadow#computeShadow(double) use case. */ private final int ordinateX; /** * Define Y axis ordinate position in {@link ReliefShadow#TABV}. * * @see ReliefShadow#computeShadow(double) use case. */ private final int ordinateY; /** * The step of the choosen axis to follow image during shadow computing. * * @see ReliefShadow#computeShadow(double) use case. */ private final int pasv; /** * The step of the other axis which is image of step {@link ReliefShadow#pasv}.<br/> * Note : pasfv = f(pasv). * * @see ReliefShadow#computeShadow(double) use case. */ private final double pasfv; /** * It is the same step like {@link ReliefShadow#pash} but expressed in DEM unit. * * @see ReliefShadow#computeShadow(double) use case. */ private double pasz; /** * {@link ColorModel} from source image. */ private ColorModel sourceColorModel; /** * Define positive altitude value sens. */ private int axisDirection; /** * Create an object to apply shadow on some {@link RenderedImage}.<br/><br/> * * Note : if a pixel is defined as a shadow the destination pixel values are <br/> * sampleValue * shadowDimming for each bands with shadowDimming attribut € [0; 1]. * * @param lightSRCAzimuth Light angle in degree from {@link RenderedImage} Y axe. * @param lightSRCAltitude Light angle in degree of the light from the ground. * @param shadowDimming dimming coefficient apply on each band on each pixel which are define as a shadow. * @throws IllegalArgumentException if shadow dimming is out from [0; 1] interval. */ public ReliefShadow(final double lightSRCAzimuth, final double lightSRCAltitude, final double shadowDimming) { this(lightSRCAzimuth, lightSRCAltitude, 1, shadowDimming); } /** * Create an object to apply shadow on some {@link RenderedImage}.<br/><br/> * * Note : if a pixel is defined as a shadow the destination pixel values are <br/> * sampleValue * shadowDimming for each bands with shadowDimming attribut € [0; 1].<br/> * Same if a pixel is defined as sunny the destination pixel values are <br/> * sampleValue * brightness for each bands with shadowDimming attribut € [0; 1]. * * @param lightSRCAzimuth Light angle in degree from {@link RenderedImage} Y axe. * @param lightSRCAltitude Light angle in degree of the light from the ground. * @param brightness increase coefficient apply on each band on each pixel which are define as sunny. * @param shadowDimming dimming coefficient apply on each band on each pixel which are define as a shadow. * @throws IllegalArgumentException if shadow dimming is out from [0; 1] interval. */ public ReliefShadow(final double lightSRCAzimuth, final double lightSRCAltitude, final double brightness, final double shadowDimming) { if (brightness < 1) { throw new IllegalArgumentException("brightness should superior or equal to 1. value found : "+brightness); } if (shadowDimming > 1 || shadowDimming < 0) { throw new IllegalArgumentException("shadowDimming should belong in [0; 1] interval. value found : "+shadowDimming); } this.shadowDimming = shadowDimming; this.brightness = brightness; alpha = (PI/2) - ((lightSRCAzimuth % 360) * PI / 180);//-- delete n 2kPI cosAlpha = Math.cos(alpha); sinAlpha = Math.sin(alpha); tanAlpha = Math.tan(alpha); this.tanAltitude = Math.tan(Math.abs(lightSRCAltitude % 360) * PI / 180); this.axisDirection = 1; if (Math.abs(cosAlpha) >= COS45) { //-- alpha € [-PI/4 ; PI/4] U [3PI/4 ; 5PI/4] + 2kPI //-- we iterate along x axis ordinateX = 0; ordinateY = 1; //-- step on x axis. pasv = (int) Math.signum(cosAlpha); //-- step on y axis. pasfv = tanAlpha * pasv; } else { //-- alpha € ]PI/4 ; 3PI/4[ U ]5PI/4 ; 7PI/4[ + 2kPI //-- we iterate along y axis. ordinateX = 1; ordinateY = 0; //-- step on Y axis. pasv = (int) Math.signum(sinAlpha); //-- step on x axis. pasfv = pasv/tanAlpha; } pash = - Math.hypot(pasv, pasfv) * Math.abs(tanAltitude); } /** * Compute and return an appropriate {@link RenderedImage} which is a copy of source image with shadow added. * * @param imgSource {@link RenderedImage} with no relief shadow. * @param dem {@link RenderedImage} which contain all pixel elevations values (Digital Elevation Model). * @param scaleZ elevation value of a pixel. * @param Define positive altitude value sens. * @throws NullArgumentException if imgSource or mnt is {@code null}. * @throws IllegalArgumentException if scaleZ is lesser or equal to zero. * @throws IllegalArgumentException if source image width and mnt image have a differente width. * @throws IllegalArgumentException if source image width and mnt image have a differente height. * @throws IllegalArgumentException if setted {@link AxisDirection} is not instance of {@link AxisDirection#DOWN} or {@link AxisDirection#UP}. * @return an appropriate {@link RenderedImage} witch is a copy of source image with shadow added. * @see #setAltitudeAxisDirection(org.opengis.referencing.cs.AxisDirection) */ public RenderedImage getRelief(final RenderedImage imgSource, final RenderedImage dem, final double scaleZ, final AxisDirection elevationValueSens) { setAltitudeAxisDirection(elevationValueSens); return getRelief(imgSource, dem, scaleZ); } /** * Compute and return an appropriate {@link RenderedImage} which is a copy of source image with shadow added. * * @param imgSource {@link RenderedImage} with no relief shadow. * @param dem {@link RenderedImage} which contain all pixel elevations values (Digital Elevation Model). * @param scaleZ elevation value of a pixel. * @throws NullArgumentException if imgSource or mnt is {@code null}. * @throws IllegalArgumentException if scaleZ is lesser or equal to zero. * @throws IllegalArgumentException if source image width and mnt image have a differente width. * @throws IllegalArgumentException if source image width and mnt image have a differente height. * @return an appropriate {@link RenderedImage} witch is a copy of source image with shadow added. */ public RenderedImage getRelief(final RenderedImage imgSource, final RenderedImage dem, final double scaleZ) { ArgumentChecks.ensureNonNull("source image", imgSource); ArgumentChecks.ensureNonNull("MNT", dem); ArgumentChecks.ensureStrictlyPositive("pixel altitude", scaleZ); final int imgWidth = imgSource.getWidth(); final int imgHeight = imgSource.getHeight(); if (dem.getWidth() != imgWidth) { throw new IllegalArgumentException("mnt image and source image should have same width. image source width = " +imgWidth+" mnt width = "+dem.getWidth()); } if (dem.getHeight() != imgHeight) { throw new IllegalArgumentException("mnt image and source image should have same height. image source height = " +imgWidth+" mnt height = "+dem.getWidth()); } this.minX = imgSource.getMinX(); this.maxX = minX + imgWidth; this.minY = imgSource.getMinY(); this.maxY = minY + imgHeight; this.sourceColorModel = imgSource.getColorModel(); final Raster srcRaster = imgSource.getData(); //-- define step altitude, when iterator travel up along v axis. pasz = pash * scaleZ; final BufferedImage imgDest = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_ARGB); mntIter = PixelIteratorFactory.createRowMajorIterator(dem); destIter = PixelIteratorFactory.createRowMajorWriteableIterator(imgDest, imgDest); //-- iteration attribut final int iterBeginX; final int iterPasX; int iterBeginY; final int iterPasY; //-- we define destination image iteration sens, in function of alpha angle value. if (cosAlpha >= 0) { //-- travel left to right on X axis. iterBeginX = minX; iterPasX = 1; if (sinAlpha >= 0) { //-- lower left corner //-- travel down to up on Y axis. iterBeginY = minY; iterPasY = 1; } else { //-- upper left corner //-- travel up to down on Y axis iterBeginY = maxY-1; iterPasY = -1; } } else { //-- travel right to left on X axis. iterBeginX = maxX - 1; iterPasX = -1; if (sinAlpha >= 0) { //-- lower right corner //-- travel down to up on Y axis. iterBeginY = minY; iterPasY = 1; } else { //-- upper right corner //-- travel up to down on Y axis iterBeginY = maxY - 1; iterPasY = -1; } } while (iterBeginY >= minY && iterBeginY < maxY) { int x = iterBeginX; while (x >= minX && x < maxX) { destIter.moveTo(x, iterBeginY, 0); final Object pix = srcRaster.getDataElements(x, iterBeginY, null); //-- get pixel final int pixel = sourceColorModel.getRGB(pix); final int red = (pixel & 0x00FF0000) >> 16; final int green = (pixel & 0x0000FF00) >> 8; final int blue = (pixel & 0x000000FF); if (destIter.getSample() == 1) { //-- already define as a shadow //-- set alpha transparency int color = (pixel & 0xFF000000) | (((int) (red * shadowDimming)) << 16) | (((int) (green * shadowDimming)) << 8) | (((int) (blue * shadowDimming))); imgDest.setRGB(x, iterBeginY, color); } else { //-- current pixel is a pikes. //-- already define as a shadow //-- set alpha transparency int color = (pixel & 0xFF000000) | (((int) (red * brightness)) << 16) | (((int) (green * brightness)) << 8) | (((int) (blue * brightness))); imgDest.setRGB(x, iterBeginY, color); mntIter.moveTo(x, iterBeginY, 0); final double z = mntIter.getSampleDouble(); if (Double.isNaN(z)) { x += iterPasX; continue; } TABV[ordinateX] = x; TABV[ordinateY] = iterBeginY; computeShadow(z * axisDirection); } x += iterPasX; } iterBeginY += iterPasY; } return imgDest; } /** * Compute shadow produced by current pixel (pikes) at {@link ReliefShadow#TABV} position. * * @param z pixel elevation of current pixel (pikes). */ private void computeShadow(final double z) { //-- position on pixel center and go to next pixel TABV[0] += 0.5 + pasv; TABV[1] += 0.5 + pasfv; //-- define pixel x, y position int ordX = (int) TABV[ordinateX]; int ordY = (int) TABV[ordinateY]; //-- current altitude. double sz = z + pasz; while (ordX >= minX && ordX < maxX && ordY >= minY && ordY < maxY) { mntIter.moveTo(ordX, ordY, 0); final double currentZ = mntIter.getSampleDouble() * axisDirection; if (!Double.isNaN(currentZ)) { if (currentZ > sz) return; destIter.moveTo(ordX, ordY, 0); // set an arbitrary value just to define a shadow pixel. destIter.setSample(1); } TABV[0] += pasv; TABV[1] += pasfv; sz += pasz; //-- define pixel x, y position ordX = (int) TABV[ordinateX]; ordY = (int) TABV[ordinateY]; } } /** * Define positive altitude value sens.<br/> * {@link AxisDirection} should be instance of following type: <br/> * {@link AxisDirection#DOWN} or {@link AxisDirection#UP}.<br/><br/> * * Note : Default value is {@link AxisDirection#UP}. * * @param altitudeDirection direction of positive altitude value sens. * @throws IllegalArgumentException if setted {@link AxisDirection} is not instance of {@link AxisDirection#DOWN} or {@link AxisDirection#UP}. */ public void setAltitudeAxisDirection(final AxisDirection altitudeDirection) { if (altitudeDirection.equals(AxisDirection.DOWN)) { this.axisDirection = -1; } else if (altitudeDirection.equals(AxisDirection.UP)) { this.axisDirection = 1; } else { throw new IllegalArgumentException("Defined altitude direction should be instance of " + "AxisDirection.DOWN or AxisDirection.UP. Defined altitude = "+altitudeDirection); } } }