package edu.oregonstate.cartography.gui;

import com.fizzysoft.sdu.RecentDocumentsManager;
import edu.oregonstate.cartography.grid.operators.shader.SphereLookupShader;
import edu.oregonstate.cartography.app.ApplicationInfo;
import edu.oregonstate.cartography.grid.operators.GridToImageOperator;
import edu.oregonstate.cartography.app.FileUtils;
import edu.oregonstate.cartography.app.GeometryUtils;
import edu.oregonstate.cartography.grid.importer.EsriASCIIGridImporter;
import edu.oregonstate.cartography.grid.exporter.EsriASCIIGridExporter;
import edu.oregonstate.cartography.grid.GaussianPyramid;
import edu.oregonstate.cartography.grid.Grid;
import edu.oregonstate.cartography.grid.MinMax;
import edu.oregonstate.cartography.grid.Model;
import edu.oregonstate.cartography.grid.ReflectanceMapper;
import edu.oregonstate.cartography.grid.exporter.GRASSASCIIGridExporter;
import edu.oregonstate.cartography.grid.exporter.GridExporter;
import edu.oregonstate.cartography.grid.exporter.SurferASCIIGridExporter;
import edu.oregonstate.cartography.grid.exporter.SurferBinary6GridExporter;
import edu.oregonstate.cartography.grid.exporter.SurferBinary7GridExporter;
import edu.oregonstate.cartography.grid.operators.AddOperator;
import edu.oregonstate.cartography.grid.operators.AddRandomOperator;
import edu.oregonstate.cartography.grid.operators.BinaryThresholdOperator;
import edu.oregonstate.cartography.grid.operators.DepressionFillOperator;
import edu.oregonstate.cartography.grid.operators.FlowAccumulationOperator;
import edu.oregonstate.cartography.grid.operators.FlowDirectionOperator;
import edu.oregonstate.cartography.grid.operators.MaximumBranchLengthsOperator;
import edu.oregonstate.cartography.grid.operators.MinMaxOperator;
import edu.oregonstate.cartography.grid.operators.PlanObliqueOperator;
import edu.oregonstate.cartography.grid.operators.ReplaceVoidOperator;
import edu.oregonstate.cartography.grid.operators.ScaleOperator;
import edu.oregonstate.cartography.grid.operators.ThreadedGridOperator;
import edu.oregonstate.cartography.grid.operators.StandardDeviationOperator;
import edu.oregonstate.cartography.grid.operators.VoidOperator;
import edu.oregonstate.cartography.grid.operators.WeightedSkeletonizerOperator;
import edu.oregonstate.cartography.grid.operators.ScaleTo255Operator;
import edu.oregonstate.cartography.grid.operators.SlopeOperator;
import edu.oregonstate.cartography.grid.workers.ContoursExporter;
import edu.oregonstate.cartography.grid.workers.FileRenderer;
import edu.oregonstate.cartography.grid.workers.FlatAreasMaskExporter;
import edu.oregonstate.cartography.grid.exporter.GridExportWorker;
import edu.oregonstate.cartography.grid.exporter.PGMGridExporter;
import edu.oregonstate.cartography.grid.exporter.Photoshop16BitGridExporter;
import edu.oregonstate.cartography.grid.importer.GridImportWorker;
import edu.oregonstate.cartography.grid.operators.LowPassOperator;
import edu.oregonstate.cartography.grid.workers.GridWorkerManager;
import edu.oregonstate.cartography.grid.workers.NormalMapExporter;
import edu.oregonstate.cartography.grid.workers.OperatorWorker;
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 edu.oregonstate.cartography.simplefeatures.input.GeometryCollectionImporter;
import edu.oregonstate.cartography.simplefeatures.input.ShapeImporter;
import edu.oregonstate.cartography.simplefeatures.output.ShapeExporter;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import javax.swing.border.Border;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.text.AttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;

/**
 * Shades a DEM grid 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 ERROR_DIALOG_TITLE = "Pyramid Shader Error";
    private static final String OPEN_ERROR_MESSAGE = "Could not open the grid file.";

    private static final FileNameExtensionFilter GRID_FILE_FILTER
            = new FileNameExtensionFilter("Grid", "asc", "txt", "grd");

    private final Model model;
    private SettingsDialog settingsDialog = null;
    private final GridWorkerManager gridWorkerManager = new GridWorkerManager();
    private RecentDocumentsManager rdm;
    private final ProgressPanel progressPanel;

    // additional zoom-in action for keys not handled by menu
    Action zoomInAction = new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            navigableImagePanel.zoomIn();
        }
    };

    // additional zoom out action for keys not handled by menu
    Action zoomOutAction = new AbstractAction() {
        @Override
        public void actionPerformed(ActionEvent e) {
            navigableImagePanel.zoomOut();
        }
    };

    /**
     * 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
        MenuKeysDispatcher.setupDialogActions(menuBar);

        // add zoom-in and zoom-out actions for alternative keys
        GUIUtil.zoomMenuCommands(zoomInAction, zoomOutAction, getRootPane());

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

        // alignment of text for current pointer position and value
        TabStop[] tabs = new TabStop[1];
        tabs[0] = new TabStop(80, TabStop.ALIGN_DECIMAL, TabStop.LEAD_NONE);
        TabSet tabset = new TabSet(tabs);
        StyleContext sc = StyleContext.getDefaultStyleContext();
        AttributeSet aset = sc.addAttribute(SimpleAttributeSet.EMPTY,
                StyleConstants.TabSet, tabset);
        infoTextPane.setParagraphAttributes(aset, false);

        showDebugMenuWhenAssertionsAreEnabled();

        // setup drop destination for grid files
        navigableImagePanel.setInfoString("Drag and drop a grid file here, or select File\u2009>\u2009Open Grid\u2026"); // \u2009 is a thin space
        Color focusColor = new Color(190, 230, 255);
        Border border = javax.swing.BorderFactory.createLineBorder(focusColor, 4);
        new FileDrop(null, navigableImagePanel, border, new FileDrop.Listener() {
            @Override
            public void filesDropped(java.io.File[] files) {
                try {
                    int maxNbrFilesToScan = Math.min(files.length, 100);
                    for (int i = 0; i < maxNbrFilesToScan; i++) {
                        String filePath = files[i].getCanonicalPath();
                        if (EsriASCIIGridImporter.canRead(filePath)) {
                            openGrid(filePath);
                        }
                    }
                } catch (IOException ex) {
                    ErrorDialog.showErrorDialog(OPEN_ERROR_MESSAGE, ERROR_DIALOG_TITLE,
                            ex, navigableImagePanel.getParent());
                }
            }
        });

        // event listener for updating position and value at pointer position
        navigableImagePanel.addPropertyChangeListener(new PropertyChangeListener() {
            final String outOfGrid = "\u2014"; // em dash
            final DecimalFormat valFormat = new DecimalFormat("#,##0.###");
            final DecimalFormat slopeFormat = new DecimalFormat("#0.0");

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if ("mouseMoved".equals(evt.getPropertyName())) {
                    int gridCol = -1, gridRow = -1;
                    double x = Double.NaN, y = Double.NaN;
                    boolean validXY = false;
                    int r = -1, g = -1, b = -1;
                    float val = Float.NaN, filteredVal = Float.NaN;
                    double slope = Double.NaN;

                    // extract values                    
                    if (evt.getNewValue() != null && model.hasGrid()) {
                        assert (evt.getNewValue() instanceof Point2D.Double);
                        Grid grid = model.getGrid();

                        // position
                        // relative pixel coordinates between 0 and 100
                        Point2D pt = (Point2D.Double) evt.getNewValue();
                        double xPerc = pt.getX();
                        double yPerc = pt.getY();
                        if (xPerc < 0) {
                            // smaller than 0 should not happen
                            gridCol = -1;
                        } else if (xPerc >= 100) {
                            // equal to 100 can happen; larger than 100 should not happen
                            gridCol = grid.getCols() - 1;
                        } else {
                            gridCol = (int) (xPerc / 100 * grid.getCols());
                        }
                        if (yPerc < 0) {
                            // smaller than 0 should not happen
                            gridRow = -1;
                        } else if (yPerc >= 100) {
                            // equal to 100 can happen; larger than 100 should not happen
                            gridRow = grid.getRows() - 1;
                        } else {
                            gridRow = (int) (yPerc / 100 * grid.getRows());
                        }
                        x = grid.getWest() + xPerc / 100 * grid.getWidth();
                        y = grid.getNorth() - yPerc / 100 * grid.getHeight();
                        validXY = Double.isFinite(x) && Double.isFinite(y);

                        // image RGB                    
                        BufferedImage image = navigableImagePanel.getImage();
                        if (image != null) {
                            int imageCol = (int) (xPerc / 100 * (image.getWidth() - 1));
                            int imageRow = (int) (yPerc / 100 * (image.getHeight() - 1));
                            int argb = image.getRGB(imageCol, imageRow);
                            Color color = new Color(argb, true);
                            r = color.getRed();
                            g = color.getGreen();
                            b = color.getBlue();
                        }

                        // grid values
                        val = grid.getValue(gridCol, gridRow);
                        if (model.isFilteringGrid() && model.getFilteredGrid() != null) {
                            filteredVal = model.getFilteredGrid().getValue(gridCol, gridRow);
                        }
                        slope = Math.toDegrees(Math.atan(grid.get8NeighborGradient(gridCol, gridRow)));
                    }

                    StringBuilder sb = new StringBuilder();

                    // value
                    if (model.isFilteringGrid()) {
                        sb.append("Original: \t");
                        sb.append(Float.isFinite(val) ? valFormat.format(val)
                                : (validXY ? "void" : outOfGrid));
                        sb.append("\nFiltered: \t");
                        sb.append(Float.isFinite(filteredVal) ? valFormat.format(filteredVal)
                                : (validXY ? "void" : outOfGrid));
                    } else {
                        sb.append("Value: \t");
                        sb.append(Float.isFinite(val) ? valFormat.format(val) : (validXY ? "void" : outOfGrid));
                    }

                    // color
                    if (r >= 0) {
                        if (r == g && g == b) {
                            sb.append("\nGray: \t").append(r);
                        } else {
                            sb.append("\nRGB: \t").append(r).append(", ");
                            sb.append(g).append(", ").append(b);
                        }
                    } else {
                        boolean rgb = false;
                        if (infoTextPane.getText() != null) {
                            rgb = infoTextPane.getText().contains("RGB");
                        }
                        sb.append("\n").append(rgb ? "RGB" : "Gray").append(": \t").append(outOfGrid);
                    }

                    // slope
                    sb.append("\nSlope: \t");
                    boolean validSlope = Double.isFinite(slope);
                    sb.append(validSlope ? slopeFormat.format(slope) : outOfGrid);
                    if (validSlope) {
                        sb.append("\u00B0"); // degree sign
                    }

                    // x/y position
                    // grid may change between calls, so do not cache formatter for coordinates
                    boolean spherical = model.hasGrid() && model.getGrid().isCellSizeInSphericalCoordinates();
                    DecimalFormat xyFormat = validXY
                            ? new DecimalFormat(spherical ? "#,##0.#####" : "#,##0.#")
                            : null;
                    sb.append("\nX: \t");
                    sb.append(xyFormat != null ? xyFormat.format(x) : outOfGrid);
                    sb.append("\nY: \t");
                    sb.append(xyFormat != null ? xyFormat.format(y) : outOfGrid);

                    // col/row, 1-based
                    boolean validCell = model.hasGrid()
                            && gridCol >= 0 && gridCol < model.getGrid().getCols()
                            && gridRow >= 0 && gridRow < model.getGrid().getRows();
                    sb.append("\nCell:   ").append(validCell ? gridCol + 1 : outOfGrid);
                    sb.append(" / ").append(validCell ? gridRow + 1 : outOfGrid);

                    infoTextPane.setText(sb.toString());
                }
            }
        }
        );

    }

    /**
     * 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();
        scaleGridFormattedTextField = 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();
        offsetGridFormattedTextField = 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();
        replaceVoidGridPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel14 = new javax.swing.JLabel();
        replaceVoidGridFormattedTextField = new javax.swing.JFormattedTextField();
        javax.swing.JLabel jLabel15 = new javax.swing.JLabel();
        lineIntegralConvolutionButtonGroup = new javax.swing.ButtonGroup();
        addRandomPanel = new javax.swing.JPanel();
        javax.swing.JLabel jLabel16 = new javax.swing.JLabel();
        addRandomFormattedTextField = new javax.swing.JFormattedTextField();
        locationInfoDialog = new javax.swing.JDialog(this, false);
        infoTextPane = new javax.swing.JTextPane();
        navigableImagePanel = new edu.oregonstate.cartography.gui.NavigableImagePanel();
        menuBar = new javax.swing.JMenuBar();
        javax.swing.JMenu fileMenu = new javax.swing.JMenu();
        openMenuItem = new javax.swing.JMenuItem();
        openRecentMenu = rdm.createOpenRecentMenu();
        javax.swing.JPopupMenu.Separator jSeparator1 = new javax.swing.JPopupMenu.Separator();
        saveFilteredGridMenuItem = new javax.swing.JMenuItem();
        saveGridMenuItem = 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 saveFlatAreasMaskMenu = new javax.swing.JMenu();
        saveFlatAreasTIFFMenuItem = new javax.swing.JMenuItem();
        saveFlatAreasPNGMenuItem = 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();
        verticalOffsetGridMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator8 = new javax.swing.JPopupMenu.Separator();
        voidGridMenuItem = new javax.swing.JMenuItem();
        replaceVoidGridValuesMenuItem = new javax.swing.JMenuItem();
        jSeparator4 = new javax.swing.JPopupMenu.Separator();
        addRandomMenuItem = new javax.swing.JMenuItem();
        jSeparator20 = new javax.swing.JPopupMenu.Separator();
        applyFilterMenuItem = new javax.swing.JMenuItem();
        viewMenu = new javax.swing.JMenu();
        viewZoomInMenuItem = new javax.swing.JMenuItem();
        viewZoomOutMenuItem = new javax.swing.JMenuItem();
        zoomToFitMenuItem = new javax.swing.JMenuItem();
        zoomToFitMenuItem1 = new javax.swing.JMenuItem();
        javax.swing.JMenu infoMenu = new javax.swing.JMenu();
        javax.swing.JMenuItem minimizeMenuItem = new javax.swing.JMenuItem();
        javax.swing.JMenuItem zoomMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator16 = new javax.swing.JPopupMenu.Separator();
        settingsMenuItem = new javax.swing.JMenuItem();
        locationInfoMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator3 = new javax.swing.JPopupMenu.Separator();
        gridInfoMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator2 = new javax.swing.JPopupMenu.Separator();
        infoMenuItem = new javax.swing.JMenuItem();
        debugMenu = new javax.swing.JMenu();
        javax.swing.JMenuItem debugInfoMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator11 = new javax.swing.JPopupMenu.Separator();
        stdoutMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator10 = new javax.swing.JPopupMenu.Separator();
        javax.swing.JMenuItem setWindowTitleMenuItem = new javax.swing.JMenuItem();
        javax.swing.JMenuItem prjMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator6 = new javax.swing.JPopupMenu.Separator();
        planObliqueFeaturesMenuItem = new javax.swing.JMenuItem();
        savePlanObliqueMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator7 = new javax.swing.JPopupMenu.Separator();
        testMenuItem = new javax.swing.JMenuItem();
        javax.swing.JPopupMenu.Separator jSeparator12 = new javax.swing.JPopupMenu.Separator();
        exportMaxBranchLengthMenuItem = new javax.swing.JMenuItem();
        exportRidgeLinesMenuItem = new javax.swing.JMenuItem();
        exportSimplifiedRidgeLinesMenuItem = new javax.swing.JMenuItem();
        jSeparator15 = new javax.swing.JPopupMenu.Separator();
        exportDepressionsFilledMenuItem = new javax.swing.JMenuItem();
        exportAccumulationFlowMenuItem = new javax.swing.JMenuItem();
        exportValleyLinesMenuItem = new javax.swing.JMenuItem();
        exportSimplifiedValleyLinesMenuItem = new javax.swing.JMenuItem();
        jSeparator9 = new javax.swing.JPopupMenu.Separator();
        exportReflectanceMapMenuItem = new javax.swing.JMenuItem();
        exportCorrectedReflectanceMapMenuItem = new javax.swing.JMenuItem();
        exportReflectanceMapWithFlatToneMenuItem = new javax.swing.JMenuItem();
        exportAngleMapMenuItem = new javax.swing.JMenuItem();
        applyReflectanceMapMenuItem = new javax.swing.JMenuItem();
        exportSlopeMaskMenuItem = new javax.swing.JMenuItem();
        jSeparator13 = new javax.swing.JPopupMenu.Separator();
        varianceMenuItem = new javax.swing.JMenuItem();
        jSeparator14 = new javax.swing.JPopupMenu.Separator();
        exportLargeLandformsMenuItem = new javax.swing.JMenuItem();
        jSeparator17 = new javax.swing.JPopupMenu.Separator();
        exportSurfer6MenuItem = new javax.swing.JMenuItem();
        jSeparator21 = new javax.swing.JPopupMenu.Separator();
        timeDepressionFillerMenuItem = new javax.swing.JMenuItem();
        timeDepressionFillerMenuItem1 = new javax.swing.JMenuItem();
        sphericalShadingMenu = new javax.swing.JMenu();
        sphericalImage1MenuItem = new javax.swing.JMenuItem();
        sphericalImage1InfoMenuItem = new javax.swing.JMenuItem();
        jSeparator19 = new javax.swing.JPopupMenu.Separator();
        sphericalImage2MenuItem = new javax.swing.JMenuItem();
        sphericalImage2InfoMenuItem = new javax.swing.JMenuItem();
        jSeparator18 = new javax.swing.JPopupMenu.Separator();
        sphericalImagesReloadMenuItem = new javax.swing.JMenuItem();

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

        jLabel1.setText("Image resolution relative to grid 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());

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

        jLabel9.setFont(jLabel9.getFont().deriveFont(jLabel9.getFont().getSize()-2f));
        jLabel9.setText("All grid values are scaled by this 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);
        scaleGridPanel.add(jLabel9, gridBagConstraints);

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

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

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

        jLabel11.setFont(jLabel11.getFont().deriveFont(jLabel11.getFont().getSize()-2f));
        jLabel11.setText("The value entered is added to all grid values.");
        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("Change this value to void value:");
        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 entered 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);

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

        jLabel14.setText("Replace void values with this value:");
        replaceVoidGridPanel.add(jLabel14, new java.awt.GridBagConstraints());

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

        jLabel15.setFont(jLabel15.getFont().deriveFont(jLabel15.getFont().getSize()-2f));
        jLabel15.setText("All void values in the grid will be replaced with the entered 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);
        replaceVoidGridPanel.add(jLabel15, gridBagConstraints);

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

        jLabel16.setText("Maximum random value to add:");
        addRandomPanel.add(jLabel16, new java.awt.GridBagConstraints());

        addRandomFormattedTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter()));
        addRandomFormattedTextField.setPreferredSize(new java.awt.Dimension(200, 28));
        addRandomFormattedTextField.setValue(new Float(20));
        addRandomPanel.add(addRandomFormattedTextField, new java.awt.GridBagConstraints());

        locationInfoDialog.setAutoRequestFocus(false);
        locationInfoDialog.setFocusable(false);
        locationInfoDialog.setFocusableWindowState(false);
        locationInfoDialog.setResizable(false);
        locationInfoDialog.setType(java.awt.Window.Type.UTILITY);

        infoTextPane.setEditable(false);
        infoTextPane.setBorder(javax.swing.BorderFactory.createEmptyBorder(4, 8, 4, 8));
        infoTextPane.setFont(infoTextPane.getFont().deriveFont(infoTextPane.getFont().getSize()-2f));
        infoTextPane.setOpaque(false);
        infoTextPane.setPreferredSize(new java.awt.Dimension(180, 102));
        locationInfoDialog.getContentPane().add(infoTextPane, java.awt.BorderLayout.CENTER);

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

        navigableImagePanel.setBackground(new java.awt.Color(247, 247, 247));
        navigableImagePanel.setNavigationImageEnabled(false);
        navigableImagePanel.setOpaque(true);
        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);

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

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

        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.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_S, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        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);

        saveFlatAreasMaskMenu.setText("Save Flat Areas Mask");

        saveFlatAreasTIFFMenuItem.setText("TIFF");
        saveFlatAreasTIFFMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveFlatAreasTIFFMenuItemActionPerformed(evt);
            }
        });
        saveFlatAreasMaskMenu.add(saveFlatAreasTIFFMenuItem);

        saveFlatAreasPNGMenuItem.setText("PNG");
        saveFlatAreasPNGMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveFlatAreasPNGMenuItemActionPerformed(evt);
            }
        });
        saveFlatAreasMaskMenu.add(saveFlatAreasPNGMenuItem);

        fileMenu.add(saveFlatAreasMaskMenu);

        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);

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

        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);

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

        addRandomMenuItem.setText("Add Random Values…");
        addRandomMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                addRandomMenuItemActionPerformed(evt);
            }
        });
        editMenu.add(addRandomMenuItem);
        editMenu.add(jSeparator20);

        applyFilterMenuItem.setText("Apply Filter to Grid");
        applyFilterMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                applyFilterMenuItemActionPerformed(evt);
            }
        });
        editMenu.add(applyFilterMenuItem);

        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) {
            }
        });

        viewZoomInMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ADD, 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_SUBTRACT, 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);

        zoomToFitMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_0,    java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        zoomToFitMenuItem.setText("Zoom to Fit");
        zoomToFitMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                zoomToFitMenuItemActionPerformed(evt);
            }
        });
        viewMenu.add(zoomToFitMenuItem);

        zoomToFitMenuItem1.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_1,    java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        zoomToFitMenuItem1.setText("Zoom to 100%");
        zoomToFitMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                zoomToFitMenuItem1ActionPerformed(evt);
            }
        });
        viewMenu.add(zoomToFitMenuItem1);

        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) {
            }
        });

        minimizeMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_M, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        minimizeMenuItem.setText("Minimize");
        minimizeMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                minimizeMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(minimizeMenuItem);

        zoomMenuItem.setText("Zoom");
        zoomMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                zoomMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(zoomMenuItem);
        infoMenu.add(jSeparator16);

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

        locationInfoMenuItem.setText("Show Location Info");
        locationInfoMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                locationInfoMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(locationInfoMenuItem);
        infoMenu.add(jSeparator3);

        gridInfoMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        gridInfoMenuItem.setText("Grid Info");
        gridInfoMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                gridInfoMenuItemActionPerformed(evt);
            }
        });
        infoMenu.add(gridInfoMenuItem);
        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);

        debugMenu.setText("Debug");

        debugInfoMenuItem.setText("Debug menu is invisible when assertions are not enabled");
        debugInfoMenuItem.setEnabled(false);
        debugMenu.add(debugInfoMenuItem);
        debugMenu.add(jSeparator11);

        stdoutMenuItem.setText("Write stdout and stderr to Window");
        stdoutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                stdoutMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(stdoutMenuItem);
        debugMenu.add(jSeparator10);

        setWindowTitleMenuItem.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_T, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        setWindowTitleMenuItem.setText("Set Window Title…");
        setWindowTitleMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                setWindowTitleMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(setWindowTitleMenuItem);

        prjMenuItem.setText("Show .PRJ File Content…");
        prjMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                prjMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(prjMenuItem);
        debugMenu.add(jSeparator6);

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

        savePlanObliqueMenuItem.setText("Save Plan Oblique… ");
        savePlanObliqueMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                savePlanObliqueMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(savePlanObliqueMenuItem);
        debugMenu.add(jSeparator7);

        testMenuItem.setText("Test");
        testMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                testMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(testMenuItem);
        debugMenu.add(jSeparator12);

        exportMaxBranchLengthMenuItem.setText("Export Maximum Branch Length…");
        exportMaxBranchLengthMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportMaxBranchLengthMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportMaxBranchLengthMenuItem);

        exportRidgeLinesMenuItem.setText("Export Ridgelines…");
        exportRidgeLinesMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportRidgeLinesMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportRidgeLinesMenuItem);

        exportSimplifiedRidgeLinesMenuItem.setText("Export Simplified Ridgelines…");
        exportSimplifiedRidgeLinesMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportSimplifiedRidgeLinesMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportSimplifiedRidgeLinesMenuItem);
        debugMenu.add(jSeparator15);

        exportDepressionsFilledMenuItem.setText("Export Depressions Filled…");
        exportDepressionsFilledMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportDepressionsFilledMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportDepressionsFilledMenuItem);

        exportAccumulationFlowMenuItem.setText("Export Accumulation Flow…");
        exportAccumulationFlowMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportAccumulationFlowMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportAccumulationFlowMenuItem);

        exportValleyLinesMenuItem.setText("Export Valley Lines…");
        exportValleyLinesMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportValleyLinesMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportValleyLinesMenuItem);

        exportSimplifiedValleyLinesMenuItem.setText("Export Simplified Valley Lines…");
        exportSimplifiedValleyLinesMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportSimplifiedValleyLinesMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportSimplifiedValleyLinesMenuItem);
        debugMenu.add(jSeparator9);

        exportReflectanceMapMenuItem.setText("Export Diffuse Reflectance Map…");
        exportReflectanceMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportReflectanceMapMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportReflectanceMapMenuItem);

        exportCorrectedReflectanceMapMenuItem.setText("Export Diffuse Reflectance Map with Azimuth Correction…");
        exportCorrectedReflectanceMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportCorrectedReflectanceMapMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportCorrectedReflectanceMapMenuItem);

        exportReflectanceMapWithFlatToneMenuItem.setText("Export Diffuse Reflectance Map with Flat Area Tone…");
        exportReflectanceMapWithFlatToneMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportReflectanceMapWithFlatToneMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportReflectanceMapWithFlatToneMenuItem);

        exportAngleMapMenuItem.setText("Export Diffuse Angle Map…");
        exportAngleMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportAngleMapMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportAngleMapMenuItem);

        applyReflectanceMapMenuItem.setText("Apply Reflectance Map…");
        applyReflectanceMapMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                applyReflectanceMapMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(applyReflectanceMapMenuItem);

        exportSlopeMaskMenuItem.setText("Export Slope Mask…");
        exportSlopeMaskMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportSlopeMaskMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportSlopeMaskMenuItem);
        debugMenu.add(jSeparator13);

        varianceMenuItem.setText("Export Standard Deviation…");
        varianceMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                varianceMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(varianceMenuItem);
        debugMenu.add(jSeparator14);

        exportLargeLandformsMenuItem.setText("Export Large Landforms Diffuse Shading…");
        exportLargeLandformsMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportLargeLandformsMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportLargeLandformsMenuItem);
        debugMenu.add(jSeparator17);

        exportSurfer6MenuItem.setText("Export to Surfer 6 Binary File…");
        exportSurfer6MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportSurfer6MenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(exportSurfer6MenuItem);
        debugMenu.add(jSeparator21);

        timeDepressionFillerMenuItem.setText("Time Depression Filler…");
        timeDepressionFillerMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                timeDepressionFillerMenuItemActionPerformed(evt);
            }
        });
        debugMenu.add(timeDepressionFillerMenuItem);

        timeDepressionFillerMenuItem1.setText("Time Gauss Filter");
        timeDepressionFillerMenuItem1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                timeDepressionFillerMenuItem1ActionPerformed(evt);
            }
        });
        debugMenu.add(timeDepressionFillerMenuItem1);

        menuBar.add(debugMenu);

        sphericalShadingMenu.setText("Spherical Shading");
        sphericalShadingMenu.addMenuListener(new javax.swing.event.MenuListener() {
            public void menuSelected(javax.swing.event.MenuEvent evt) {
                sphericalShadingMenuMenuSelected(evt);
            }
            public void menuDeselected(javax.swing.event.MenuEvent evt) {
            }
            public void menuCanceled(javax.swing.event.MenuEvent evt) {
            }
        });

        sphericalImage1MenuItem.setText("Select Spherical Shading Image for Low Areas…");
        sphericalImage1MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                sphericalImage1MenuItemActionPerformed(evt);
            }
        });
        sphericalShadingMenu.add(sphericalImage1MenuItem);

        sphericalImage1InfoMenuItem.setText("Path to Image for Low Areas");
        sphericalImage1InfoMenuItem.setEnabled(false);
        sphericalShadingMenu.add(sphericalImage1InfoMenuItem);
        sphericalShadingMenu.add(jSeparator19);

        sphericalImage2MenuItem.setText("Select Spherical Shading Image for High Areas…");
        sphericalImage2MenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                sphericalImage2MenuItemActionPerformed(evt);
            }
        });
        sphericalShadingMenu.add(sphericalImage2MenuItem);

        sphericalImage2InfoMenuItem.setText("Path to Image for High Areas");
        sphericalImage2InfoMenuItem.setEnabled(false);
        sphericalShadingMenu.add(sphericalImage2InfoMenuItem);
        sphericalShadingMenu.add(jSeparator18);

        sphericalImagesReloadMenuItem.setText("Reload Spherical Shading Images and Render");
        sphericalImagesReloadMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                sphericalImagesReloadMenuItemActionPerformed(evt);
            }
        });
        sphericalShadingMenu.add(sphericalImagesReloadMenuItem);

        menuBar.add(sphericalShadingMenu);

        setJMenuBar(menuBar);

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

    public void positionInfoDialog(int left, int bottom) {
        locationInfoDialog.pack();
        locationInfoDialog.setLocation(left + 5, bottom - locationInfoDialog.getHeight() - 5);
        locationInfoDialog.setVisible(true);
    }

    /**
     * Hide the debugMenu if assertions are not enabled.
     */
    private void showDebugMenuWhenAssertionsAreEnabled() {
        boolean assertsEnabled = false;
        assert assertsEnabled = true; // intentional side effect
        if (!assertsEnabled) {
            menuBar.remove(debugMenu);
        }
    }

    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_DIALOG_TITLE, ex, null);
                    }
                }
            }

            @Override
            protected Icon getFileIcon(File file) {
                return null;
            }
        };
        rdm.setMaxRecentDocuments(25);
    }

    private void saveBackgroundImage(String format, String message) {
        String filePath = FileUtils.askFile(this, message, fileName(format),
                false, format, null);
        if (filePath != null) {
            FileRenderer fileRenderer = new FileRenderer(model, filePath,
                    format, this, "Saving Visualization");
            gridWorkerManager.start(fileRenderer, true, true);
        }
    }
    private void saveTIFFImageMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveTIFFImageMenuItemActionPerformed
        saveBackgroundImage("tif", "Save TIFF Image");
    }//GEN-LAST:event_saveTIFFImageMenuItemActionPerformed

    private void saveFilteredGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveFilteredGridMenuItemActionPerformed
        saveGrid(model.getFilteredGrid(), "Save Filtered Grid");
    }//GEN-LAST:event_saveFilteredGridMenuItemActionPerformed

    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 gridInfoMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_gridInfoMenuItemActionPerformed
        StringBuilder sb = new StringBuilder();
        sb.append("<html> <b>");
        sb.append(getTitle());
        sb.append("</b><br>");
        sb.append(model.getGrid().getDescriptionWithStatistics("<br>"));
        String title = "Grid Info";
        JOptionPane.showMessageDialog(this, sb.toString(), title, JOptionPane.PLAIN_MESSAGE);
    }//GEN-LAST:event_gridInfoMenuItemActionPerformed

    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
        gridInfoMenuItem.setEnabled(model.hasGrid());
        boolean settingsVisible = settingsDialog.isVisible();
        boolean locationInfoVisible = locationInfoDialog.isVisible();
        settingsMenuItem.setText(settingsVisible ? "Hide Settings" : "Show Settings");
        locationInfoMenuItem.setText(locationInfoVisible ? "Hide Location Info" : "Show Location Info");
    }//GEN-LAST:event_infoMenuMenuSelected

    private void zoomToFitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomToFitMenuItemActionPerformed
        navigableImagePanel.setZoomToFillComponent();
    }//GEN-LAST:event_zoomToFitMenuItemActionPerformed

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

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

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

    /**
     * Render an image and export it to a file, while a progress dialog with a
     * cancel button is displayed.
     *
     * @param filePath file to export to.
     * @param scale The final image will be this many times larger than the
     * grid.
     */
    private void exportContours(final String filePath, final String imageFormat, final int scale) {
        ContoursExporter contoursExporter = new ContoursExporter(model,
                filePath, imageFormat, this, "Contours Export",
                "Extracting contours", scale);
        gridWorkerManager.start(contoursExporter, true, true);
    }

    private void saveContours(String askFileMessage, String imageFormat) {
        String PREFS_NAME = "contours_scale";
        String filePath = FileUtils.askFile(this, askFileMessage,
                fileName(imageFormat), false, imageFormat, null);
        if (filePath != null) {
            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
        final boolean hasGrid = model.hasGrid();
        final boolean canStartWorker = gridWorkerManager.scheduledWorkersChangeModel();
        Model.ForegroundVisualization overlayVis = model.foregroundVisualization;
        boolean contoursVisible
                = overlayVis == Model.ForegroundVisualization.ILLUMINATED_CONTOURS
                || overlayVis == Model.ForegroundVisualization.SHADED_CONTOURS;
        boolean flatAreasMaskVisible
                = model.foregroundVisualization == Model.ForegroundVisualization.FLAT_AREAS_MASK;
        openMenuItem.setEnabled(canStartWorker);
        openRecentMenu.setEnabled(canStartWorker);
        saveGridMenuItem.setEnabled(hasGrid && canStartWorker);
        saveFilteredGridMenuItem.setEnabled(hasGrid && canStartWorker && model.isFilteringGrid());
        saveDownsampledMenu.setEnabled(hasGrid && canStartWorker);
        saveTIFFImageMenuItem.setEnabled(hasGrid && canStartWorker);
        savePNGImageMenuItem.setEnabled(hasGrid && canStartWorker);
        saveTIFFContoursMenuItem.setEnabled(contoursVisible && canStartWorker);
        savePNGContoursMenuItem.setEnabled(contoursVisible && canStartWorker);
        saveFlatAreasTIFFMenuItem.setEnabled(flatAreasMaskVisible && canStartWorker);
        saveFlatAreasPNGMenuItem.setEnabled(flatAreasMaskVisible && canStartWorker);
        saveTIFFNormalMapMenuItem.setEnabled(hasGrid && canStartWorker);
        savePNGNormalMapMenuItem.setEnabled(hasGrid && canStartWorker);
    }//GEN-LAST:event_fileMenuMenuSelected

    private void viewMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_viewMenuMenuSelected
        boolean enable = model != null && model.hasGrid();
        for (int i = 0; i < viewMenu.getMenuComponentCount(); i++) {
            Component menuComponent = viewMenu.getMenuComponent(i);
            if (menuComponent instanceof JMenuItem) {
                menuComponent.setEnabled(enable);
            }
        }
    }//GEN-LAST:event_viewMenuMenuSelected

    private void executeOperatorInWorker(ThreadedGridOperator op, String message) {
        final OperatorWorker operatorWorker = new OperatorWorker(progressPanel,
                model.getGrid(), op);
        operatorWorker.resetProgressGUI(true);
        operatorWorker.setMessage(message);
        operatorWorker.addPropertyChangeListener(
                new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent event) {
                if ("state".equals(event.getPropertyName())
                        && SwingWorker.StateValue.DONE == event.getNewValue()) {
                    if (operatorWorker.isCancelled() == false) {
                        model.setGrid(operatorWorker.getResultGrid());
                        settingsDialog.gridChanged();
                    }
                }
            }
        });
        gridWorkerManager.start(operatorWorker, true, true);
    }

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

        try {
            scaleGridFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (scaleGridFormattedTextField.getValue());
            float scale = f.floatValue();
            executeOperatorInWorker(new ScaleOperator(scale), "Scaling grid values\u2026");
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while scaling the grid.",
                    ERROR_DIALOG_TITLE, exc, this);
        }
    }//GEN-LAST:event_scaleGridMenuItemActionPerformed

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

        try {
            offsetGridFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (offsetGridFormattedTextField.getValue());
            float offset = f.floatValue();
            executeOperatorInWorker(new AddOperator(offset), "Vertically offsetting grid values\u2026");
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the grid.",
                    ERROR_DIALOG_TITLE, exc, this);
        }
    }//GEN-LAST:event_verticalOffsetGridMenuItemActionPerformed

    private void editMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_editMenuMenuSelected
        boolean enable = model != null && model.hasGrid() && gridWorkerManager.scheduledWorkersChangeModel();
        for (int i = 0; i < editMenu.getMenuComponentCount(); i++) {
            Component menuComponent = editMenu.getMenuComponent(i);
            if (menuComponent instanceof JMenuItem) {
                menuComponent.setEnabled(enable);
            }
        }

        applyFilterMenuItem.setEnabled(enable && model.isFilteringGrid());
    }//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 = FileUtils.askFile(this, "Select a Line Shapefile", true);
            if (filePath == null) {
                // user cancelled
                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.getFilteredGrid().getBoundingBox();
            if (GeometryUtils.rectanglesIntersect(gridBB, vectorsBB) == false) {
                String msg = "<html>The shapefile geometry does not align with "
                        + "the grid.<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 cancelled
                return;
            }

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

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

            // ask the user for a file to save
            filePath = FileUtils.askFile(this, "Save Plan Oblique Lines", false);
            if (filePath == null) {
                // user cancelled
                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) {
            return;
        }

        final int nbrLaplacianBands = GaussianPyramid.getPyramidLevelsForGrid(model.getGrid());
        int[][] dimensions = GaussianPyramid.getPyramidSizeForGrid(model.getGrid());
        for (int i = 1; i < nbrLaplacianBands; i++) { // don't include level 0, which is the full resolution grid
            int cols = dimensions[i][0];
            int rows = dimensions[i][1];
            JMenuItem menuItem = new JMenuItem(cols + "\u00D7" /* multiplication sign */ + rows);
            menuItem.setName(Integer.toString(i));
            menuItem.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    try {
                        // weights for Laplacian pyramid levels
                        int pyramidLevel = Integer.parseInt(((Component) e.getSource()).getName());
                        float[] weights = new float[nbrLaplacianBands];
                        Arrays.fill(weights, 1f);
                        Arrays.fill(weights, 0, pyramidLevel, 0f);
                        Grid grid = model.laplacianSum(weights, false, null); // FIXME use OperatorWorker to pass progress indicator
                        saveGrid(grid, "Save Downsampled Grid");
                    } catch (Throwable exc) {
                        ErrorDialog.showErrorDialog("Could not save downsampled grid.", ERROR_DIALOG_TITLE, exc, navigableImagePanel);
                    }
                }
            });
            saveDownsampledMenu.add(menuItem);
        }
    }//GEN-LAST:event_saveDownsampledMenuMenuSelected

    private void saveNormalMap(String format) {
        String filePath = FileUtils.askFile(this, "Normal Map",
                fileName(format), false, format, null);
        if (filePath != null) {
            NormalMapExporter exporter = new NormalMapExporter(model, filePath,
                    format, this, "Normal Map");
            gridWorkerManager.start(exporter, true, true);
        }
    }

    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());
            executeOperatorInWorker(new VoidOperator(f.floatValue()), "Void grid values\u2026");
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the grid.",
                    ERROR_DIALOG_TITLE, exc, this);
        }
    }//GEN-LAST:event_voidGridMenuItemActionPerformed

    private void saveGridMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveGridMenuItemActionPerformed
        saveGrid(model.getGrid(), "Save Grid");
    }//GEN-LAST:event_saveGridMenuItemActionPerformed

    private void savePlanObliqueMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_savePlanObliqueMenuItemActionPerformed
        Grid grid = model.getGrid();
        if (model.planObliqueAngle != 90) {
            float min = grid.getMinMax()[0];
            PlanObliqueOperator planObliqueOp = new PlanObliqueOperator(model.planObliqueAngle, min);
            grid = planObliqueOp.operate(grid);
        }
        saveGrid(grid, "Save Plan Oblique Grid");
    }//GEN-LAST:event_savePlanObliqueMenuItemActionPerformed

    private void setWindowTitleMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_setWindowTitleMenuItemActionPerformed
        String title = JOptionPane.showInputDialog("Window title", getTitle());
        if (title != null) {
            setTitle(title);
        }
    }//GEN-LAST:event_setWindowTitleMenuItemActionPerformed

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

        try {
            replaceVoidGridFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (replaceVoidGridFormattedTextField.getValue());
            executeOperatorInWorker(new ReplaceVoidOperator(f.floatValue()), "Replace void grid values");
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the grid.",
                    ERROR_DIALOG_TITLE, exc, this);
        }
    }//GEN-LAST:event_replaceVoidGridValuesMenuItemActionPerformed

    private void prjMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_prjMenuItemActionPerformed
        if (model.hasGrid()) {
            JOptionPane.showMessageDialog(this, model.getGrid().getPrjFileContent(), ".prj File Content", JOptionPane.PLAIN_MESSAGE);
        }
    }//GEN-LAST:event_prjMenuItemActionPerformed

    private void saveFlatAreasMask(String fileFormat) {
        String filePath = FileUtils.askFile(this, "Save Flat Areas Mask",
                fileName(fileFormat), false, fileFormat, null);
        if (filePath != null) {
            String msg = "Saving Flat Areas Mask";
            FlatAreasMaskExporter exporter = new FlatAreasMaskExporter(
                    model, filePath, fileFormat, this, "", msg);
            gridWorkerManager.start(exporter, true, true);
        }
    }

    private void saveFlatAreasTIFFMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveFlatAreasTIFFMenuItemActionPerformed
        saveFlatAreasMask("tif");
    }//GEN-LAST:event_saveFlatAreasTIFFMenuItemActionPerformed

    private void saveFlatAreasPNGMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveFlatAreasPNGMenuItemActionPerformed
        saveFlatAreasMask("png");
    }//GEN-LAST:event_saveFlatAreasPNGMenuItemActionPerformed

    private void zoomToFitMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomToFitMenuItem1ActionPerformed
        navigableImagePanel.zoomTo100PercentAndCenter();
    }//GEN-LAST:event_zoomToFitMenuItem1ActionPerformed

    private void addRandomMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addRandomMenuItemActionPerformed
        int option = JOptionPane.showOptionDialog(this,
                addRandomPanel,
                "Add Random Noise",
                JOptionPane.OK_CANCEL_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null, null, null);
        if (option != JOptionPane.OK_OPTION) {
            return;
        }

        try {
            addRandomFormattedTextField.commitEdit();
            java.lang.Number f = (java.lang.Number) (addRandomFormattedTextField.getValue());
            executeOperatorInWorker(new AddRandomOperator(f.floatValue()), "Add random values");
        } catch (Exception exc) {
            ErrorDialog.showErrorDialog("An error occured while changing the grid.",
                    ERROR_DIALOG_TITLE, exc, this);
        }
    }//GEN-LAST:event_addRandomMenuItemActionPerformed

    private void stdoutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_stdoutMenuItemActionPerformed
        // redirect stdout and stderr to custom window
        RedirectedFrame consoleFrame = new RedirectedFrame(true, false, null, 600, 900, JFrame.DO_NOTHING_ON_CLOSE);
        consoleFrame.setVisible(true);
        stdoutMenuItem.setEnabled(false);
    }//GEN-LAST:event_stdoutMenuItemActionPerformed

    private void testMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_testMenuItemActionPerformed
//        Grid grid = new MultigridDiffusor().getGrid();
//        model.setGrid(grid);
        settingsDialog.gridChanged();
    }//GEN-LAST:event_testMenuItemActionPerformed

    private void exportMaxBranchLengthMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportMaxBranchLengthMenuItemActionPerformed

        //fill the depressions
        DepressionFillOperator depFillOp = new DepressionFillOperator();
        Grid depFillGrid = depFillOp.operate(model.getFilteredGrid());

        // find the D8-esque directions
        FlowDirectionOperator flowDirectionOp = new FlowDirectionOperator();
        Grid flowDirections = flowDirectionOp.operate(depFillGrid);

        // find the accumulation grid and update the directions
        FlowAccumulationOperator flowAccumOp = new FlowAccumulationOperator(flowDirections);
        Grid flowAccumulationGrid = flowAccumOp.operate(depFillGrid);

        // find the max branch lengths
        MaximumBranchLengthsOperator mbl = new MaximumBranchLengthsOperator(new HashMap(), null, null);
        Grid maxBranchLenghtsGrid = mbl.operate(flowDirections);

        String filePath = FileUtils.askFile(this, "Maximum Branch Lengths", false);
        if (filePath != null) {
            try {
                EsriASCIIGridExporter.export(maxBranchLenghtsGrid, filePath, null);
                System.out.println(maxBranchLenghtsGrid.getDescriptionWithStatistics("\n"));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
                ErrorDialog.showErrorDialog("Could not export maximum branch length grid.", ERROR_DIALOG_TITLE, ex, this);
            }
        }

        // find threshold value for converting maxBranchLenghtsGrid to binary grid
        MinMaxOperator minMaxOp = new MinMaxOperator();
        MinMax minMax = minMaxOp.findMinMax(maxBranchLenghtsGrid);
        double threshold = model.localIllumination.getMaxBranchLengthThreshold() * minMax.range + minMax.min;

        // convert to binary grid using threshold
        ScaleTo255Operator thresholder = new ScaleTo255Operator(minMax.max, (float) threshold);
        Grid thresholded = thresholder.operate(maxBranchLenghtsGrid);

        filePath = FileUtils.askFile(this, "Binary Grid", false);
        if (filePath != null) {
            try {
                EsriASCIIGridExporter.export(thresholded, filePath, null);
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
                ErrorDialog.showErrorDialog("Error", ERROR_DIALOG_TITLE, ex, this);
            }
        }

        // create skeleton lines (lines with mininmum width)
        Grid skeleton = thresholded;
        new WeightedSkeletonizerOperator().operate(skeleton);

        BinaryThresholdOperator biThresholder = new BinaryThresholdOperator(1);
        skeleton = biThresholder.operate(skeleton);

        filePath = FileUtils.askFile(this, "Skeleton Lines", false);
        if (filePath != null) {
            try {
                EsriASCIIGridExporter.export(skeleton, filePath, null);
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
                ErrorDialog.showErrorDialog("Error", ERROR_DIALOG_TITLE, ex, this);
            }
        }
    }//GEN-LAST:event_exportMaxBranchLengthMenuItemActionPerformed

    private void exportLines(GeometryCollection geometryCollection, String outputFilePath) {
        try {
            ShapeExporter geometryExporter = new ShapeExporter();
            geometryExporter.export(geometryCollection, outputFilePath);
            geometryExporter.exportTableForGeometry(outputFilePath, geometryCollection, null);
        } catch (IOException ex) {
            ErrorDialog.showErrorDialog("Error", "Shapefile export failed: ", ex, null);
        }
    }

    private void exportRidgeLinesMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportRidgeLinesMenuItemActionPerformed
        String outputFilePath = FileUtils.askFile(this, "Export Ridgelines", "ridegelines.shp", false, "shp", null);
        if (outputFilePath != null) {
            exportLines(model.ridgeLinesForLightDirections, outputFilePath);
        }
    }//GEN-LAST:event_exportRidgeLinesMenuItemActionPerformed

    private void exportSimplifiedRidgeLinesMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSimplifiedRidgeLinesMenuItemActionPerformed
        String outputFilePath = FileUtils.askFile(this, "Export Simplified Ridgelines", "ridegelines.shp", false, "shp", null);
        if (outputFilePath != null) {
            exportLines(model.simplifiedRidgeLinesForLightDirections, outputFilePath);
        }
    }//GEN-LAST:event_exportSimplifiedRidgeLinesMenuItemActionPerformed

    private void exportDepressionsFilledMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportDepressionsFilledMenuItemActionPerformed
        String filePath = FileUtils.askFile(this, "Depressions Filled", false);
        if (filePath != null) {
            try {
                DepressionFillOperator depFillOp = new DepressionFillOperator();
                Grid depFillGrid = depFillOp.operate(model.getFilteredGrid());
                EsriASCIIGridExporter.export(depFillGrid, filePath, null);
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
                ErrorDialog.showErrorDialog("Error", ERROR_DIALOG_TITLE, ex, this);
            }
        }
    }//GEN-LAST:event_exportDepressionsFilledMenuItemActionPerformed

    private void exportAccumulationFlowMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportAccumulationFlowMenuItemActionPerformed
        String filePath = FileUtils.askFile(this, "Depressions Filled", false);
        if (filePath != null) {
            try {
                // fill depressions
                DepressionFillOperator depFillOp = new DepressionFillOperator();
                Grid depFillGrid = depFillOp.operate(model.getFilteredGrid());

                // find the D8 directions for non-flat areas
                FlowDirectionOperator flowDirectionOp = new FlowDirectionOperator();
                Grid flowDirections = flowDirectionOp.operate(depFillGrid);

                // find the flow accumulation grid and update D8 directions in flat areas
                FlowAccumulationOperator flowAccumOp = new FlowAccumulationOperator(flowDirections);
                Grid flowAccumulationGrid = flowAccumOp.operate(depFillGrid);

                EsriASCIIGridExporter.export(flowAccumulationGrid, filePath, null);
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
                ErrorDialog.showErrorDialog("Error", ERROR_DIALOG_TITLE, ex, this);
            }
        }
    }//GEN-LAST:event_exportAccumulationFlowMenuItemActionPerformed

    private void locationInfoMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_locationInfoMenuItemActionPerformed
        locationInfoDialog.setVisible(!locationInfoDialog.isVisible());
    }//GEN-LAST:event_locationInfoMenuItemActionPerformed

    private void exportReflectanceMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportReflectanceMapMenuItemActionPerformed
        ReflectanceMapper reflectanceMap = new ReflectanceMapper(model.getAzimuth(), model.zenith);
        Grid reflectanceMapGrid = reflectanceMap.getDiffuseReflectanceMap();

        // export reflectance map
        String fileName = reflectanceMap.getFileName() + ".png";
        BufferedImage image = GridToImageOperator.convert(reflectanceMapGrid, 0, 1);
        String filePath = FileUtils.askFile(this, "Reflectance Map Image", fileName, false, "png", null);
        if (filePath != null) {
            try {
                ImageIO.write(image, "png", new File(filePath));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_exportReflectanceMapMenuItemActionPerformed

    private void applyReflectanceMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_applyReflectanceMapMenuItemActionPerformed
        try {
            Grid grid = model.getFilteredGrid();
            if (grid == null) {
                return;
            }

            // load reflectance map
            String filePath = FileUtils.askFile(this, "Reflectance Map", "Reflectance map.png", true, "png", null);
            if (filePath == null) {
                return;
            }
            BufferedImage reflectanceMap = ImageIO.read(new File(filePath));

            // apply reflectance map to grid
            BufferedImage out = ReflectanceMapper.applyReflectanceMap(reflectanceMap, grid);

            // export shading
            filePath = FileUtils.askFile(this, "Reflectance Map Shading", "Shading.png", false, "png", null);
            if (filePath != null) {
                ImageIO.write(out, "png", new File(filePath));
            }
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_applyReflectanceMapMenuItemActionPerformed

    private void varianceMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_varianceMenuItemActionPerformed
        String filePath = FileUtils.askFile(this, "Esri ASCII Grid", true);
        if (filePath != null) {
            try {
                Grid grid = EsriASCIIGridImporter.read(filePath);
                String str = JOptionPane.showInputDialog("Filter Size (odd number)", 3);
                int filterSize = Integer.parseInt(str);
                Grid varianceGrid = new StandardDeviationOperator(filterSize, false).operate(grid);
                filePath = FileUtils.askFile(this, "Standard Deviation Grid", false);
                if (filePath != null) {
                    EsriASCIIGridExporter.export(varianceGrid, filePath, null);
                }
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_varianceMenuItemActionPerformed

    private void exportAngleMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportAngleMapMenuItemActionPerformed
        ReflectanceMapper reflectanceMap = new ReflectanceMapper(model.getAzimuth(), model.zenith);
        Grid angleMap = reflectanceMap.getReflectanceAngleGrid();

        String fileName = "Angle " + reflectanceMap.getFileName();
        // export image
        BufferedImage image = GridToImageOperator.convert(angleMap, 0, 180);
        String filePath = FileUtils.askFile(this, "Angle Map Image", fileName + ".png", false, "png", null);
        if (filePath != null) {
            try {
                ImageIO.write(image, "png", new File(filePath));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        // export grid
        try {
            filePath = FileUtils.askFile(this, "Angle Map Grid", fileName + ".asc", false, "asc", null);
            EsriASCIIGridExporter.export(angleMap, filePath, null);
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_exportAngleMapMenuItemActionPerformed

    private void exportReflectanceMapWithFlatToneMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportReflectanceMapWithFlatToneMenuItemActionPerformed
        ReflectanceMapper reflectanceMap = new ReflectanceMapper(model.getAzimuth(), model.zenith);
        Grid reflectanceMapGrid = reflectanceMap.getReflectanceMap(0.7f, 3, 12);

        String fileName = "Slope " + reflectanceMap.getFileName() + ".png";
        BufferedImage image = GridToImageOperator.convert(reflectanceMapGrid, -1, +1);
        String filePath = FileUtils.askFile(this, "Reflectance Map Image", fileName, false, "png", null);
        if (filePath != null) {
            try {
                ImageIO.write(image, "png", new File(filePath));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_exportReflectanceMapWithFlatToneMenuItemActionPerformed

    private void exportSlopeMaskMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSlopeMaskMenuItemActionPerformed
        float slope1Deg = 3;
        float slope2Deg = 12;
        Grid slopeGrid = new SlopeOperator().operate(model.getFilteredGrid());
        int nCols = slopeGrid.getCols();
        int nRows = slopeGrid.getRows();
        for (int row = 0; row < nRows; row++) {
            for (int col = 0; col < nCols; col++) {
                double v;
                double slope = Math.toDegrees(slopeGrid.getValue(col, row));
                if (slope > slope2Deg) {
                    v = 0;
                } else if (slope <= slope1Deg) {
                    v = 1;
                } else {
                    double w = (slope - slope1Deg) / (1d + slope2Deg - slope1Deg);
                    v = (1 - w);
                }
                slopeGrid.setValue(v, col, row);
            }
        }

        String fileName = "Slope mask.png";
        BufferedImage image = GridToImageOperator.convert(slopeGrid, 0, 1);
        String filePath = FileUtils.askFile(this, "Slope Mask", fileName, false, "png", null);
        if (filePath != null) {
            try {
                ImageIO.write(image, "png", new File(filePath));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_exportSlopeMaskMenuItemActionPerformed

    private void exportLargeLandformsMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportLargeLandformsMenuItemActionPerformed
        if (model.simplifiedRidgeLinesForLargeForms == null
                || model.simplifiedValleyLinesForLargeForms == null) {
            ErrorDialog.showErrorDialog("Apply large forms correction first.", this);
            return;
        }

        Grid diffuseShading = model.largeLandformsShading(
                model.simplifiedRidgeLinesForLargeForms,
                model.simplifiedValleyLinesForLargeForms,
                null);

        String filePath = FileUtils.askFile(this, "Large Landforms", fileName("asc"),
                false, "asc", GRID_FILE_FILTER);
        if (filePath != null) {
            try {
                EsriASCIIGridExporter.export(diffuseShading, filePath, null);
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_exportLargeLandformsMenuItemActionPerformed

    private void exportValleyLinesMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportValleyLinesMenuItemActionPerformed
        String outputFilePath = FileUtils.askFile(this, "Export Valley Lines", "valleylines.shp", false, "shp", null);
        if (outputFilePath != null) {
            exportLines(model.valleyLinesForLightDirections, outputFilePath);
        }
    }//GEN-LAST:event_exportValleyLinesMenuItemActionPerformed

    private void exportSimplifiedValleyLinesMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSimplifiedValleyLinesMenuItemActionPerformed
        String outputFilePath = FileUtils.askFile(this, "Export Simplified Valley Lines", "simplified_valleylines.shp", false, "shp", null);
        if (outputFilePath != null) {
            exportLines(model.simplifiedValleyLinesForLightDirections, outputFilePath);
        }
    }//GEN-LAST:event_exportSimplifiedValleyLinesMenuItemActionPerformed

    private void minimizeMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_minimizeMenuItemActionPerformed
        setExtendedState(ICONIFIED);
    }//GEN-LAST:event_minimizeMenuItemActionPerformed

    private void zoomMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomMenuItemActionPerformed
        if (getExtendedState() != 6) {
            setExtendedState(Frame.MAXIMIZED_BOTH);
        } else {
            setExtendedState(Frame.NORMAL);
        }
    }//GEN-LAST:event_zoomMenuItemActionPerformed

    private void exportCorrectedReflectanceMapMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportCorrectedReflectanceMapMenuItemActionPerformed
        ReflectanceMapper reflectanceMap = new ReflectanceMapper(model.getAzimuth(), model.zenith);
        Grid reflectanceMapGrid = reflectanceMap.getCorrectedReflectanceMap();

        // export reflectance map
        String fileName = "Corrected " + reflectanceMap.getFileName() + ".png";
        BufferedImage image = GridToImageOperator.convert(reflectanceMapGrid, -1, +1);
        String filePath = FileUtils.askFile(this, "Corrected Reflectance Map Image", fileName, false, "png", null);
        if (filePath != null) {
            try {
                ImageIO.write(image, "png", new File(filePath));
            } catch (IOException ex) {
                Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }//GEN-LAST:event_exportCorrectedReflectanceMapMenuItemActionPerformed

    private void exportSurfer6MenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSurfer6MenuItemActionPerformed
        if (model.getGrid() == null) {
            return;
        }

        String filePath = FileUtils.askFile(this, "Export to Surfer 6 Binary File",
                fileName("grd"), false, "grd", null);
        if (filePath != null) {

//            SurferBinary6GridExporter gridExporter = new SurferBinary6GridExporter();
//            SwingWorkerWithProgressIndicatorDialog exportWorker
//                    = new SwingWorkerWithProgressIndicatorDialog<Void>(this,
//                            progressPanel,
//                            "Exporting to Surfer 6 Binary File") {
//                @Override
//                protected Void doInBackground() throws Exception {
//                    //gridExporter.setProgressIndicator(exportWorker);
//                    //gridExporter.exportToFile(model.getGrid(), filePath);
//                    
//                    return null;
//                }
//
//                @Override
//                public String getProgressMessage() {
//                    return "Exporting to \u2026";
//                }
//            };
            //gridWorkerManager.start(exporter, true, true);
        }
    }//GEN-LAST:event_exportSurfer6MenuItemActionPerformed

    private void sphericalImage2MenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sphericalImage2MenuItemActionPerformed
        try {
            dev.Dev.lookUpImageFilePath2 = FileUtils.askFile(null, "Spherical Image for High Areas", true);
            BufferedImage img = ImageIO.read(new File(dev.Dev.lookUpImageFilePath2));
            SphereLookupShader.setCircularLookupImageHigh(img);
            settingsDialog.gridChanged();
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_sphericalImage2MenuItemActionPerformed

    private void sphericalImagesReloadMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sphericalImagesReloadMenuItemActionPerformed
        try {
            BufferedImage img1 = ImageIO.read(new File(dev.Dev.lookUpImageFilePath1));
            SphereLookupShader.setCircularLookupImageHigh(img1);
            BufferedImage img2 = ImageIO.read(new File(dev.Dev.lookUpImageFilePath2));
            SphereLookupShader.setCircularLookupImageLow(img2);
            settingsDialog.gridChanged();
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_sphericalImagesReloadMenuItemActionPerformed

    private void sphericalImage1MenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sphericalImage1MenuItemActionPerformed
        try {
            dev.Dev.lookUpImageFilePath1 = FileUtils.askFile(null, "Spherical Image for Low Areas", true);
            BufferedImage img = ImageIO.read(new File(dev.Dev.lookUpImageFilePath1));
            SphereLookupShader.setCircularLookupImageLow(img);
            settingsDialog.gridChanged();
        } catch (IOException ex) {
            Logger.getLogger(MainWindow.class.getName()).log(Level.SEVERE, null, ex);
        }
    }//GEN-LAST:event_sphericalImage1MenuItemActionPerformed

    private void sphericalShadingMenuMenuSelected(javax.swing.event.MenuEvent evt) {//GEN-FIRST:event_sphericalShadingMenuMenuSelected
        String str1 = (dev.Dev.lookUpImageFilePath1 != null) ? dev.Dev.lookUpImageFilePath1 : "-";
        String str2 = (dev.Dev.lookUpImageFilePath2 != null) ? dev.Dev.lookUpImageFilePath2 : "-";
        sphericalImage1InfoMenuItem.setText(str1);
        sphericalImage2InfoMenuItem.setText(str2);
    }//GEN-LAST:event_sphericalShadingMenuMenuSelected

    private void applyFilterMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_applyFilterMenuItemActionPerformed
        Grid filteredGrid = model.getFilteredGrid();
        model.filterManager.clear();
        model.setGrid(filteredGrid);
        settingsDialog.updateGUI();
        settingsDialog.gridChanged();
    }//GEN-LAST:event_applyFilterMenuItemActionPerformed

    private void timeDepressionFillerMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeDepressionFillerMenuItemActionPerformed
        DepressionFillOperator depFillOp = new DepressionFillOperator();
        for (int i = 0; i < 10; i++) {
            System.out.println(i + 1 + " of 10");
            depFillOp.operate(model.getFilteredGrid());
        }
    }//GEN-LAST:event_timeDepressionFillerMenuItemActionPerformed

    private void timeDepressionFillerMenuItem1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_timeDepressionFillerMenuItem1ActionPerformed
//        double std = 0.6;
//        GaussLowPassOperator gaussLowPassOperator = new GaussLowPassOperator(std);
//        for (int i = 0; i < 10; i++) {
//            System.out.println(i + 1 + " of 10");
//            gaussLowPassOperator.operate(model.getGrid());
//        }
//        
//        Dev.Gauss3x3Operator op = new Dev.Gauss3x3Operator();
//        for (int i = 0; i < 10; i++) {
//            System.out.println(i + 1 + " of 10");
//            op.operate(model.getGrid());
//        }

        int nbrRuns = 20;
        float sigma = 20;
        int maxNbrThreads = Runtime.getRuntime().availableProcessors() / 2;
        int maxCachedRows = 8;

        for (int dim = 1024; dim <= 8192 * 2; dim *= 2) {
            System.out.println("\n" + dim + "x" + dim);
            for (int nbrThreads = maxNbrThreads; nbrThreads <= maxNbrThreads; nbrThreads++) {
               //ThreadedGridOperator.resetThreadPool(nbrThreads);
                System.out.print(nbrThreads + " threads: ");

                //for (int cachedRows = 1; cachedRows <= maxCachedRows; cachedRows++) {
                    Grid grid = new Grid(dim, dim);
                    new AddRandomOperator(100).operate(grid, grid);
                    LowPassOperator op = new LowPassOperator(sigma);
                    //op.nbrChachedRows = cachedRows;
                    long time = 0;

                    for (int i = 0; i < nbrRuns; i++) {
                        //System.out.print(i +" ");
                        long start = System.currentTimeMillis();
                        op.operate(grid);
                        long end = System.currentTimeMillis();
                        time += end - start;
                    }

                    System.out.print(Math.round(((float) time) / nbrRuns) + "ms" /*+ "(" + cachedRows + "r)"*/ +"\t");
                //}
                System.out.println();
            }

        }

    }//GEN-LAST:event_timeDepressionFillerMenuItem1ActionPerformed

    /**
     * Initialize the image used by the NavigableImagePanel.
     */
    public void initDisplayImage() {
        int imageScaleFactor = 1;
        BufferedImage image = model.createDestinationImage(imageScaleFactor);
        if (image != null) {
            navigableImagePanel.setImage(image);
            navigableImagePanel.setPixelScale(1d / imageScaleFactor);
            navigableImagePanel.autoZoom();
        }
    }

    /**
     * A grid has been opened and is ready to replace the current grid. This
     * method is called by the GridImportWorker after importing a grid.
     *
     * @param grid the new grid to replace the old grid
     * @param filePath path to the file that was read
     */
    public void openGrid(Grid grid, String filePath) {
        try {
            model.setGrid(grid);

            // 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);

            initDisplayImage();
            settingsDialog.gridChanged();
        } catch (Throwable e) {
            // erase the image 
            // FIXME use this technique elsewhere when an exception occurs
            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 reading a file.";
            ErrorDialog.showErrorDialog(msg, ERROR_DIALOG_TITLE, e, getContentPane());
        }
    }

    /**
     * Open a grid file and replace the current grid.
     *
     * @param filePath The file to open
     */
    private void openGrid(String filePath) {
        String dialogTitle = ApplicationInfo.getApplicationName();
        GridImportWorker worker = new GridImportWorker(this, filePath, dialogTitle);
        worker.resetProgressGUI(true);
        gridWorkerManager.start(worker, true, true);
    }

    /**
     * Ask user for grid file, open the file, and initialized data model.
     */
    public void openGrid() {
        // ask the user for a file
        String filePath = FileUtils.askFile(this, "Select a Grid File (Esri ASCII or Surfer 6)",
                null, true, null, GRID_FILE_FILTER);
        if (filePath != null) {
            openGrid(filePath);
        }
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JFormattedTextField addRandomFormattedTextField;
    private javax.swing.JMenuItem addRandomMenuItem;
    private javax.swing.JPanel addRandomPanel;
    private javax.swing.JMenuItem applyFilterMenuItem;
    private javax.swing.JMenuItem applyReflectanceMapMenuItem;
    private javax.swing.JMenu debugMenu;
    private javax.swing.JMenu editMenu;
    private javax.swing.JMenuItem exportAccumulationFlowMenuItem;
    private javax.swing.JMenuItem exportAngleMapMenuItem;
    private javax.swing.JMenuItem exportCorrectedReflectanceMapMenuItem;
    private javax.swing.JMenuItem exportDepressionsFilledMenuItem;
    private javax.swing.JMenuItem exportLargeLandformsMenuItem;
    private javax.swing.JMenuItem exportMaxBranchLengthMenuItem;
    private javax.swing.JMenuItem exportReflectanceMapMenuItem;
    private javax.swing.JMenuItem exportReflectanceMapWithFlatToneMenuItem;
    private javax.swing.JMenuItem exportRidgeLinesMenuItem;
    private javax.swing.JMenuItem exportSimplifiedRidgeLinesMenuItem;
    private javax.swing.JMenuItem exportSimplifiedValleyLinesMenuItem;
    private javax.swing.JMenuItem exportSlopeMaskMenuItem;
    private javax.swing.JMenuItem exportSurfer6MenuItem;
    private javax.swing.JMenuItem exportValleyLinesMenuItem;
    private javax.swing.JMenuItem gridInfoMenuItem;
    private javax.swing.JPanel imageResolutionPanel;
    private javax.swing.JSpinner imageResolutionSpinner;
    private javax.swing.JMenuItem infoMenuItem;
    private javax.swing.JTextPane infoTextPane;
    private javax.swing.JPopupMenu.Separator jSeparator13;
    private javax.swing.JPopupMenu.Separator jSeparator14;
    private javax.swing.JPopupMenu.Separator jSeparator15;
    private javax.swing.JPopupMenu.Separator jSeparator17;
    private javax.swing.JPopupMenu.Separator jSeparator18;
    private javax.swing.JPopupMenu.Separator jSeparator19;
    private javax.swing.JPopupMenu.Separator jSeparator20;
    private javax.swing.JPopupMenu.Separator jSeparator21;
    private javax.swing.JPopupMenu.Separator jSeparator4;
    private javax.swing.JPopupMenu.Separator jSeparator9;
    private javax.swing.ButtonGroup lineIntegralConvolutionButtonGroup;
    private javax.swing.JDialog locationInfoDialog;
    private javax.swing.JMenuItem locationInfoMenuItem;
    private javax.swing.JMenuBar menuBar;
    private edu.oregonstate.cartography.gui.NavigableImagePanel navigableImagePanel;
    private javax.swing.JFormattedTextField offsetGridFormattedTextField;
    private javax.swing.JPanel offsetGridPanel;
    private javax.swing.JMenuItem openMenuItem;
    private javax.swing.JMenu openRecentMenu;
    private javax.swing.JMenuItem planObliqueFeaturesMenuItem;
    private javax.swing.JFormattedTextField replaceVoidGridFormattedTextField;
    private javax.swing.JPanel replaceVoidGridPanel;
    private javax.swing.JMenuItem replaceVoidGridValuesMenuItem;
    private javax.swing.JMenu saveDownsampledMenu;
    private javax.swing.JMenuItem saveFilteredGridMenuItem;
    private javax.swing.JMenuItem saveFlatAreasPNGMenuItem;
    private javax.swing.JMenuItem saveFlatAreasTIFFMenuItem;
    private javax.swing.JMenuItem saveGridMenuItem;
    private javax.swing.JMenuItem savePNGContoursMenuItem;
    private javax.swing.JMenuItem savePNGImageMenuItem;
    private javax.swing.JMenuItem savePNGNormalMapMenuItem;
    private javax.swing.JMenuItem savePlanObliqueMenuItem;
    private javax.swing.JMenuItem saveTIFFContoursMenuItem;
    private javax.swing.JMenuItem saveTIFFImageMenuItem;
    private javax.swing.JMenuItem saveTIFFNormalMapMenuItem;
    private javax.swing.JFormattedTextField scaleGridFormattedTextField;
    private javax.swing.JMenuItem scaleGridMenuItem;
    private javax.swing.JPanel scaleGridPanel;
    private javax.swing.JMenuItem settingsMenuItem;
    private javax.swing.JMenuItem sphericalImage1InfoMenuItem;
    private javax.swing.JMenuItem sphericalImage1MenuItem;
    private javax.swing.JMenuItem sphericalImage2InfoMenuItem;
    private javax.swing.JMenuItem sphericalImage2MenuItem;
    private javax.swing.JMenuItem sphericalImagesReloadMenuItem;
    private javax.swing.JMenu sphericalShadingMenu;
    private javax.swing.JMenuItem stdoutMenuItem;
    private javax.swing.JMenuItem testMenuItem;
    private javax.swing.JMenuItem timeDepressionFillerMenuItem;
    private javax.swing.JMenuItem timeDepressionFillerMenuItem1;
    private javax.swing.JMenuItem varianceMenuItem;
    private javax.swing.JMenuItem verticalOffsetGridMenuItem;
    private javax.swing.JMenu viewMenu;
    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;
    private javax.swing.JMenuItem zoomToFitMenuItem;
    private javax.swing.JMenuItem zoomToFitMenuItem1;
    // End of variables declaration//GEN-END:variables

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

    /**
     * Call repaint() of the NavigableImagePanel. Call this after the image
     * returned by getDisplayImage() has been changed.
     */
    public void repaintDisplayImage() {
        navigableImagePanel.repaint();
    }

    /**
     * Returns the image used by the NavigableImagePanel. Creates a new image if
     * none exists.
     *
     * @return the image used by the NavigableImagePanel
     */
    public BufferedImage getDisplayImage() {
        BufferedImage displayImage = navigableImagePanel.getImage();
        if (displayImage == null) {
            initDisplayImage();
        }
        return navigableImagePanel.getImage();
    }

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

        // get key events for zooming when settings dialog has the focus
        InputMap inputMap = settingsDialog.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = settingsDialog.getRootPane().getActionMap();
        GUIUtil.zoomMenuCommands(inputMap, actionMap, zoomInAction, zoomOutAction);

        settingsDialog.setGridWorkerManager(gridWorkerManager);
    }

    /**
     * Adds a GridExporter to a list if the exporter can export a specified
     * grid.
     *
     * @param exporters list of exporters
     * @param exporter exporter to add
     * @param grid grid that the exporter needs to be able to export
     */
    private void addExporterIfSupported(ArrayList<GridExporter> exporters,
            GridExporter exporter, Grid grid) {
        if (exporter.canExport(grid)) {
            exporters.add(exporter);
        }
    }

    /**
     * Ask the user to select a GridExporter.
     *
     * @param grid select a GridExporter for this Grid
     * @return a GridExporter or null.
     */
    private GridExporter askExporter(Grid grid) {
        ArrayList<GridExporter> exporters = new ArrayList<>();
        addExporterIfSupported(exporters, new EsriASCIIGridExporter(), grid);
        addExporterIfSupported(exporters, new GRASSASCIIGridExporter(), grid);
        addExporterIfSupported(exporters, new PGMGridExporter(), grid);
        addExporterIfSupported(exporters, new Photoshop16BitGridExporter(), grid);
        addExporterIfSupported(exporters, new SurferBinary6GridExporter(), grid);
        addExporterIfSupported(exporters, new SurferBinary7GridExporter(), grid);
        addExporterIfSupported(exporters, new SurferASCIIGridExporter(), grid);

        GridExporter exporter = (GridExporter) JOptionPane.showInputDialog(this,
                "Grid File Format",
                null,
                JOptionPane.PLAIN_MESSAGE,
                null,
                exporters.toArray(),
                null);

        if (exporter instanceof EsriASCIIGridExporter) {
            // ask for number of decimals
            Object[] nbrDecimalsOptions = {0, 1, 2, 3, 4, 5, 6, 7, 8};
            Integer nbrDecimals = (Integer) JOptionPane.showInputDialog(null,
                    "Select number of decimals:", "Decimal Precision",
                    JOptionPane.PLAIN_MESSAGE, null, nbrDecimalsOptions, nbrDecimalsOptions[1]);
            if (nbrDecimals == null) {
                return null;
            }
        }
        return exporter;
    }

    /**
     * Ask the user for a grid format and a file path, then write the grid to a
     * file.
     *
     * @param grid the grid to export
     * @param dialogTitle title for GUI windows
     */
    private void saveGrid(Grid grid, String dialogTitle) {
        GridExporter gridExporter = askExporter(grid);
        if (gridExporter == null) {
            return;
        }
        String fileExtension = gridExporter.getFileExtension();
        String filePath = FileUtils.askFile(this, dialogTitle,
                fileName(fileExtension), false, fileExtension, GRID_FILE_FILTER);
        if (filePath == null) {
            return;
        }
        GridExportWorker exportWorker = new GridExportWorker(
                grid, gridExporter, this, filePath, dialogTitle);
        exportWorker.resetProgressGUI(true);
        gridWorkerManager.start(exportWorker, true, true);
    }

    /**
     * Returns a name for a file with the name of the current grid and a
     * specified file extension.
     *
     * @param ext file extension
     * @return
     */
    private String fileName(String ext) {
        String fileName = getTitle(); // a hacky way to retrieve the file name
        if (fileName == null || fileName.trim().isEmpty()) {
            fileName = "Grid";
        }
        return FileUtils.replaceExtension(fileName, ext, 3);
    }

    public ProgressPanel getProgressPanel() {
        return progressPanel;
    }
}