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