package edu.oregonstate.cartography.gui;

import com.fizzysoft.sdu.RecentDocumentsManager;
import edu.oregonstate.cartography.app.FileUtils;
import edu.oregonstate.cartography.app.GeometryUtils;
import edu.oregonstate.cartography.geometryexport.ShapeExporter;
import edu.oregonstate.cartography.geometryimport.GeometryCollectionImporter;
import edu.oregonstate.cartography.geometryimport.ShapeImporter;
import edu.oregonstate.cartography.grid.ESRIASCIIGridExporter;
import edu.oregonstate.cartography.grid.EsriASCIIGridReader;
import edu.oregonstate.cartography.grid.Grid;
import edu.oregonstate.cartography.grid.Model;
import edu.oregonstate.cartography.grid.Model.ForegroundVisualization;
import edu.oregonstate.cartography.grid.WorldFileExporter;
import edu.oregonstate.cartography.grid.operators.IlluminatedContoursOperator;
import edu.oregonstate.cartography.grid.operators.NormalMapOperator;
import edu.oregonstate.cartography.grid.operators.NormalMapOperator.Channel;
import edu.oregonstate.cartography.simplefeatures.GeometryCollection;
import edu.oregonstate.cartography.simplefeatures.LineString;
import edu.oregonstate.cartography.simplefeatures.PlanObliqueShearing;
import edu.oregonstate.cartography.simplefeatures.Point;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

/**
 * Shades a DEM generalizedGrid using weighted Laplacian Pyramids. Results are
 * displayed in this JFrame with options for user input.
 *
 * @author Charles Preppernau and Bernie Jenny, Oregon State University
 */
public class MainWindow extends javax.swing.JFrame {

    private static final String OPEN_ERROR_MESSAGE = "Could not open the terrain file.";
    private static final String SAVE_IMAGE_ERROR_MESSAGE = "Could not save the image file.";
    private static final String SAVE_TERRAIN_ERROR_MESSAGE = "Could not save the terrain file.";

    private final Model model;
    private SettingsDialog settingsDialog = null;

    private RecentDocumentsManager rdm;
    private final ProgressPanel progressPanel;

    /**
     * Constructor for the JFrame. Initializes components and sets up the
     * default color gradient.
     *
     * @param model model object
     */
    public MainWindow(Model model) {
        this.model = model;

        initRecentDocumentsMenu();
        initComponents();
        
        // hide menu with experimental features
        menuBar.remove(experimentalMenu);

        // get menu keyboard events from owned dialogs
        DialogUtil.setupDialogActions(menuBar);

        progressPanel = new ProgressPanel();
        progressPanel.setOpaque(false);
        setGlassPane(progressPanel);
        progressPanel.setVisible(false);
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        imageResolutionPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel1 = new javax.swing.JLabel();
        imageResolutionSpinner = new javax.swing.JSpinner();
        scaleGridPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel8 = new javax.swing.JLabel();
        scaleTerrainFormattedTextField = new javax.swing.JFormattedTextField();
        javax.swing.JLabel jLabel9 = new javax.swing.JLabel();
        offsetGridPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel10 = new javax.swing.JLabel();
        offsetTerrainFormattedTextField = new javax.swing.JFormattedTextField();
        javax.swing.JLabel jLabel11 = new javax.swing.JLabel();
        voidGridPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel12 = new javax.swing.JLabel();
        voidGridFormattedTextField = new javax.swing.JFormattedTextField();
        javax.swing.JLabel jLabel13 = new javax.swing.JLabel();
        navigableImagePanel = new edu.oregonstate.cartography.gui.NavigableImagePanel();
        menuBar = new javax.swing.JMenuBar();
        javax.swing.JMenu fileMenu = new javax.swing.JMenu();
        javax.swing.JMenuItem openMenuItem = new javax.swing.JMenuItem();
        openRecentMenu = rdm.createOpenRecentMenu();
        javax.swing.JPopupMenu.Separator jSeparator1 = new javax.swing.JPopupMenu.Separator();
        saveGridMenuItem = new javax.swing.JMenuItem();
        saveGeneralizedGridMenuItem = new javax.swing.JMenuItem();
        saveLocalGridMenuItem = new javax.swing.JMenuItem();
        saveDownsampledMenu = new javax.swing.JMenu();
        javax.swing.JPopupMenu.Separator jSeparator5 = new javax.swing.JPopupMenu.Separator();
        javax.swing.JMenu saveImageMenu = new javax.swing.JMenu();
        saveTIFFImageMenuItem = new javax.swing.JMenuItem();
        savePNGImageMenuItem = new javax.swing.JMenuItem();
        javax.swing.JMenu saveContoursMenu = new javax.swing.JMenu();
        saveTIFFContoursMenuItem = new javax.swing.JMenuItem();
        savePNGContoursMenuItem = new javax.swing.JMenuItem();
        javax.swing.JMenu saveContoursMenu1 = new javax.swing.JMenu();
        saveTIFFNormalMapMenuItem = new javax.swing.JMenuItem();
        savePNGNormalMapMenuItem = new javax.swing.JMenuItem();
        editMenu = new javax.swing.JMenu();
        scaleGridMenuItem = new javax.swing.JMenuItem();
        offsetGridMenuItem = new javax.swing.JMenuItem();
        voidGridMenuItem = new javax.swing.JMenuItem();
        viewMenu = new javax.swing.JMenu();
        viewResetMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator4 = new javax.swing.JPopupMenu.Separator();
        viewZoomInMenuItem = new javax.swing.JMenuItem();
        viewZoomOutMenuItem = new javax.swing.JMenuItem();
        javax.swing.JMenu infoMenu = new javax.swing.JMenu();
        settingsMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator3 = new javax.swing.JPopupMenu.Separator();
        terrainInfoMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator2 = new javax.swing.JPopupMenu.Separator();
        infoMenuItem = new javax.swing.JMenuItem();
        experimentalMenu = new javax.swing.JMenu();
        planObliqueFeaturesMenuItem = new javax.swing.JMenuItem();

        imageResolutionPanel.setLayout(new java.awt.GridBagLayout());

        jLabel1.setText("Image resolution relative to terrain model size:");
        imageResolutionPanel.add(jLabel1, new java.awt.GridBagConstraints());

        imageResolutionSpinner.setModel(new javax.swing.SpinnerNumberModel(1, 1, 25, 1));
        imageResolutionPanel.add(imageResolutionSpinner, new java.awt.GridBagConstraints());

        scaleGridPanel.setLayout(new java.awt.GridBagLayout());

        jLabel8.setText("Scale Factor:");
        scaleGridPanel.add(jLabel8, new java.awt.GridBagConstraints());

        scaleTerrainFormattedTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter()));
        scaleTerrainFormattedTextField.setPreferredSize(new java.awt.Dimension(200, 28));
        scaleTerrainFormattedTextField.setValue(new Float(1));
        scaleGridPanel.add(scaleTerrainFormattedTextField, new java.awt.GridBagConstraints());

        jLabel9.setFont(jLabel9.getFont().deriveFont(jLabel9.getFont().getSize()-2f));
        jLabel9.setText("All values in the terrain grid are scaled by this factor.");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(10, 0, 0, 0);
        scaleGridPanel.add(jLabel9, gridBagConstraints);

        offsetGridPanel.setLayout(new java.awt.GridBagLayout());

        jLabel10.setText("Value to Add:");
        offsetGridPanel.add(jLabel10, new java.awt.GridBagConstraints());

        offsetTerrainFormattedTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter()));
        offsetTerrainFormattedTextField.setPreferredSize(new java.awt.Dimension(200, 28));
        offsetTerrainFormattedTextField.setValue(new Float(1));
        offsetGridPanel.add(offsetTerrainFormattedTextField, new java.awt.GridBagConstraints());

        jLabel11.setFont(jLabel11.getFont().deriveFont(jLabel11.getFont().getSize()-2f));
        jLabel11.setText("The value entered is added to all values in the terrain grid.");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(10, 0, 0, 0);
        offsetGridPanel.add(jLabel11, gridBagConstraints);

        voidGridPanel.setLayout(new java.awt.GridBagLayout());

        jLabel12.setText("Mark this value as void:");
        voidGridPanel.add(jLabel12, new java.awt.GridBagConstraints());

        voidGridFormattedTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter()));
        voidGridFormattedTextField.setPreferredSize(new java.awt.Dimension(200, 28));
        voidGridFormattedTextField.setValue(new Float(0));
        voidGridPanel.add(voidGridFormattedTextField, new java.awt.GridBagConstraints());

        jLabel13.setFont(jLabel13.getFont().deriveFont(jLabel13.getFont().getSize()-2f));
        jLabel13.setText("All values in the grid with the selected value will be set to the void value.");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(10, 0, 0, 0);
        voidGridPanel.add(jLabel13, gridBagConstraints);

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Pyramid Shader");
        setMinimumSize(new java.awt.Dimension(700, 700));

        navigableImagePanel.setNavigationImageEnabled(false);
        getContentPane().add(navigableImagePanel, java.awt.BorderLayout.CENTER);

        fileMenu.setText("File");
        fileMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                fileMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });

        openMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_O, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        openMenuItem.setText("Open Grid…");
        openMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                openMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(openMenuItem);

        openRecentMenu.setText("Open Recent");
        fileMenu.add(openRecentMenu);
        fileMenu.add(jSeparator1);

        saveGridMenuItem.setText("Save Grid…");
        saveGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveGridMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(saveGridMenuItem);

        saveGeneralizedGridMenuItem.setText("Save Generalized Grid…");
        saveGeneralizedGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveGeneralizedGridMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(saveGeneralizedGridMenuItem);

        saveLocalGridMenuItem.setText("Save Locally Filtered Grid…");
        saveLocalGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveLocalGridMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(saveLocalGridMenuItem);

        saveDownsampledMenu.setText("Save Downsampled Grid");
        saveDownsampledMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                saveDownsampledMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });
        fileMenu.add(saveDownsampledMenu);
        fileMenu.add(jSeparator5);

        saveImageMenu.setText("Save Image");

        saveTIFFImageMenuItem.setText("TIFF");
        saveTIFFImageMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveTIFFImageMenuItemActionPerformed(evt);
            }
        });
        saveImageMenu.add(saveTIFFImageMenuItem);

        savePNGImageMenuItem.setText("PNG");
        savePNGImageMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                savePNGImageMenuItemActionPerformed(evt);
            }
        });
        saveImageMenu.add(savePNGImageMenuItem);

        fileMenu.add(saveImageMenu);

        saveContoursMenu.setText("Save Contour Image");

        saveTIFFContoursMenuItem.setText("TIFF");
        saveTIFFContoursMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveTIFFContoursMenuItemActionPerformed(evt);
            }
        });
        saveContoursMenu.add(saveTIFFContoursMenuItem);

        savePNGContoursMenuItem.setText("PNG");
        savePNGContoursMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                savePNGContoursMenuItemActionPerformed(evt);
            }
        });
        saveContoursMenu.add(savePNGContoursMenuItem);

        fileMenu.add(saveContoursMenu);

        saveContoursMenu1.setText("Save Normal Map");

        saveTIFFNormalMapMenuItem.setText("TIFF");
        saveTIFFNormalMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveTIFFNormalMapMenuItemActionPerformed(evt);
            }
        });
        saveContoursMenu1.add(saveTIFFNormalMapMenuItem);

        savePNGNormalMapMenuItem.setText("PNG");
        savePNGNormalMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                savePNGNormalMapMenuItemActionPerformed(evt);
            }
        });
        saveContoursMenu1.add(savePNGNormalMapMenuItem);

        fileMenu.add(saveContoursMenu1);

        menuBar.add(fileMenu);

        editMenu.setText("Edit");
        editMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                editMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });

        scaleGridMenuItem.setText("Scale Grid Values…");
        scaleGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                scaleGridMenuItemActionPerformed(evt);
            }
        });
        editMenu.add(scaleGridMenuItem);

        offsetGridMenuItem.setText("Offset Grid Values…");
        offsetGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                offsetGridMenuItemActionPerformed(evt);
            }
        });
        editMenu.add(offsetGridMenuItem);

        voidGridMenuItem.setText("Void Grid Values…");
        voidGridMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                voidGridMenuItemActionPerformed(evt);
            }
        });
        editMenu.add(voidGridMenuItem);

        menuBar.add(editMenu);

        viewMenu.setText("View");
        viewMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                viewMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });

        viewResetMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_0, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        viewResetMenuItem.setText("Reset View");
        viewResetMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                viewResetMenuItemActionPerformed(evt);
            }
        });
        viewMenu.add(viewResetMenuItem);
        viewMenu.add(jSeparator4);

        viewZoomInMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_PLUS,    java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        viewZoomInMenuItem.setText("Zoom In");
        viewZoomInMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                viewZoomInMenuItemActionPerformed(evt);
            }
        });
        viewMenu.add(viewZoomInMenuItem);

        viewZoomOutMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_MINUS,    java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        viewZoomOutMenuItem.setText("Zoom Out");
        viewZoomOutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                viewZoomOutMenuItemActionPerformed(evt);
            }
        });
        viewMenu.add(viewZoomOutMenuItem);

        menuBar.add(viewMenu);

        infoMenu.setText("Window");
        infoMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                infoMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });

        settingsMenuItem.setText("Show Settings");
        settingsMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                settingsMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(settingsMenuItem);
        infoMenu.add(jSeparator3);

        terrainInfoMenuItem.setText("Grid Info");
        terrainInfoMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                terrainInfoMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(terrainInfoMenuItem);
        infoMenu.add(jSeparator2);

        infoMenuItem.setText("Info");
        infoMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                infoMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(infoMenuItem);

        menuBar.add(infoMenu);

        experimentalMenu.setText("Experimental");

        planObliqueFeaturesMenuItem.setText("Plan Oblique Lines…");
        planObliqueFeaturesMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                planObliqueFeaturesMenuItemActionPerformed(evt);
            }
        });
        experimentalMenu.add(planObliqueFeaturesMenuItem);

        menuBar.add(experimentalMenu);

        setJMenuBar(menuBar);

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void initRecentDocumentsMenu() {
        final String APPNAME = "PyramidShader";
        rdm = new RecentDocumentsManager() {

            private Preferences getPreferences() {
                return Preferences.userNodeForPackage(MainWindow.class);
            }

            @Override
            protected byte[] readRecentDocs() {
                return getPreferences().getByteArray("RecentDocuments" + APPNAME, null);
            }

            @Override
            protected void writeRecentDocs(byte[] data) {
                getPreferences().putByteArray("RecentDocuments" + APPNAME, data);
            }

            @Override
            protected void openFile(File file, ActionEvent event) {
                if (file != null) {
                    try {
                        openGrid(file.getCanonicalPath());
                    } catch (IOException ex) {
                        ErrorDialog.showErrorDialog(OPEN_ERROR_MESSAGE, "Error", ex, null);
                    }
                }
            }

            @Override
            protected Icon getFileIcon(File file) {
                return null;
            }
        };
    }

    // A SwingWorker for rendering the background image to a file.
    class FileRenderer extends SwingWorkerWithProgressIndicatorDialog<Void> {

        private final BufferedImage backgroundImage;
        private final String filePath;
        private final String fileFormat;

        protected FileRenderer(BufferedImage backgroundImage, String filePath,
                String fileFormat, Frame owner) {
            super(owner, "", "", true);
            this.backgroundImage = backgroundImage;
            this.filePath = filePath;
            this.fileFormat = fileFormat;
            this.setIndeterminate(false);
            this.setCancellable(false);
        }

        @Override
        public Void doInBackground() {
            if (model.getGeneralizedGrid() != null) {
                // initialize the progress dialog
                start();
                model.renderBackgroundImage(backgroundImage, this);
            }
            return null;
        }

        @Override
        protected void done() {
            try {
                if (!isCancelled()) {
                    get();
                    this.setIndeterminate(true);
                    ImageIO.write(backgroundImage, fileFormat, new File(filePath));

                    // create world file for image file
                    String worldFilePath = WorldFileExporter.constructPath(filePath);
                    Grid dem = model.getGeneralizedGrid();
                    double cellSize = dem.getCellSize();
                    // for each grid value, one image cell is contructed. The extent
                    // of the image is therefore one pixel larger horizontally and
                    // vertically.
                    double west = dem.getWest() - cellSize / 2;
                    double north = dem.getNorth() + cellSize / 2;
                    WorldFileExporter.writeWorldFile(worldFilePath, cellSize, west, north);
                }
            } catch (Exception ignore) {
            } finally {
                completeProgress();
            }
        }
    }

    private void saveImage(String format, String message) {
        String filePath = askFile(message, false); // file selection dialog
        // make sure the choice is a valid file
        if (filePath != null) {
            filePath = FileUtils.forceFileNameExtension(filePath, format);
            // render to image with the same size as the input grid
            // instead of using navigableImagePanel.getImage(), which can
            // have a different size
            BufferedImage img = model.createDestinationImage(1);
            new FileRenderer(img, filePath, format, this).execute();
        }
    }
    private void saveTIFFImageMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveTIFFImageMenuItemActionPerformed
        saveImage("tif", "Save TIFF Image");
    }//GEN-LAST:event_saveTIFFImageMenuItemActionPerformed

    private void saveGeneralizedGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveGeneralizedGridMenuItemActionPerformed
        saveTerrain(model.getGeneralizedGrid());
    }//GEN-LAST:event_saveGeneralizedGridMenuItemActionPerformed

    private void openMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openMenuItemActionPerformed
        openGrid();
    }//GEN-LAST:event_openMenuItemActionPerformed

    private void infoMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_infoMenuItemActionPerformed
        ProgramInfoPanel.showApplicationInfo(this);
    }//GEN-LAST:event_infoMenuItemActionPerformed

    private void terrainInfoMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_terrainInfoMenuItemActionPerformed
        StringBuilder sb = new StringBuilder();
        sb.append("<html> <b>");
        sb.append(getTitle());
        sb.append("</b><br>");
        sb.append(model.getGrid().getDescriptionWithStatistics("<br>"));
        String title = "Terrain Model Info";
        JOptionPane.showMessageDialog(this, sb.toString(), title, JOptionPane.PLAIN_MESSAGE);
    }//GEN-LAST:event_terrainInfoMenuItemActionPerformed

    private void settingsMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_settingsMenuItemActionPerformed
        settingsDialog.setVisible(!settingsDialog.isVisible());
    }//GEN-LAST:event_settingsMenuItemActionPerformed

    private void infoMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_infoMenuMenuSelected
        boolean gridLoaded = (model.getGrid() != null);
        terrainInfoMenuItem.setEnabled(gridLoaded);

        boolean settingsVisible = settingsDialog.isVisible();
        settingsMenuItem.setText(settingsVisible ? "Hide Settings" : "Show Settings");
    }//GEN-LAST:event_infoMenuMenuSelected

    private void viewResetMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewResetMenuItemActionPerformed
        navigableImagePanel.setZoom(0);
    }//GEN-LAST:event_viewResetMenuItemActionPerformed

    private void viewZoomInMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewZoomInMenuItemActionPerformed
        navigableImagePanel.setZoom(navigableImagePanel.getZoom() + navigableImagePanel.getZoomIncrement());
    }//GEN-LAST:event_viewZoomInMenuItemActionPerformed

    private void viewZoomOutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewZoomOutMenuItemActionPerformed
        navigableImagePanel.setZoom(navigableImagePanel.getZoom() - navigableImagePanel.getZoomIncrement());
    }//GEN-LAST:event_viewZoomOutMenuItemActionPerformed

    private void savePNGImageMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_savePNGImageMenuItemActionPerformed
        saveImage("png", "Save PNG Image");
    }//GEN-LAST:event_savePNGImageMenuItemActionPerformed

    /**
     * Render an image and write it to a file, while a progress dialog with a
     * cancel button is displayed.
     *
     * @param filePath file to write to.
     * @param scale The final image will be this many times larger than the
     * terrain model.
     */
    private void exportContours(final String filePath, final String imageFormat, final int scale) {
        String dialogTitle = "Contours Export";
        SwingWorkerWithProgressIndicatorDialog worker;
        worker = new SwingWorkerWithProgressIndicatorDialog<Void>(this, dialogTitle, "", true) {
            @Override
            public void done() {
                try {
                    // a call to get() will throw an ExecutionException if an 
                    // exception occured in doInBackground
                    get();

                    // create world file for image file
                    String worldFilePath = WorldFileExporter.constructPath(filePath);
                    Grid dem = model.getGeneralizedGrid();
                    double cellSize = dem.getCellSize() / scale;
                    double west = dem.getWest() - cellSize / 2;
                    double north = dem.getNorth() + cellSize / 2;
                    WorldFileExporter.writeWorldFile(worldFilePath, cellSize, west, north);
                } catch (ExecutionException e) {
                    // an exception was thrown in doInBackground
                    new File(filePath).delete();
                    String msg = "An error occured when exporting the image.";
                    String title = "Export Error";
                    JOptionPane.showMessageDialog(getContentPane(), msg, title, JOptionPane.ERROR_MESSAGE);
                } catch (InterruptedException | CancellationException e) {
                } catch (IOException exc) {
                    new File(filePath).delete();
                } finally {
                    // hide the progress dialog
                    completeProgress();
                }
            }

            @Override
            protected Void doInBackground() throws Exception {
                // initialize the progress dialog
                start();
                // compute the image with illuminated contours
                IlluminatedContoursOperator op;
                boolean illuminated = (model.foregroundVisualization == ForegroundVisualization.ILLUMINATED_CONTOURS);

                op = model.setupIlluminatedContoursOperator(illuminated);
                int w = model.getGeneralizedGrid().getCols() * scale;
                int h = model.getGeneralizedGrid().getRows() * scale;
                BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
                op.renderToImage(image, model.getGeneralizedGrid(),
                        model.getGeneralizedSlopeGrid(), this);

                if (!isCancelled()) {
                    // writing to file cannot be canceled
                    setCancellable(false);
                    setIndeterminate(true);

                    File file = new File(filePath);
                    setMessage("Writing image to \"" + file.getName() + "\"");
                    ImageIO.write(image, imageFormat, file);
                }
                return null;
            }
        };

        worker.setMaxTimeWithoutDialogMilliseconds(500);
        worker.setIndeterminate(false);
        worker.setMessage("Computing contours image");
        worker.execute();
    }

    private void saveContours(String askFileMessage, String imageFormat) {
        String PREFS_NAME = "contours_scale";
        String filePath = askFile(askFileMessage, false);
        if (filePath != null) {
            filePath = FileUtils.forceFileNameExtension(filePath, imageFormat);
            String title = "Image Resolution";
            Preferences prefs = Preferences.userNodeForPackage(MainWindow.class);
            imageResolutionSpinner.setValue(prefs.getInt(PREFS_NAME, 5));
            int res = JOptionPane.showOptionDialog(getContentPane(), imageResolutionPanel, title,
                    JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null);
            if (res == JOptionPane.OK_OPTION) {
                int scale = (Integer) (imageResolutionSpinner.getValue());
                exportContours(filePath, imageFormat, scale);
                prefs.putInt(PREFS_NAME, scale);
            }
        }
    }
    private void saveTIFFContoursMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveTIFFContoursMenuItemActionPerformed
        saveContours("Save TIFF Contour Image", "tif");
    }//GEN-LAST:event_saveTIFFContoursMenuItemActionPerformed

    private void savePNGContoursMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_savePNGContoursMenuItemActionPerformed
        saveContours("Save PNG Contour Image", "png");
    }//GEN-LAST:event_savePNGContoursMenuItemActionPerformed

    private void fileMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_fileMenuMenuSelected
        boolean gridLoaded = (model.getGrid() != null);
        boolean contoursVisible
                = model.foregroundVisualization != Model.ForegroundVisualization.NONE;
        saveGridMenuItem.setEnabled(gridLoaded);
        saveGeneralizedGridMenuItem.setEnabled(gridLoaded);
        saveLocalGridMenuItem.setEnabled(model.backgroundVisualization.isLocal());
        saveDownsampledMenu.setEnabled(gridLoaded);
        saveTIFFImageMenuItem.setEnabled(gridLoaded);
        savePNGImageMenuItem.setEnabled(gridLoaded);
        saveTIFFContoursMenuItem.setEnabled(contoursVisible);
        savePNGContoursMenuItem.setEnabled(contoursVisible);
        saveTIFFNormalMapMenuItem.setEnabled(gridLoaded);
        savePNGNormalMapMenuItem.setEnabled(gridLoaded);
    }//GEN-LAST:event_fileMenuMenuSelected

    private void viewMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_viewMenuMenuSelected
        boolean gridLoaded = (model.getGrid() != null);
        viewResetMenuItem.setEnabled(gridLoaded);
        viewZoomInMenuItem.setEnabled(gridLoaded);
        viewZoomOutMenuItem.setEnabled(gridLoaded);
    }//GEN-LAST:event_viewMenuMenuSelected

    private void saveLocalGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveLocalGridMenuItemActionPerformed
        saveTerrain(model.getLocalGrid());
    }//GEN-LAST:event_saveLocalGridMenuItemActionPerformed

    private void scaleGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scaleGridMenuItemActionPerformed
        int option = JOptionPane.showOptionDialog(this,
                scaleGridPanel,
                "Scale Terrain",
                JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null);
        if (option != JOptionPane.OK_OPTION) {
            return;
        }

        try {
            scaleTerrainFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (scaleTerrainFormattedTextField.getValue());
            float scale = f.floatValue();
            model.scaleGrid(scale);
            settingsDialog.modelChanged();
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while scaling the terrain.",
                    "Error", exc, this);
        }
    }//GEN-LAST:event_scaleGridMenuItemActionPerformed

    private void offsetGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_offsetGridMenuItemActionPerformed
        int option = JOptionPane.showOptionDialog(this,
                offsetGridPanel,
                "Vertically Offset Terrain",
                JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null);
        if (option != JOptionPane.OK_OPTION) {
            return;
        }

        try {
            offsetTerrainFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (offsetTerrainFormattedTextField.getValue());
            float offset = f.floatValue();
            model.verticallyOffsetGrid(offset);
            settingsDialog.modelChanged();
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the terrain.",
                    "Error", exc, this);
        }
    }//GEN-LAST:event_offsetGridMenuItemActionPerformed

    private void editMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_editMenuMenuSelected
        boolean gridLoaded = (model.getGrid() != null);
        scaleGridMenuItem.setEnabled(gridLoaded);
        offsetGridMenuItem.setEnabled(gridLoaded);
        voidGridMenuItem.setEnabled(gridLoaded);
    }//GEN-LAST:event_editMenuMenuSelected

    /**
     * Draw a set of lines into an image
     *
     * @param img
     * @param lines
     * @param grid
     */
    private static void drawLines(BufferedImage img, GeometryCollection lines, Grid grid) {
        int imageHeight = img.getHeight();
        Graphics2D g2 = (Graphics2D) img.getGraphics();
        RenderingHints rh = new RenderingHints(
                RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHints(rh);
        g2.setStroke(new BasicStroke(3));
        g2.setColor(Color.BLACK);

        double dx = grid.getWest();
        double sx = img.getWidth() / (grid.getEast() - grid.getWest());
        double dy = grid.getSouth();
        double sy = img.getHeight() / (grid.getNorth() - grid.getSouth());
        int nLines = lines.getNumGeometries();
        for (int lineID = 0; lineID < nLines; lineID++) {
            LineString line = (LineString) lines.getGeometryN(lineID);
            int nPoints = line.getNumPoints();
            GeneralPath path = new GeneralPath();
            Point pt = line.getFirstPoint();
            double x = (pt.getX() - dx) * sx;
            double y = (pt.getY() - dy) * sy;
            path.moveTo(x, imageHeight - y);
            for (int pointID = 1; pointID < nPoints; pointID++) {
                pt = line.getPointN(pointID);
                x = (pt.getX() - dx) * sx;
                y = (pt.getY() - dy) * sy;
                path.lineTo(x, imageHeight - y);
            }
            g2.draw(path);
        }
        g2.dispose();
    }

    private void planObliqueFeaturesMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_planObliqueFeaturesMenuItemActionPerformed
        try {
            // erase previously drawn lines
            navigableImagePanel.repaint();

            if (model.planObliqueAngle == 90) {
                String msg = "Please first adjust the plan oblique relief angle.";
                String title = "Plan Oblique Lines";
                JOptionPane.showMessageDialog(getContentPane(), msg, title, JOptionPane.WARNING_MESSAGE);
                return;
            }

            // ask the user for a file to read
            String filePath = askFile("Select a Line Shapefile", true);
            if (filePath == null) {
                // user canceled
                return;
            }
            GeometryCollectionImporter importer = new ShapeImporter();
            GeometryCollection vectors = importer.importData(filePath);

            // test whether the vector geometry intersects with the grid
            Rectangle2D vectorsBB = vectors.getBoundingBox();
            Rectangle2D gridBB = model.getGeneralizedGrid().getBoundingBox();
            if (GeometryUtils.rectanglesIntersect(gridBB, vectorsBB) == false) {
                String msg = "<html>The shapefile geometry does not align with "
                        + "the terrain model.<br>It is likely that the shapefile "
                        + "and the grid use different coordinate systems.";
                String title = "Data Misalignment";
                JOptionPane.showMessageDialog(getContentPane(), msg, title, JOptionPane.ERROR_MESSAGE);
                return;
            }

            // ask the user whether occluded line segments are to be clipped
            String[] options = new String[]{"Clip", "Do Not Clip", "Cancel"};
            String title = "Occlusion Clipping";
            String msg = "Clip occluded line segments?";
            int lineClipping = JOptionPane.showOptionDialog(getContentPane(), msg, title,
                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE,
                    null, options, options[0]);
            if (lineClipping == 2) {
                // user canceled
                return;
            }

            // apply shearing
            PlanObliqueShearing shearingOp = new PlanObliqueShearing(
                    model.planObliqueAngle,
                    model.getGeneralizedGrid(),
                    model.getGeneralizedGrid().getMinMax()[0],
                    lineClipping == 0);
            GeometryCollection shearedLines = shearingOp.shear(vectors);

            BufferedImage img = navigableImagePanel.getImage();
            drawLines(img, shearedLines, model.getGeneralizedGrid());
            navigableImagePanel.repaint();

            // ask the user for a file to save
            filePath = askFile("Save Plan Oblique Lines", false);
            if (filePath == null) {
                // user canceled
                return;
            }
            filePath = FileUtils.forceFileNameExtension(filePath, "shp");
            ShapeExporter geometryExporter = new ShapeExporter();
            geometryExporter.export(shearedLines, filePath);
            geometryExporter.exportTableForGeometry(filePath, shearedLines, "");
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_planObliqueFeaturesMenuItemActionPerformed

    private void saveDownsampledMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_saveDownsampledMenuMenuSelected
        saveDownsampledMenu.removeAll();
        if (model == null || model.laplacianPyramid == null) {
            return;
        }
        Grid[] levels = model.laplacianPyramid.getLevels();
        for (int i = 0; i < levels.length; i++) {
            String str = levels[i].getCols() + "\u00D7" + levels[i].getRows();
            if (i == 0) {
                str += " (Full Resolution)";
            }
            JMenuItem menuItem = new JMenuItem(str);
            menuItem.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    // weights for Laplacian pyramid levels
                    float[] w = model.laplacianPyramid.createConstantWeights(1f);
                    for (int j = 0; j < saveDownsampledMenu.getMenuComponentCount(); j++) {
                        Component comp = saveDownsampledMenu.getMenuComponent(j);
                        if (comp == e.getSource()) {
                            Arrays.fill(w, 0, j, 0f);
                            break;
                        }
                    }

                    saveTerrain(model.laplacianPyramid.sumLevels(w, false));
                }
            });
            saveDownsampledMenu.add(menuItem);
        }
    }//GEN-LAST:event_saveDownsampledMenuMenuSelected

    private void saveNormalMap(String format) {
        String filePath = askFile("Normal Map", false);
        if (filePath != null) {
            filePath = FileUtils.forceFileNameExtension(filePath, format);
            try {
                BufferedImage img = model.createDestinationImage(1);
                NormalMapOperator op = new NormalMapOperator(Channel.R, Channel.G, Channel.B,
                        false, false, false, 1f);
                op.operate(model.getGeneralizedGrid(), img, model.shadingVerticalExaggeration);
                ImageIO.write(img, format, new File(filePath));

                // create world file for image file
                String worldFilePath = WorldFileExporter.constructPath(filePath);
                Grid dem = model.getGeneralizedGrid();
                double cellSize = dem.getCellSize();
                // for each grid value, one image cell is contructed. The extent
                // of the image is therefore one pixel larger horizontally and
                // vertically.
                double west = dem.getWest() - cellSize / 2;
                double north = dem.getNorth() + cellSize / 2;
                WorldFileExporter.writeWorldFile(worldFilePath, cellSize, west, north);
            } catch (IOException ex) {
                ErrorDialog.showErrorDialog(SAVE_IMAGE_ERROR_MESSAGE, "Error", ex, this);
            }
        }
    }
    private void saveTIFFNormalMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveTIFFNormalMapMenuItemActionPerformed
        saveNormalMap("tif");
    }//GEN-LAST:event_saveTIFFNormalMapMenuItemActionPerformed

    private void savePNGNormalMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_savePNGNormalMapMenuItemActionPerformed
        saveNormalMap("png");
    }//GEN-LAST:event_savePNGNormalMapMenuItemActionPerformed

    private void voidGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_voidGridMenuItemActionPerformed
                int option = JOptionPane.showOptionDialog(this,
                voidGridPanel,
                "Void Values",
                JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null);
        if (option != JOptionPane.OK_OPTION) {
            return;
        }

        try {
            voidGridFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (voidGridFormattedTextField.getValue());
            model.voidGridValue(f.floatValue());
            settingsDialog.modelChanged();
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the terrain.",
                    "Error", exc, this);
        }
    }//GEN-LAST:event_voidGridMenuItemActionPerformed

    private void saveGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveGridMenuItemActionPerformed
        saveTerrain(model.getGrid());
    }//GEN-LAST:event_saveGridMenuItemActionPerformed

    /**
     * Ask the user for a file to read or write.
     *
     * @param frame A Frame for which to display the dialog.
     * @param message A message that will be displayed in the dialog.
     * @param load Pass true if an existing file for reading should be selected.
     * Pass false if a new file for writing should be specified.
     * @return A path to the file, including the file name.
     */
    private String askFile(String message, boolean load) {
        int flag = load ? java.awt.FileDialog.LOAD : java.awt.FileDialog.SAVE;

        // display the dialog
        java.awt.FileDialog fd = new java.awt.FileDialog(this, message, flag);
        fd.setVisible(true);

        // construct the path to the file that the user selected.
        String fileName = fd.getFile();
        String directory = fd.getDirectory();
        if (fileName == null || directory == null) {
            return null;
        }
        return directory + fileName;
    }

    public void initDisplayImage() {
        // compute a scale factor for the display image
        // this makes sure contour lines for small grids do not 
        // appear overly pixelated
        GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
        int screenWidth = gd.getDisplayMode().getWidth();
        int screenHeight = gd.getDisplayMode().getHeight();
        int imageScaleFactor = 1;
        Dimension dim = model.getGridDimensionForDisplay();
        double heightRatio = (double) screenHeight / dim.getHeight();
        double widthRatio = (double) screenWidth / dim.getWidth();
        double maxRatio = Math.max(heightRatio, widthRatio);
        if (maxRatio > 1) {
            imageScaleFactor = (int) Math.ceil(maxRatio);
        }
        // initialize the display image
        BufferedImage image = model.createDestinationImage(imageScaleFactor);
        navigableImagePanel.setImage(image);
    }

    /**
     * Open a grid file and initialize the data model.
     *
     * @param filePath The file to open
     * @throws IOException
     */
    private void openGrid(final String filePath) throws IOException {
        SwingWorkerWithProgressIndicatorDialog worker;
        String dialogTitle = "Pyramid Shader";

        worker = new SwingWorkerWithProgressIndicatorDialog<Void>(this, dialogTitle, "", true) {

            @Override
            public void done() {
                try {
                    // a call to get() will throw an ExecutionException if an 
                    // exception occured in doInBackground
                    get();

                    // hide the progress dialog before rendering the image
                    // if rendering throws an error, the progress dialog should 
                    // have been closed
                    completeProgress();

                    // set window title to file name
                    setTitle(filePath.substring(filePath.lastIndexOf(File.separator) + 1));

                    // add document to Open Recent menu
                    rdm.addDocument(new File(filePath), null);
                } catch (InterruptedException | CancellationException e) {
                    return;
                } catch (Throwable e) {
                    // hide the progress dialog
                    completeProgress();
                    // an exception was thrown in doInBackground
                    String msg = "<html>An error occured when importing the terrain model."
                            + "<br>The file must be in Esri ASCII Grid format.</html>";
                    ErrorDialog.showErrorDialog(msg, "Error", null, getContentPane());
                    return;
                } finally {
                    // hide the progress dialog
                    completeProgress();
                }

                try {
                    initDisplayImage();
                    
                    // this will render the image
                    settingsDialog.modelChanged();

                    // display warning when value range in grid is so low that
                    // shading does not show any variation
                    float[] minMax = model.getGrid().getMinMax();
                    float range = minMax[1] - minMax[0];
                    if (range < model.getGrid().getCellSize() * 2) {
                        String msg = "<html>"
                                + "This grid is rather flat and the shading may "
                                + "not show any variation."
                                + "<br>Use <i>Edit > " + scaleGridMenuItem.getText()
                                + "</i> to vertically exaggerate the grid."
                                + "</html>";
                        JOptionPane.showMessageDialog(getContentPane(), msg,
                                "Pyramid Shader", JOptionPane.WARNING_MESSAGE);
                    }
                } catch (Throwable e) {
                    BufferedImage img = navigableImagePanel.getImage();
                    if (img != null) {
                        Graphics g = img.getGraphics();
                        g.setColor(Color.WHITE);
                        g.fillRect(0, 0, img.getWidth(), img.getHeight());
                        g.dispose();
                    }
                    String msg = "An error occured when rendering the terrain.";
                    ErrorDialog.showErrorDialog(msg, "Error", e, getContentPane());
                    assert false : e.getStackTrace();
                }
            }

            @Override
            protected Void doInBackground() throws Exception {
                start();
                //import the DEM and create pyramids
                Grid grid = EsriASCIIGridReader.read(filePath, this);
                this.setIndeterminate(true);
                this.setCancellable(false);
                model.setGrid(grid);
                return null;
            }

        };

        worker.setMaxTimeWithoutDialogMilliseconds(2000);
        worker.setIndeterminate(false);
        worker.setMessage("Importing Terrain Model");
        worker.execute();
    }

    /**
     * Ask user for grid file, open the file, and initialized data model.
     */
    public void openGrid() {
        try {
            // ask the user for a file
            String filePath = askFile("Select an Esri ASCII Grid", true);
            if (filePath != null) {
                openGrid(filePath);
            }
        } catch (IOException ex) {
            ErrorDialog.showErrorDialog(OPEN_ERROR_MESSAGE, "Error", ex, this);
        }
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenu editMenu;
    private javax.swing.JMenu experimentalMenu;
    private javax.swing.JPanel imageResolutionPanel;
    private javax.swing.JSpinner imageResolutionSpinner;
    private javax.swing.JMenuItem infoMenuItem;
    private javax.swing.JMenuBar menuBar;
    private edu.oregonstate.cartography.gui.NavigableImagePanel navigableImagePanel;
    private javax.swing.JMenuItem offsetGridMenuItem;
    private javax.swing.JPanel offsetGridPanel;
    private javax.swing.JFormattedTextField offsetTerrainFormattedTextField;
    private javax.swing.JMenu openRecentMenu;
    private javax.swing.JMenuItem planObliqueFeaturesMenuItem;
    private javax.swing.JMenu saveDownsampledMenu;
    private javax.swing.JMenuItem saveGeneralizedGridMenuItem;
    private javax.swing.JMenuItem saveGridMenuItem;
    private javax.swing.JMenuItem saveLocalGridMenuItem;
    private javax.swing.JMenuItem savePNGContoursMenuItem;
    private javax.swing.JMenuItem savePNGImageMenuItem;
    private javax.swing.JMenuItem savePNGNormalMapMenuItem;
    private javax.swing.JMenuItem saveTIFFContoursMenuItem;
    private javax.swing.JMenuItem saveTIFFImageMenuItem;
    private javax.swing.JMenuItem saveTIFFNormalMapMenuItem;
    private javax.swing.JMenuItem scaleGridMenuItem;
    private javax.swing.JPanel scaleGridPanel;
    private javax.swing.JFormattedTextField scaleTerrainFormattedTextField;
    private javax.swing.JMenuItem settingsMenuItem;
    private javax.swing.JMenuItem terrainInfoMenuItem;
    private javax.swing.JMenu viewMenu;
    private javax.swing.JMenuItem viewResetMenuItem;
    private javax.swing.JMenuItem viewZoomInMenuItem;
    private javax.swing.JMenuItem viewZoomOutMenuItem;
    private javax.swing.JFormattedTextField voidGridFormattedTextField;
    private javax.swing.JMenuItem voidGridMenuItem;
    private javax.swing.JPanel voidGridPanel;
    // End of variables declaration//GEN-END:variables

    /**
     * @return the model
     */
    public Model getModel() {
        return model;
    }

    public void repaintImage() {
        navigableImagePanel.repaint();
    }

    public BufferedImage getImage() {
        return navigableImagePanel.getImage();
    }

    void setPreferredImageSize(Dimension dimension) {
        navigableImagePanel.setPreferredSize(dimension);
    }

    /**
     * @param settingsDialog the settingsDialog to set
     */
    public void setSettingsDialog(SettingsDialog settingsDialog) {
        this.settingsDialog = settingsDialog;
        navigableImagePanel.addPropertyChangeListener(settingsDialog);
    }

    private void saveTerrain(Grid grid) {
        //call up a save file dialog
        String filePath = askFile("Save Terrain File", false);
        if (filePath != null) {
            filePath = FileUtils.forceFileNameExtension(filePath, "asc");
            try {
                ESRIASCIIGridExporter.export(grid, filePath);
            } catch (IOException ex) {
                ErrorDialog.showErrorDialog(SAVE_TERRAIN_ERROR_MESSAGE, "Error", ex, this);
            }
        }
    }

    public ProgressPanel getProgressPanel() {
        return progressPanel;
    }
}
