--- /dev/null
+package tim.prune.save;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.io.File;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+
+import tim.prune.I18nManager;
+import tim.prune.config.Config;
+import tim.prune.data.Track;
+import tim.prune.gui.map.MapSource;
+import tim.prune.gui.map.MapSourceLibrary;
+import tim.prune.threedee.ImageDefinition;
+
+/**
+ * Dialog to let you choose the parameters for a base image
+ * (source and zoom) including preview
+ */
+public class BaseImageConfigDialog implements Runnable
+{
+ /** Parent to notify */
+ private BaseImageConsumer _parent = null;
+ /** Parent dialog for position */
+ private JDialog _parentDialog = null;
+ /** Track to use for preview image */
+ private Track _track = null;
+ /** Dialog to show */
+ private JDialog _dialog = null;
+ /** Checkbox for using an image or not */
+ private JCheckBox _useImageCheckbox = null;
+ /** Panel to hold the other controls */
+ private JPanel _mainPanel = null;
+ /** Dropdown for map source */
+ private JComboBox<String> _mapSourceDropdown = null;
+ /** Dropdown for zoom levels */
+ private JComboBox<String> _zoomDropdown = null;
+ /** Button to trigger a download of the missing map tiles */
+ private JButton _downloadTilesButton = null;
+ /** Progress bar for downloading additional tiles */
+ private JProgressBar _progressBar = null;
+ /** Label for number of tiles found */
+ private JLabel _tilesFoundLabel = null;
+ /** Label for image size in pixels */
+ private JLabel _imageSizeLabel = null;
+ /** Image preview panel */
+ private ImagePreviewPanel _previewPanel = null;
+ /** Grouter, used to avoid regenerating images */
+ private MapGrouter _grouter = new MapGrouter();
+ /** OK button, needs to be enabled/disabled */
+ private JButton _okButton = null;
+ /** Flag for rebuilding dialog, don't bother refreshing and recalculating */
+ private boolean _rebuilding = false;
+ /** Cached values to allow cancellation of dialog */
+ private ImageDefinition _imageDef = new ImageDefinition();
+
+
+ /**
+ * Constructor
+ * @param inParent parent object to notify on completion of dialog
+ * @param inParentDialog parent dialog
+ * @param inTrack track object
+ */
+ public BaseImageConfigDialog(BaseImageConsumer inParent, JDialog inParentDialog, Track inTrack)
+ {
+ _parent = inParent;
+ _parentDialog = inParentDialog;
+ _dialog = new JDialog(inParentDialog, I18nManager.getText("dialog.baseimage.title"), true);
+ _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+ _dialog.getContentPane().add(makeDialogComponents());
+ _dialog.pack();
+ _track = inTrack;
+ }
+
+ /**
+ * @param inDefinition image definition object from previous dialog
+ */
+ public void setImageDefinition(ImageDefinition inDefinition)
+ {
+ _imageDef = inDefinition;
+ if (_imageDef == null) {
+ _imageDef = new ImageDefinition();
+ }
+ }
+
+ /**
+ * Begin the function
+ */
+ public void begin()
+ {
+ initDialog();
+ _dialog.setLocationRelativeTo(_parentDialog);
+ _dialog.setVisible(true);
+ }
+
+ /**
+ * Begin the function with a default of using an image
+ */
+ public void beginWithImageYes()
+ {
+ initDialog();
+ _useImageCheckbox.setSelected(true);
+ refreshDialog();
+ _dialog.setVisible(true);
+ }
+
+ /**
+ * Initialise the dialog from the cached values
+ */
+ private void initDialog()
+ {
+ _rebuilding = true;
+ _useImageCheckbox.setSelected(_imageDef.getUseImage());
+ // Populate the dropdown of map sources from the library in case it has changed
+ _mapSourceDropdown.removeAllItems();
+ for (int i=0; i<MapSourceLibrary.getNumSources(); i++)
+ {
+ _mapSourceDropdown.addItem(MapSourceLibrary.getSource(i).getName());
+ }
+ int sourceIndex = _imageDef.getSourceIndex();
+ if (sourceIndex < 0 || sourceIndex >= _mapSourceDropdown.getItemCount()) {
+ sourceIndex = 0;
+ }
+ _mapSourceDropdown.setSelectedIndex(sourceIndex);
+
+ // Zoom level
+ int zoomLevel = _imageDef.getZoom();
+ if (_imageDef.getUseImage())
+ {
+ for (int i=0; i<_zoomDropdown.getItemCount(); i++)
+ {
+ String item = _zoomDropdown.getItemAt(i).toString();
+ try {
+ if (Integer.parseInt(item) == zoomLevel)
+ {
+ _zoomDropdown.setSelectedIndex(i);
+ break;
+ }
+ }
+ catch (NumberFormatException nfe) {}
+ }
+ }
+ _rebuilding = false;
+ refreshDialog();
+ }
+
+ /**
+ * Update the visibility of the controls, and update the zoom dropdown based on the selected map source
+ */
+ private void refreshDialog()
+ {
+ _mainPanel.setVisible(_useImageCheckbox.isSelected());
+ // Exit if we're in the middle of something
+ if (_rebuilding) {return;}
+ int currentZoom = 0;
+ try {
+ currentZoom = Integer.parseInt(_zoomDropdown.getSelectedItem().toString());
+ }
+ catch (Exception nfe) {}
+ // First time in, the dropdown might be empty but we still might have a zoom in the definition
+ if (_zoomDropdown.getItemCount() == 0) {
+ currentZoom = _imageDef.getZoom();
+ }
+ // Get the extent of the track so we can work out how big the images are going to be for each zoom level
+ final double xyExtent = Math.max(_track.getXRange().getRange(), _track.getYRange().getRange());
+ int zoomToSelect = -1;
+
+ _rebuilding = true;
+ _zoomDropdown.removeAllItems();
+ if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getItemCount() > 0)
+ {
+ int currentSource = _mapSourceDropdown.getSelectedIndex();
+ for (int i=5; i<18; i++)
+ {
+ // How many pixels does this give?
+ final int zoomFactor = 1 << i;
+ final int pixCount = (int) (xyExtent * zoomFactor * 256);
+ if (pixCount > 100 // less than this isn't worth it
+ && pixCount < 4000 // don't want to run out of memory
+ && isZoomAvailable(i, MapSourceLibrary.getSource(currentSource)))
+ {
+ _zoomDropdown.addItem("" + i);
+ if (i == currentZoom) {
+ zoomToSelect = _zoomDropdown.getItemCount() - 1;
+ }
+ }
+ // else System.out.println("Not using zoom " + i + " because pixCount=" + pixCount + " and xyExtent=" + xyExtent);
+ }
+ }
+ _zoomDropdown.setSelectedIndex(zoomToSelect);
+ _rebuilding = false;
+
+ _okButton.setEnabled(!_useImageCheckbox.isSelected() ||
+ (_zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0));
+ updateImagePreview();
+ }
+
+ /**
+ * @return true if it should be possible to use an image, false if no disk cache or cache empty
+ */
+ public static boolean isImagePossible()
+ {
+ String path = Config.getConfigString(Config.KEY_DISK_CACHE);
+ if (path != null && !path.equals(""))
+ {
+ File cacheDir = new File(path);
+ if (cacheDir.exists() && cacheDir.isDirectory())
+ {
+ // Check if there are any directories in the cache
+ for (File subdir : cacheDir.listFiles())
+ {
+ if (subdir.exists() && subdir.isDirectory()) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * See if the requested zoom level is available
+ * @param inZoom zoom level
+ * @param inSource selected map source
+ * @return true if there is a zoom directory for each of the source's layers
+ */
+ private static boolean isZoomAvailable(int inZoom, MapSource inSource)
+ {
+ if (inSource == null) {return false;}
+ String path = Config.getConfigString(Config.KEY_DISK_CACHE);
+ if (path == null || path.equals("")) {
+ return false;
+ }
+ File cacheDir = new File(path);
+ if (!cacheDir.exists() || !cacheDir.isDirectory()) {
+ return false;
+ }
+ // First layer
+ File layer0 = new File(cacheDir, inSource.getSiteName(0) + inZoom);
+ if (!layer0.exists() || !layer0.isDirectory() || !layer0.canRead()) {
+ return false;
+ }
+ // Second layer, if any
+ if (inSource.getNumLayers() > 1)
+ {
+ File layer1 = new File(cacheDir, inSource.getSiteName(1) + inZoom);
+ if (!layer1.exists() || !layer1.isDirectory() || !layer1.canRead()) {
+ return false;
+ }
+ }
+ // must be ok
+ return true;
+ }
+
+
+ /**
+ * @return image definition object
+ */
+ public ImageDefinition getImageDefinition() {
+ return _imageDef;
+ }
+
+ /**
+ * Make the dialog components to select the options
+ * @return Component holding gui elements
+ */
+ private Component makeDialogComponents()
+ {
+ JPanel panel = new JPanel();
+ panel.setLayout(new BorderLayout());
+ _useImageCheckbox = new JCheckBox(I18nManager.getText("dialog.baseimage.useimage"));
+ _useImageCheckbox.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4));
+ _useImageCheckbox.setHorizontalAlignment(JLabel.CENTER);
+ _useImageCheckbox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ refreshDialog();
+ }
+ });
+ panel.add(_useImageCheckbox, BorderLayout.NORTH);
+
+ // Outer panel with the grid and the map preview
+ _mainPanel = new JPanel();
+ _mainPanel.setLayout(new BorderLayout(1, 10));
+ // Central stuff with labels and dropdowns
+ JPanel controlsPanel = new JPanel();
+ controlsPanel.setLayout(new GridLayout(0, 2, 10, 4));
+ // map source
+ JLabel sourceLabel = new JLabel(I18nManager.getText("dialog.baseimage.mapsource") + ": ");
+ sourceLabel.setHorizontalAlignment(JLabel.RIGHT);
+ controlsPanel.add(sourceLabel);
+ _mapSourceDropdown = new JComboBox<String>();
+ _mapSourceDropdown.addItem("name of map source");
+ // Add listener to dropdown to change zoom levels
+ _mapSourceDropdown.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ refreshDialog();
+ }
+ });
+ controlsPanel.add(_mapSourceDropdown);
+ // zoom level
+ JLabel zoomLabel = new JLabel(I18nManager.getText("dialog.baseimage.zoom") + ": ");
+ zoomLabel.setHorizontalAlignment(JLabel.RIGHT);
+ controlsPanel.add(zoomLabel);
+ _zoomDropdown = new JComboBox<String>();
+ // Add action listener to enable ok button when zoom changed
+ _zoomDropdown.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ if (_zoomDropdown.getSelectedIndex() >= 0) {
+ _okButton.setEnabled(true);
+ updateImagePreview();
+ }
+ }
+ });
+ controlsPanel.add(_zoomDropdown);
+ _mainPanel.add(controlsPanel, BorderLayout.NORTH);
+
+ JPanel imagePanel = new JPanel();
+ imagePanel.setLayout(new BorderLayout(10, 1));
+ // image preview
+ _previewPanel = new ImagePreviewPanel();
+ imagePanel.add(_previewPanel, BorderLayout.CENTER);
+
+ // Label panel on right
+ JPanel labelPanel = new JPanel();
+ labelPanel.setLayout(new BorderLayout());
+ JPanel downloadPanel = new JPanel();
+ downloadPanel.setLayout(new BorderLayout(4, 4));
+ _downloadTilesButton = new JButton(I18nManager.getText("button.load"));
+ _downloadTilesButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent arg0) {
+ downloadRemainingTiles();
+ }
+ });
+ _downloadTilesButton.setVisible(false);
+ downloadPanel.add(_downloadTilesButton, BorderLayout.NORTH);
+ _progressBar = new JProgressBar();
+ _progressBar.setIndeterminate(true);
+ _progressBar.setVisible(false);
+ downloadPanel.add(_progressBar, BorderLayout.SOUTH);
+ labelPanel.add(downloadPanel, BorderLayout.NORTH);
+ JPanel labelGridPanel = new JPanel();
+ labelGridPanel.setLayout(new GridLayout(0, 2, 10, 4));
+ labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.tiles") + ": "));
+ _tilesFoundLabel = new JLabel("11 / 11");
+ labelGridPanel.add(_tilesFoundLabel);
+ labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.size") + ": "));
+ _imageSizeLabel = new JLabel("1430");
+ labelGridPanel.add(_imageSizeLabel);
+ labelGridPanel.add(new JLabel(" ")); // just for spacing
+ labelPanel.add(labelGridPanel, BorderLayout.SOUTH);
+ imagePanel.add(labelPanel, BorderLayout.EAST);
+
+ _mainPanel.add(imagePanel, BorderLayout.CENTER);
+ panel.add(_mainPanel, BorderLayout.CENTER);
+
+ // OK, Cancel buttons
+ JPanel buttonPanel = new JPanel();
+ buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
+ _okButton = new JButton(I18nManager.getText("button.ok"));
+ _okButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ // Check values, maybe don't want to exit
+ if (!_useImageCheckbox.isSelected()
+ || (_mapSourceDropdown.getSelectedIndex() >= 0 && _zoomDropdown.getSelectedIndex() >= 0))
+ {
+ storeValues();
+ _dialog.dispose();
+ }
+ }
+ });
+ buttonPanel.add(_okButton);
+ JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ _dialog.dispose();
+ }
+ });
+ buttonPanel.add(cancelButton);
+ panel.add(buttonPanel, BorderLayout.SOUTH);
+
+ // Listener to close dialog if escape pressed
+ KeyAdapter closer = new KeyAdapter() {
+ public void keyReleased(KeyEvent e)
+ {
+ if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+ _dialog.dispose();
+ }
+ }
+ };
+ _useImageCheckbox.addKeyListener(closer);
+ _mapSourceDropdown.addKeyListener(closer);
+ _zoomDropdown.addKeyListener(closer);
+ _okButton.addKeyListener(closer);
+ cancelButton.addKeyListener(closer);
+
+ return panel;
+ }
+
+ /**
+ * Use the selected settings to make a preview image and (asynchronously) update the preview panel
+ */
+ private void updateImagePreview()
+ {
+ // Clear labels
+ _downloadTilesButton.setVisible(false);
+ _tilesFoundLabel.setText("");
+ _imageSizeLabel.setText("");
+ if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getSelectedIndex() >= 0
+ && _zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0)
+ {
+ _previewPanel.startLoading();
+ // Launch a separate thread to create an image and pass it to the preview panel
+ new Thread(this).start();
+ }
+ else {
+ // clear preview
+ _previewPanel.setImage(null);
+ }
+ }
+
+ /**
+ * Store the selected details in the variables
+ */
+ private void storeValues()
+ {
+ // Store values of controls in variables
+ _imageDef.setUseImage(_useImageCheckbox.isSelected(),
+ _mapSourceDropdown.getSelectedIndex(),
+ getSelectedZoomLevel());
+ // Inform parent that details have changed
+ _parent.baseImageChanged();
+ }
+
+ /**
+ * Run method for separate thread. Uses the current dialog parameters
+ * to trigger a call to the Grouter, and pass the image to the preview panel
+ */
+ public void run()
+ {
+ // Remember the current dropdown indices, so we know whether they've changed or not
+ final int mapIndex = _mapSourceDropdown.getSelectedIndex();
+ final int zoomIndex = _zoomDropdown.getSelectedIndex();
+ if (!_useImageCheckbox.isSelected() || mapIndex < 0 || zoomIndex < 0) {return;}
+
+ // Get the map source from the index
+ MapSource mapSource = MapSourceLibrary.getSource(mapIndex);
+
+ // Use the Grouter to create an image (slow, blocks thread)
+ GroutedImage groutedImage = _grouter.createMapImage(_track, mapSource, getSelectedZoomLevel());
+
+ // If the dialog hasn't changed, pass the generated image to the preview panel
+ if (_useImageCheckbox.isSelected()
+ && _mapSourceDropdown.getSelectedIndex() == mapIndex
+ && _zoomDropdown.getSelectedIndex() == zoomIndex
+ && groutedImage != null)
+ {
+ _previewPanel.setImage(groutedImage);
+ final int numTilesRemaining = groutedImage.getNumTilesTotal() - groutedImage.getNumTilesUsed();
+ final boolean offerDownload = numTilesRemaining > 0 && numTilesRemaining < 50 && Config.getConfigBoolean(Config.KEY_ONLINE_MODE);
+ // Set values of labels
+ _downloadTilesButton.setVisible(offerDownload);
+ _downloadTilesButton.setEnabled(offerDownload);
+ _tilesFoundLabel.setText(groutedImage.getNumTilesUsed() + " / " + groutedImage.getNumTilesTotal());
+ if (groutedImage.getImageSize() > 0) {
+ _imageSizeLabel.setText("" + groutedImage.getImageSize());
+ }
+ else {
+ _imageSizeLabel.setText("");
+ }
+ }
+ else
+ {
+ _previewPanel.setImage(null);
+ // Clear labels
+ _downloadTilesButton.setVisible(false);
+ _tilesFoundLabel.setText("");
+ _imageSizeLabel.setText("");
+ }
+ }
+
+ /**
+ * @return zoom level selected in the dropdown
+ */
+ private int getSelectedZoomLevel()
+ {
+ int zoomLevel = 0;
+ try {
+ zoomLevel = Integer.parseInt(_zoomDropdown.getSelectedItem().toString());
+ }
+ catch (NullPointerException npe) {}
+ catch (Exception e) {
+ System.err.println("Exception: " + e.getClass().getName() + " : " + e.getMessage());
+ }
+ return zoomLevel;
+ }
+
+ /**
+ * @return true if any map data has been found for the image
+ */
+ public boolean getFoundData()
+ {
+ return _imageDef.getUseImage() && _imageDef.getZoom() > 0
+ && _previewPanel != null && _previewPanel.getTilesFound();
+ }
+
+ /**
+ * @return true if selected zoom is valid for the current track (based only on pixel size)
+ */
+ public boolean isSelectedZoomValid()
+ {
+ final double xyExtent = Math.max(_track.getXRange().getRange(), _track.getYRange().getRange());
+ // How many pixels does this give?
+ final int zoomFactor = 1 << _imageDef.getZoom();
+ final int pixCount = (int) (xyExtent * zoomFactor * 256);
+ return (pixCount > 100 // less than this isn't worth it
+ && pixCount < 4000); // don't want to run out of memory
+ }
+
+ /**
+ * @return the map grouter for retrieval of generated image
+ */
+ public MapGrouter getGrouter()
+ {
+ return _grouter;
+ }
+
+ /**
+ * @return method triggered by "download" button, to asynchronously download the missing tiles
+ */
+ private void downloadRemainingTiles()
+ {
+ _downloadTilesButton.setEnabled(false);
+ new Thread(new Runnable() {
+ public void run()
+ {
+ _progressBar.setVisible(true);
+ // Use a grouter to get all tiles from the TileManager, including downloading
+ MapGrouter grouter = new MapGrouter();
+ final int mapIndex = _mapSourceDropdown.getSelectedIndex();
+ if (!_useImageCheckbox.isSelected() || mapIndex < 0) {return;}
+ MapSource mapSource = MapSourceLibrary.getSource(mapIndex);
+ grouter.createMapImage(_track, mapSource, getSelectedZoomLevel(), true);
+ _progressBar.setVisible(false);
+ // And then refresh the dialog
+ _grouter.clearMapImage();
+ updateImagePreview();
+ }
+ }).start();
+ }
+}