com.lizardtech.expresszip.model.Job.java Source code

Java tutorial

Introduction

Here is the source code for com.lizardtech.expresszip.model.Job.java

Source

/*******************************************************************************
 * Copyright 2014 Celartem, Inc., dba LizardTech
 * 
 * Licensed 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 com.lizardtech.expresszip.model;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Appender;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.FileAppender;
import org.apache.log4j.HTMLLayout;
import org.apache.log4j.Logger;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.io.AbstractGridCoverageWriter;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.processing.CoverageProcessor;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureSource;
import org.geotools.data.wms.WebMapServer;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.gce.geotiff.GeoTiffFormat;
import org.geotools.gce.geotiff.GeoTiffWriteParams;
import org.geotools.gce.geotiff.GeoTiffWriter;
import org.geotools.gce.image.WorldImageFormat;
import org.geotools.gce.image.WorldImageWriter;
import org.geotools.geometry.jts.JTS;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapContent;
import org.geotools.map.MapViewport;
import org.geotools.map.WMSLayer;
import org.geotools.referencing.CRS;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.RenderListener;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.resources.coverage.IntersectUtils;
import org.opengis.coverage.Coverage;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.vaadin.vol.Bounds;

import com.lizardtech.expresszip.model.ExportProps.OutputPackageFormat;
import com.lizardtech.expresszip.vaadin.ExportOptionsViewComponent;
import com.lizardtech.expresszip.vaadin.ExpressZipApplication;
import com.lizardtech.expresszip.vaadin.ExpressZipWindow;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;

public class Job extends Observable implements Runnable, ExecutorListener {
    private ExportProps jobParams = null;
    private Logger logger = null;
    private List<String> shapefiles = new ArrayList<String>();
    private File exportDir = null;
    private ExportOptionsViewComponent parent;
    private String jobStatus = "Submitted";
    private volatile boolean shutdown = false;
    private RunState runState = RunState.Uninitialized;
    private List<String> outputTiles;
    private String outputArchiveName;
    private final GeoTiffWriteParams geoTiffWPs;

    public enum RunState {
        Uninitialized, Queued, Running, FinishedOK, Failed
    };

    private File outputLogFile = null;
    private Appender outputLogAppender = null;
    private URL url;
    private static int WMSTimeOut = 0;

    public Job(ExportProps param, ExportOptionsViewComponent view) {
        jobParams = param;
        parent = view;

        url = view.getApplication().getMainWindow().getURL();
        if (exportDir == null) {
            exportDir = new File(ExpressZipApplication.Configuration.getProperty("exportdirectory"),
                    jobParams.getJobName());
            exportDir.mkdir();
        }
        String JobRunID = String.format("%d", System.identityHashCode(this));
        logger = Logger.getLogger(JobRunID);
        logger.setAdditivity(false);
        try {
            outputLogFile = new File(exportDir, getLogName());
            outputLogAppender = new FileAppender(new HTMLLayout(), outputLogFile.getPath());
            logger.addAppender(outputLogAppender);
        } catch (IOException e) {
            BasicConfigurator.configure();
        }

        // Set up GeoTIFF writer compression and tiling
        geoTiffWPs = new GeoTiffWriteParams();
        geoTiffWPs.setCompressionMode(GeoTiffWriteParams.MODE_EXPLICIT);
        geoTiffWPs.setCompressionType("LZW");
        geoTiffWPs.setCompressionQuality(0.75F);
        geoTiffWPs.setTilingMode(GeoTiffWriteParams.MODE_EXPLICIT);
        geoTiffWPs.setTiling(256, 256);
    }

    public URL getUrl() {
        return url;
    }

    public void setStatus(String s) {
        jobStatus = s;
    }

    // this is called by the databinding in JobStatus view component
    public String getStatus() {
        switch (getRunState()) {
        case Uninitialized:
            return "Submitted";
        case Queued:
            return "Queued";
        case Running:
            return jobStatus;
        case Failed:
            return "Failed";
        case FinishedOK:
            return "Completed OK";
        default:
            return "Unknown status";
        }
    }

    private String _archiveName = "Uninitialized_OutputArchive.zip";

    public String getDL_URL() {
        String url = "";
        if (getRunState() == RunState.FinishedOK) {
            URL u = getUrl();
            String containerURL = u.getProtocol() + "://" + u.getAuthority();
            url = containerURL + "/exportdir/" + jobParams.getJobName() + "/" + _archiveName;
        }
        return url;
    }

    private File cropFile;

    private String getLogName() {
        return jobParams.getJobName() + "_Log.html";
    }

    public String getlog_URL() {
        String url = "";
        if (getRunState() == RunState.FinishedOK || getRunState() == RunState.Failed) {
            URL u = getUrl();
            String containerURL = u.getProtocol() + "://" + u.getAuthority();
            url = containerURL + "/exportdir/" + jobParams.getJobName() + "/" + getLogName();
        }
        return url;
    }

    public ExportProps getExporProps() {
        return jobParams;
    }

    public String getJobName() {
        return jobParams.getJobName();
    }

    public void cancel() {
        logger.info("Cancelling job " + jobParams.getJobName());
        shutdown = true;
    }

    public void run() {
        try {
            addshpFileLayers();
            logger.info("Saving the output image ...");
            saveImage(jobParams.getJobName(), jobParams.getWidth(), jobParams.getHeight(), jobParams.getFormat());
            logger.info("Done with job " + jobParams.getJobName());

        } catch (Exception e) {

            try {
                setRunState(RunState.Failed);
                logger.error("Job failed (" + e.getMessage() + ")", e);
            } catch (Exception e1) {
                throw new RuntimeException(e1);
            }
        }
    }

    private void saveImage(final String jobname, final int imageWidth, final int imageHeight, final String format)
            throws Exception {
        _archiveName = exportTiles(jobParams, jobname, format);
    }

    private int getWMSTimeOut() {
        if (WMSTimeOut == 0) {
            String strTimeOut = ExpressZipApplication.Configuration.getProperty("wmstimeout");
            if (strTimeOut != null) {
                try {
                    WMSTimeOut = Integer.parseInt(strTimeOut);
                } catch (NumberFormatException e) {
                    // empty
                }
            }
        }
        if (WMSTimeOut == 0)
            WMSTimeOut = 7 * 1000;
        return WMSTimeOut;
    }

    /**
     * Add enable shapefiles to map to be saved as an image
     * 
     * @throws IOException
     */
    private void addshpFileLayers() throws IOException {
        VectorLayer cropLayer = jobParams.getCroppingLayer();
        if (null != cropLayer) {
            logger.info("Adding VectorLayer " + cropLayer.getName());
            shapefiles.add(cropLayer.getName());
            cropFile = cropLayer.buildShpFilePath();
        }
        incrementShapefileRefCounts();
    }

    private void writeTarFile(File baseDir, File archive, List<String> files) throws IOException {
        FileOutputStream fOut = null;
        BufferedOutputStream bOut = null;
        GzipCompressorOutputStream gzOut = null;
        TarArchiveOutputStream tOut = null;
        try {
            fOut = new FileOutputStream(archive);
            bOut = new BufferedOutputStream(fOut);
            gzOut = new GzipCompressorOutputStream(bOut);
            tOut = new TarArchiveOutputStream(gzOut);

            for (String f : files) {
                File myfile = new File(baseDir, f);
                String entryName = myfile.getName();
                logger.info(String.format("Writing %s to TAR archive %s", f, archive));

                TarArchiveEntry tarEntry = new TarArchiveEntry(myfile, entryName);
                tOut.putArchiveEntry(tarEntry);

                FileInputStream fis = new FileInputStream(myfile);
                IOUtils.copy(fis, tOut);
                fis.close();
                tOut.closeArchiveEntry();
            }
        } finally {
            tOut.finish();
            tOut.close();
            gzOut.close();
            bOut.close();
            fOut.close();
        }
    }

    private void writeZipFile(File baseDir, File archive, List<String> files)
            throws FileNotFoundException, IOException {
        FilterOutputStream out = null;
        ZipOutputStream stream = new ZipOutputStream(new FileOutputStream(archive));
        stream.setLevel(Deflater.DEFAULT_COMPRESSION);
        out = stream;

        byte data[] = new byte[18024];

        for (String f : files) {
            logger.info(String.format("Writing %s to ZIP archive %s", f, archive));
            ((ZipOutputStream) out).putNextEntry(new ZipEntry(f));

            BufferedInputStream in = new BufferedInputStream(new FileInputStream(new File(baseDir, f)));

            int len = 0;
            while ((len = in.read(data)) > 0) {
                out.write(data, 0, len);
            }

            out.flush();
            in.close();
        }

        out.close();
    }

    private String zipFiles(List<String> files, String jobName, File baseDir) throws IOException {

        String suffix = "";
        String basename = "";
        OutputPackageFormat fmt = jobParams.getOutputPackageFormat();
        String extension = fmt.toString().toLowerCase();
        if (fmt == OutputPackageFormat.TAR)
            extension += ".gz";

        boolean done = false;
        int count = 1;
        try {
            File file = null;
            while (!done) { // Generate a unique name for the output archive

                basename = jobName + suffix + "." + extension;
                file = new File(baseDir, basename);
                done = file.createNewFile(); // stake our claim to a unique
                // filename
                suffix = "_" + String.valueOf(count++);
            }
            // Add log to a copy of the list so we archive it but don't delete it on clean up.
            List<String> copyFiles = new ArrayList<String>(files);
            copyFiles.add(outputLogFile.getName());
            if (fmt == OutputPackageFormat.ZIP)
                writeZipFile(baseDir, file, copyFiles);
            else
                writeTarFile(baseDir, file, copyFiles);
        } catch (IllegalArgumentException e) {
            logger.error(e);
            return null;
        }
        return basename;
    }

    enum extensions {
        jpg, png, tif, gif, bmp
    };

    private String exportTiles(ExportProps props, final String jobName, String format) throws Exception {
        // Hack to get proper extension and WorldImageFormat
        if (format.equals("JPEG")) {
            format = extensions.jpg.toString();
        } else if (format.equals("PNG")) {
            format = extensions.png.toString();
        } else if (format.equals("TIFF")) {
            format = extensions.tif.toString();
        } else if (format.equals("GIF")) {
            format = extensions.gif.toString();
        } else if (format.equals("BMP")) {
            format = extensions.bmp.toString();
        } else {
            throw new RuntimeException("Unknown format: " + format);
        }

        outputTiles = new ArrayList<String>();

        GriddingOptions griddingOptions = props.getGriddingOptions();
        if (griddingOptions.isGridding()) {

            Bounds bounds = getExportProps().getExportRegion();

            double cellSpacingX = 0.0;
            double cellSpacingY = 0.0;
            int tilePixelWidth = 0;
            int tilePixelHeight = 0;

            if (griddingOptions.getGridMode() == GriddingOptions.GridMode.DIVISION) {

                double boundsMaxX = Math.max(bounds.getLeft(), bounds.getRight());
                double boundsMinX = Math.min(bounds.getLeft(), bounds.getRight());
                double boundsMaxY = Math.max(bounds.getTop(), bounds.getBottom());
                double boundsMinY = Math.min(bounds.getTop(), bounds.getBottom());

                cellSpacingX = (boundsMaxX - boundsMinX) / griddingOptions.getDivX();
                cellSpacingY = (boundsMaxY - boundsMinY) / griddingOptions.getDivY();
                tilePixelWidth = props.getWidth() / griddingOptions.getDivX();
                tilePixelHeight = props.getHeight() / griddingOptions.getDivY();
            } else { // GRID_TILE_DIMENSIONS or GRID_GROUND_DISTANCE

                griddingOptions.setExportWidth(props.getWidth());
                griddingOptions.setExportHeight(props.getHeight());

                double widthRatio = (double) props.getWidth() / (double) griddingOptions.getTileSizeX();
                double heightRatio = (double) props.getHeight() / (double) griddingOptions.getTileSizeY();

                double max = Math.max(bounds.getLeft(), bounds.getRight());
                double min = Math.min(bounds.getLeft(), bounds.getRight());
                cellSpacingX = (max - min) / widthRatio;
                tilePixelWidth = griddingOptions.getTileSizeX();

                max = Math.max(bounds.getTop(), bounds.getBottom());
                min = Math.min(bounds.getTop(), bounds.getBottom());
                cellSpacingY = (max - min) / heightRatio;
                tilePixelHeight = griddingOptions.getTileSizeY();
            }

            double ulx = bounds.getLeft();
            double lrx = bounds.getRight();
            double uly = bounds.getTop();
            double lry = bounds.getBottom();

            double t_ulx = ulx, t_uly = uly, t_lrx = 0.0, t_lry = 0.0;
            int maxRows = (int) Math.ceil((bounds.getTop() - bounds.getBottom()) / cellSpacingY);
            int maxCols = (int) Math.ceil((bounds.getRight() - bounds.getLeft()) / cellSpacingX);
            int xCounter = 0, yCounter = 0;
            boolean lastColumn = false;
            boolean meters = griddingOptions.getGridMode() == GriddingOptions.GridMode.METERS;
            int tileCounter = 1;
            while (!lastColumn) {

                int lastWidth = 0;
                int lastHeight = 0;
                t_lrx = t_ulx + cellSpacingX;
                t_uly = uly;
                yCounter = 0;

                if (Math.abs((t_ulx + cellSpacingX) - lrx) <= 0.00001f || (t_ulx + cellSpacingX) >= lrx) {
                    lastColumn = true;
                    if (meters) {
                        lastWidth = props.getWidth() - (tilePixelWidth * xCounter);
                    }
                }

                boolean lastRow = false;
                while (!lastRow) {

                    // TODO: this is wrong when NOT lat-lon
                    if (Math.abs((t_uly - cellSpacingY) - lry) <= 0.00001f || (t_uly - cellSpacingY) <= lry) {

                        lastRow = true;
                        if (meters) {
                            lastHeight = props.getHeight() - (tilePixelHeight * yCounter);
                        }
                    }

                    yCounter++;
                    t_lry = t_uly - cellSpacingY; // TODO: this is wrong when NOT lat-lon

                    Bounds tileBounds = new Bounds();
                    tileBounds.setLeft(t_ulx);
                    tileBounds.setRight(meters && lastColumn ? lrx : t_lrx);
                    tileBounds.setTop(t_uly);
                    tileBounds.setBottom(meters && lastRow ? lry : t_lry);
                    TileExport tile = new TileExport(tileBounds, meters && lastColumn ? lastWidth : tilePixelWidth,
                            meters && lastRow ? lastHeight : tilePixelHeight);
                    exportTile(tile, jobName, format, tileCounter++);
                    setStatus("Exporting tile " + tileCounter + " of " + maxRows * maxCols);
                    t_uly = t_lry;
                }

                xCounter++;
                t_ulx = t_lrx;
            }
        } else {
            TileExport singleTile = new TileExport(props.getExportRegion(), props.getWidth(), props.getHeight());
            exportTile(singleTile, jobName, format, 0);
        }

        setStatus("Packaging tiles....");
        outputArchiveName = zipFiles(outputTiles, jobName, exportDir);
        return outputArchiveName;
    }

    private void exportTile(TileExport t, String jobName, String format, int counter) throws Exception {

        if (shutdown)
            throw new RuntimeException("Cancelled");

        ReferencedEnvelope tileBounds = null;
        Rectangle imageBounds = null;

        double minx = Math.min(t.bounds.getLeft(), t.bounds.getRight());
        double maxx = Math.max(t.bounds.getLeft(), t.bounds.getRight());
        double miny = Math.min(t.bounds.getTop(), t.bounds.getBottom());
        double maxy = Math.max(t.bounds.getTop(), t.bounds.getBottom());
        logger.info(String.format("Adding tile %d x: %f - %f, y: %f - %f", counter, minx, maxx, miny, maxy));
        if (jobParams.isProjectedCRS(CRS.decode(jobParams.getMapProjection()))) {
            tileBounds = new ReferencedEnvelope(t.bounds.getLeft(), t.bounds.getRight(), t.bounds.getTop(),
                    t.bounds.getBottom(), CRS.decode(jobParams.getMapProjection()));

        } else {
            tileBounds = new ReferencedEnvelope(minx, maxx, miny, maxy, CRS.decode(jobParams.getMapProjection()));
        }

        // Set view point on map
        MapViewport viewport = new MapViewport(tileBounds);
        viewport.setCoordinateReferenceSystem(CRS.decode(jobParams.getMapProjection()));

        // Create the bound for the image
        imageBounds = new Rectangle(0, 0, t.width, t.height);

        // Set up an ImageBuffer to write image to
        logger.info(
                String.format("Allocating BufferedImage of size %d x %d", imageBounds.width, imageBounds.height));
        int bufferType = BufferedImage.TYPE_INT_RGB;
        if (extensions.valueOf(format) == extensions.png)
            bufferType = BufferedImage.TYPE_INT_ARGB;

        BufferedImage image = new BufferedImage(imageBounds.width, imageBounds.height, bufferType);
        Graphics2D graphics = image.createGraphics();

        // Render the unclipped image
        MapContent map = new MapContent();
        map.setViewport(viewport);

        WebMapServer wms = new WebMapServer(MapModel.getPrivateServerURL(), getWMSTimeOut());
        WMSLayer wmsLayer = null;
        for (ExpressZipLayer l : jobParams.getEnabledLayers()) {
            if (l instanceof RasterLayer && l.projectionIsSupported(jobParams.getMapProjection())) {
                if (wmsLayer == null)
                    wmsLayer = new WMSLayer(wms, l.getGeoToolsLayer());
                else
                    wmsLayer.addLayer(l.getGeoToolsLayer());
            }
        }
        if (wmsLayer != null)
            map.addLayer(wmsLayer);

        GTRenderer renderer = new StreamingRenderer();
        renderer.setMapContent(map);
        renderer.addRenderListener(new RenderListener() {

            @Override
            public void featureRenderer(SimpleFeature feature) {
            }

            @Override
            public void errorOccurred(Exception e) {
                logger.error("Streaming Renderer for tile failed (" + e.getMessage() + ")");
                throw new RuntimeException(e);
            }
        });

        try {
            renderer.paint(graphics, imageBounds, tileBounds);
        } finally {

            try {
                // clean up
                graphics.dispose();
            } catch (Exception e) {
            }

            try {
                map.dispose();
            } catch (Exception e) {
            }
        }

        // Clip the image to the shapefile
        if (cropFile != null) {
            FeatureSource<SimpleFeatureType, SimpleFeature> featureSource = getFeatureSource(cropFile);
            GridCoverage2D clippedCoverage = (GridCoverage2D) clipImageToFeatureSource(image, tileBounds,
                    featureSource);
            if (null == clippedCoverage) {
                logger.info("Tile does not intersect shapefile. Skipping.");
                return;
            }
            BufferedImage matted = mattCroppedImage(image, clippedCoverage, bufferType);
            image = matted;
        }

        // Save the image
        String fileBase = "";
        File fileToSave = null;
        try {
            do {
                fileBase = jobName;
                if (counter > 0)
                    fileBase = fileBase + "_" + counter;
                fileToSave = new File(exportDir, fileBase + "." + format);
                counter++;
            } while (!fileToSave.createNewFile());

            logger.info(String.format("Writing image %s", fileToSave.getAbsolutePath()));

            GridCoverageFactory factory = new GridCoverageFactory();
            GridCoverage2D coverage = factory.create("LT", image, tileBounds);
            AbstractGridCoverageWriter coverageWriter = null;
            GeneralParameterValue[] formatParam = null;
            if (format.equals(extensions.tif.toString())) {
                ParameterValueGroup params = new GeoTiffFormat().getWriteParameters();
                params.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString())
                        .setValue(geoTiffWPs);
                coverageWriter = new GeoTiffWriter(fileToSave);
                formatParam = (GeneralParameterValue[]) params.values().toArray(new GeneralParameterValue[1]);
            } else {
                ParameterValueGroup params = new WorldImageFormat().getWriteParameters();
                params.parameter(WorldImageFormat.FORMAT.getName().toString()).setValue(format);
                formatParam = new GeneralParameterValue[] {
                        params.parameter(WorldImageFormat.FORMAT.getName().toString()) };
                coverageWriter = new WorldImageWriter(fileToSave);
            }
            try {
                coverageWriter.write(coverage, formatParam);
            } finally {
                try {
                    coverageWriter.dispose();
                } catch (Exception e) {
                }

                try {
                    coverage.dispose(true);
                } catch (Exception e) {
                }
            }
        } catch (Exception e) {
            logger.error(
                    String.format("FAILED writing image %s (%s)", fileToSave.getAbsolutePath(), e.getMessage()));
            throw new RuntimeException(e);
        }

        // Gather up fileBase.* (include world and prj filenames) for zip file
        for (File child : exportDir.listFiles()) {
            String childName = child.getName();
            String baseName = FilenameUtils.getBaseName(childName);
            if (child.isFile() && baseName.equals(fileBase)) {
                String extension = FilenameUtils.getExtension(childName);
                extension.toLowerCase();
                if (extension.length() == 4 && extension.endsWith("w")) {
                    extension = extension.substring(0, 1) + extension.substring(2);
                    File renamedChild = new File(exportDir, baseName + "." + extension);
                    child.renameTo(renamedChild);
                    child = renamedChild;
                }

                outputTiles.add(child.getName());
            }
        }
    }

    private BufferedImage mattCroppedImage(final BufferedImage source, GridCoverage2D cropped, int bufferType) {
        int height = source.getHeight();
        int width = source.getWidth();
        BufferedImage image = new BufferedImage(width, height, bufferType);
        Graphics2D gr = image.createGraphics(); // the way getRenderedImage works, this image should be drawn at 0, 0, not at based
        // on where the coverage's tile is
        AffineTransform at = AffineTransform.getTranslateInstance(0, 0);
        gr.drawRenderedImage(cropped.getRenderedImage(), at);
        return image;
    }

    public static FeatureSource<SimpleFeatureType, SimpleFeature> getFeatureSource(File cropFile)
            throws MalformedURLException, IOException {
        Map<String, Serializable> connectParameters = new java.util.HashMap<String, Serializable>();
        connectParameters.put("url", cropFile.toURI().toURL());
        connectParameters.put("create spatial index", true);
        DataStore dataStore = DataStoreFinder.getDataStore(connectParameters);
        if (dataStore == null)
            throw new RuntimeException("No DataStore found to handle" + cropFile.getPath());
        String[] typeNames = dataStore.getTypeNames();
        String typeName = typeNames[0];
        FeatureSource<SimpleFeatureType, SimpleFeature> featureSource = (FeatureSource<SimpleFeatureType, SimpleFeature>) dataStore
                .getFeatureSource(typeName);
        return featureSource;
    }

    private void cleanUp() {
        try {
            // downloaded tiles
            for (String file : outputTiles)
                (new File(exportDir, file)).delete();
            for (String s : shapefiles)
                ((ExpressZipWindow) parent.getApplication().getMainWindow()).getSetupMapModel()
                        .ShapeFileDecrement(s);
        } catch (Exception e) {
            logger.error(e);
        } finally {
            outputLogAppender.close();
            logger.removeAllAppenders();
        }
        BackgroundExecutor executor = new BackgroundExecutor.Factory().getBackgroundExecutor();
        executor.removeListener(this);
    }

    public void submit(ExecutorListener view) throws Exception {
        BackgroundExecutor executor = new BackgroundExecutor.Factory().getBackgroundExecutor();
        executor.addListener(this);
        executor.execute(this);
    }

    public ExportProps getExportProps() {
        return jobParams;
    }

    public void setExportProps(ExportProps p) {
        jobParams = p;
    }

    private static List<Job> jobQueue = new ArrayList<Job>();

    public static List<Job> getJobQueue() {
        return jobQueue;
    }

    public static void removeJobFromQueue(Job job) {
        synchronized (jobQueue) {
            jobQueue.remove(job);
            job.cleanupFiles();
        }
    }

    private void cleanupFiles() {
        try {
            File file = new File(exportDir, _archiveName);
            file.delete();
            outputLogFile.delete();
            exportDir.delete();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void incrementShapefileRefCounts() {
        for (String s : shapefiles) {
            ((ExpressZipWindow) parent.getApplication().getMainWindow()).getSetupMapModel().ShapeFileIncrement(s);
        }
    }

    public void decrementShapefileRefCounts() {
        for (String s : shapefiles) {
            ((ExpressZipWindow) parent.getApplication().getMainWindow()).getSetupMapModel().ShapeFileDecrement(s);
        }
    }

    static { // prevents exception due to missing library.
        System.setProperty("com.sun.media.jai.disableMediaLib", "true");
    }

    public RunState getRunState() {
        return runState;
    }

    private Coverage clipImageToFeatureSource(RenderedImage image, ReferencedEnvelope bounds,
            FeatureSource<SimpleFeatureType, SimpleFeature> featureSource)
            throws IOException, FactoryException, MismatchedDimensionException, TransformException {
        FeatureCollection<SimpleFeatureType, SimpleFeature> collection = featureSource.getFeatures();

        CoordinateReferenceSystem crsFeatures = featureSource.getSchema().getCoordinateReferenceSystem();
        CoordinateReferenceSystem crsMap = bounds.getCoordinateReferenceSystem();
        boolean needsReproject = !CRS.equalsIgnoreMetadata(crsFeatures, crsMap);
        MathTransform transform = CRS.findMathTransform(crsFeatures, crsMap, true);

        FeatureIterator<SimpleFeature> iterator = collection.features();
        List<Geometry> all = new ArrayList<Geometry>();
        try {
            while (iterator.hasNext()) {
                SimpleFeature feature = iterator.next();
                Geometry geometry = (Geometry) feature.getDefaultGeometry();
                if (geometry == null)
                    continue;
                if (!geometry.isSimple())
                    continue;
                if (needsReproject) {
                    geometry = JTS.transform(geometry, transform);
                    System.out.println("Reprojected a geometry.  Result is " + geometry.toString());
                }
                Geometry intersection = geometry.intersection(JTS.toGeometry(bounds));
                if (intersection.isEmpty()) {
                    continue;
                }
                // String name = (String) feature.getAttribute("NAME");
                // if (name == null)
                // name = (String) feature.getAttribute("CNTRY_NAME");
                if (intersection instanceof MultiPolygon) {
                    MultiPolygon mp = (MultiPolygon) intersection;
                    for (int i = 0; i < mp.getNumGeometries(); i++) {
                        com.vividsolutions.jts.geom.Polygon g = (com.vividsolutions.jts.geom.Polygon) mp
                                .getGeometryN(i);
                        Geometry gIntersection = IntersectUtils.intersection(g, JTS.toGeometry(bounds));
                        if (gIntersection.isEmpty()) {
                            continue;
                        }
                        all.add(g);
                    }
                } else if (intersection instanceof Polygon)
                    all.add(intersection);
                else
                    continue;
            }
        } finally {
            if (iterator != null) {
                iterator.close();
            }
        }
        GridCoverageFactory gridCoverageFactory = new GridCoverageFactory();
        Coverage coverage = gridCoverageFactory.create("Raster", image, bounds);
        Coverage clippedCoverage = null;
        if (all.size() > 0) {
            CoverageProcessor processor = new CoverageProcessor();
            ParameterValueGroup params = processor.getOperation("CoverageCrop").getParameters();
            params.parameter("Source").setValue(coverage);
            GeometryFactory factory = JTSFactoryFinder.getGeometryFactory(null);
            Geometry[] a = all.toArray(new Geometry[0]);
            GeometryCollection c = new GeometryCollection(a, factory);
            // params.parameter("ENVELOPE").setValue(bounds);
            params.parameter("ROI").setValue(c);
            params.parameter("ForceMosaic").setValue(true);
            clippedCoverage = processor.doOperation(params);
        }
        if (all.size() == 0) {
            logger.info("Crop by shapefile requested but no simple features matched extent!");
        }
        return clippedCoverage;
    }

    public void setRunState(RunState runState) {
        this.runState = runState;
    }

    @Override
    public void taskFinished(Runnable r) {
        Job j = (Job) r;
        if (j == this) {
            if (getRunState() == RunState.Failed) {
                logger.info("Job FAILED " + j.getExportProps().getJobName());
            } else {
                setRunState(RunState.FinishedOK);
                logger.info("Finished job " + j.getExportProps().getJobName());
            }
            emailUser();
            cleanUp();
        }
    }

    @Override
    public void taskRunning(Runnable r) {
        Job j = (Job) r;
        if (j == this) {
            setRunState(RunState.Running);
            logger.info("Running job " + j.getExportProps().getJobName());
        }
    }

    @Override
    public void taskQueued(Runnable r) {
        Job j = (Job) r;
        if (j == this) {
            setRunState(RunState.Queued);
            synchronized (jobQueue) {
                jobQueue.add(j);
            }
            logger.info("Queued job " + getExportProps().getJobName());
            ((ExpressZipWindow) parent.getApplication().getMainWindow())
                    .showNotification(String.format("Job \"%s\" Queued", getJobName()));
        }
    }

    @Override
    public void taskError(Runnable r, Throwable e) {
        Job j = (Job) r;
        if (j == this) {
            setRunState(RunState.Failed);
            emailUser();
            logger.error("Failed job " + j.getExportProps().getJobName() + " Cause: " + e.getMessage());
            cleanUp();
        }
    }

    private void emailUser() {
        ExportProps props = getExportProps();
        String email = props.getEmail();

        if (email == null || email.isEmpty() || email.equalsIgnoreCase(ExportProps.DefaultNotSupplied)) {
            logger.info("email not configured or address not given: not sending notification");
            return;
        }

        logger.info("Sending email notification to: " + email);
        StringBuilder msgBody = new StringBuilder();
        String statuslink = getDL_URL();
        if (getRunState() == RunState.Failed) {
            URL u = getUrl();
            statuslink = u.getProtocol() + "://" + u.getAuthority() + "/ExpressZip";
        }
        msgBody.append("<p>Your ExpressZip request, Job \"");
        msgBody.append(props.getJobName());
        msgBody.append("\" completed processing at ");
        DateFormat fmttime = new SimpleDateFormat("HH:mm:ss");
        DateFormat fmtdate = new SimpleDateFormat("MM/dd/yyyy");
        Date rightnow = Calendar.getInstance().getTime();
        msgBody.append(fmttime.format(rightnow) + " on ");
        msgBody.append(fmtdate.format(rightnow));
        msgBody.append(".  You may download your data at:</p>");
        msgBody.append("<a href=\"" + statuslink + "\">" + statuslink + "</a>");

        BackgroundExecutor executor = new BackgroundExecutor.Factory().getBackgroundExecutor();
        executor.getMailServices().sendEmail(email, "ExpressZip@" + getUrl().getHost(),
                "Job " + props.getJobName() + " has finished processing", msgBody.toString(), "text/html");
    }

    @Override
    public String toString() {
        return getJobName();
    }
}