]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/save/BaseImageConfigDialog.java
Version 20.4, May 2021
[GpsPrune.git] / src / tim / prune / save / BaseImageConfigDialog.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.FlowLayout;
6 import java.awt.GridLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.KeyAdapter;
10 import java.awt.event.KeyEvent;
11 import java.io.File;
12
13 import javax.swing.BorderFactory;
14 import javax.swing.JButton;
15 import javax.swing.JCheckBox;
16 import javax.swing.JComboBox;
17 import javax.swing.JDialog;
18 import javax.swing.JLabel;
19 import javax.swing.JPanel;
20 import javax.swing.JProgressBar;
21
22 import tim.prune.I18nManager;
23 import tim.prune.config.Config;
24 import tim.prune.data.Track;
25 import tim.prune.gui.map.MapSource;
26 import tim.prune.gui.map.MapSourceLibrary;
27 import tim.prune.threedee.ImageDefinition;
28
29 /**
30  * Dialog to let you choose the parameters for a base image
31  * (source and zoom) including preview
32  */
33 public class BaseImageConfigDialog implements Runnable
34 {
35         /** Parent to notify */
36         private BaseImageConsumer _parent = null;
37         /** Parent dialog for position */
38         private JDialog _parentDialog = null;
39         /** Track to use for preview image */
40         private Track _track = null;
41         /** Dialog to show */
42         private JDialog _dialog = null;
43         /** Checkbox for using an image or not */
44         private JCheckBox _useImageCheckbox = null;
45         /** Panel to hold the other controls */
46         private JPanel _mainPanel = null;
47         /** Dropdown for map source */
48         private JComboBox<String> _mapSourceDropdown = null;
49         /** Dropdown for zoom levels */
50         private JComboBox<String> _zoomDropdown = null;
51         /** Button to trigger a download of the missing map tiles */
52         private JButton _downloadTilesButton = null;
53         /** Progress bar for downloading additional tiles */
54         private JProgressBar _progressBar = null;
55         /** Label for number of tiles found */
56         private JLabel _tilesFoundLabel = null;
57         /** Label for image size in pixels */
58         private JLabel _imageSizeLabel = null;
59         /** Image preview panel */
60         private ImagePreviewPanel _previewPanel = null;
61         /** Grouter, used to avoid regenerating images */
62         private MapGrouter _grouter = new MapGrouter();
63         /** OK button, needs to be enabled/disabled */
64         private JButton _okButton = null;
65         /** Flag for rebuilding dialog, don't bother refreshing and recalculating */
66         private boolean _rebuilding = false;
67         /** Cached values to allow cancellation of dialog */
68         private ImageDefinition _imageDef = new ImageDefinition();
69
70
71         /**
72          * Constructor
73          * @param inParent parent object to notify on completion of dialog
74          * @param inParentDialog parent dialog
75          * @param inTrack track object
76          */
77         public BaseImageConfigDialog(BaseImageConsumer inParent, JDialog inParentDialog, Track inTrack)
78         {
79                 _parent = inParent;
80                 _parentDialog = inParentDialog;
81                 _dialog = new JDialog(inParentDialog, I18nManager.getText("dialog.baseimage.title"), true);
82                 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
83                 _dialog.getContentPane().add(makeDialogComponents());
84                 _dialog.pack();
85                 _track = inTrack;
86         }
87
88         /**
89          * @param inDefinition image definition object from previous dialog
90          */
91         public void setImageDefinition(ImageDefinition inDefinition)
92         {
93                 _imageDef = inDefinition;
94                 if (_imageDef == null) {
95                         _imageDef = new ImageDefinition();
96                 }
97         }
98
99         /**
100          * Begin the function
101          */
102         public void begin()
103         {
104                 initDialog();
105                 _dialog.setLocationRelativeTo(_parentDialog);
106                 _dialog.setVisible(true);
107         }
108
109         /**
110          * Begin the function with a default of using an image
111          */
112         public void beginWithImageYes()
113         {
114                 initDialog();
115                 _useImageCheckbox.setSelected(true);
116                 refreshDialog();
117                 _dialog.setVisible(true);
118         }
119
120         /**
121          * Initialise the dialog from the cached values
122          */
123         private void initDialog()
124         {
125                 _rebuilding = true;
126                 _useImageCheckbox.setSelected(_imageDef.getUseImage());
127                 // Populate the dropdown of map sources from the library in case it has changed
128                 _mapSourceDropdown.removeAllItems();
129                 for (int i=0; i<MapSourceLibrary.getNumSources(); i++)
130                 {
131                         _mapSourceDropdown.addItem(MapSourceLibrary.getSource(i).getName());
132                 }
133                 int sourceIndex = _imageDef.getSourceIndex();
134                 if (sourceIndex < 0 || sourceIndex >= _mapSourceDropdown.getItemCount()) {
135                         sourceIndex = 0;
136                 }
137                 _mapSourceDropdown.setSelectedIndex(sourceIndex);
138
139                 // Zoom level
140                 int zoomLevel = _imageDef.getZoom();
141                 if (_imageDef.getUseImage())
142                 {
143                         for (int i=0; i<_zoomDropdown.getItemCount(); i++)
144                         {
145                                 String item = _zoomDropdown.getItemAt(i);
146                                 try {
147                                         if (Integer.parseInt(item) == zoomLevel)
148                                         {
149                                                 _zoomDropdown.setSelectedIndex(i);
150                                                 break;
151                                         }
152                                 }
153                                 catch (NumberFormatException nfe) {}
154                         }
155                 }
156                 _rebuilding = false;
157                 refreshDialog();
158         }
159
160         /**
161          * Update the visibility of the controls, and update the zoom dropdown based on the selected map source
162          */
163         private void refreshDialog()
164         {
165                 _mainPanel.setVisible(_useImageCheckbox.isSelected());
166                 // Exit if we're in the middle of something
167                 if (_rebuilding) {return;}
168                 int currentZoom = 0;
169                 try {
170                         currentZoom = Integer.parseInt(_zoomDropdown.getSelectedItem().toString());
171                 }
172                 catch (Exception nfe) {}
173                 // First time in, the dropdown might be empty but we still might have a zoom in the definition
174                 if (_zoomDropdown.getItemCount() == 0) {
175                         currentZoom = _imageDef.getZoom();
176                 }
177                 // Get the extent of the track so we can work out how big the images are going to be for each zoom level
178                 final double xyExtent = Math.max(_track.getXRange().getRange(), _track.getYRange().getRange());
179                 int zoomToSelect = -1;
180
181                 _rebuilding = true;
182                 _zoomDropdown.removeAllItems();
183                 if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getItemCount() > 0)
184                 {
185                         int currentSource = _mapSourceDropdown.getSelectedIndex();
186                         for (int i=5; i<18; i++)
187                         {
188                                 // How many pixels does this give?
189                                 final int zoomFactor = 1 << i;
190                                 final int pixCount = (int) (xyExtent * zoomFactor * 256);
191                                 if (pixCount > 100      // less than this isn't worth it
192                                         && pixCount < 4000  // don't want to run out of memory
193                                         && isZoomAvailable(i, MapSourceLibrary.getSource(currentSource)))
194                                 {
195                                         _zoomDropdown.addItem("" + i);
196                                         if (i == currentZoom) {
197                                                 zoomToSelect = _zoomDropdown.getItemCount() - 1;
198                                         }
199                                 }
200                                 // else System.out.println("Not using zoom " + i + " because pixCount=" + pixCount + " and xyExtent=" + xyExtent);
201                         }
202                 }
203                 _zoomDropdown.setSelectedIndex(zoomToSelect);
204                 _rebuilding = false;
205
206                 _okButton.setEnabled(!_useImageCheckbox.isSelected() ||
207                         (_zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0));
208                 updateImagePreview();
209         }
210
211         /**
212          * @return true if it should be possible to use an image, false if no disk cache or cache empty
213          */
214         public static boolean isImagePossible()
215         {
216                 String path = Config.getConfigString(Config.KEY_DISK_CACHE);
217                 if (path != null && !path.equals(""))
218                 {
219                         File cacheDir = new File(path);
220                         if (cacheDir.exists() && cacheDir.isDirectory())
221                         {
222                                 // Check if there are any directories in the cache
223                                 for (File subdir : cacheDir.listFiles())
224                                 {
225                                         if (subdir.exists() && subdir.isDirectory()) {
226                                                 return true;
227                                         }
228                                 }
229                         }
230                 }
231                 return false;
232         }
233
234         /**
235          * See if the requested zoom level is available
236          * @param inZoom zoom level
237          * @param inSource selected map source
238          * @return true if there is a zoom directory for each of the source's layers
239          */
240         private static boolean isZoomAvailable(int inZoom, MapSource inSource)
241         {
242                 if (inSource == null) {return false;}
243                 String path = Config.getConfigString(Config.KEY_DISK_CACHE);
244                 if (path == null || path.equals("")) {
245                         return false;
246                 }
247                 File cacheDir = new File(path);
248                 if (!cacheDir.exists() || !cacheDir.isDirectory()) {
249                         return false;
250                 }
251                 // First layer
252                 File layer0 = new File(cacheDir, inSource.getSiteName(0) + inZoom);
253                 if (!layer0.exists() || !layer0.isDirectory() || !layer0.canRead()) {
254                         return false;
255                 }
256                 // Second layer, if any
257                 if (inSource.getNumLayers() > 1)
258                 {
259                         File layer1 = new File(cacheDir, inSource.getSiteName(1) + inZoom);
260                         if (!layer1.exists() || !layer1.isDirectory() || !layer1.canRead()) {
261                                 return false;
262                         }
263                 }
264                 // must be ok
265                 return true;
266         }
267
268
269         /**
270          * @return image definition object
271          */
272         public ImageDefinition getImageDefinition() {
273                 return _imageDef;
274         }
275
276         /**
277          * Make the dialog components to select the options
278          * @return Component holding gui elements
279          */
280         private Component makeDialogComponents()
281         {
282                 JPanel panel = new JPanel();
283                 panel.setLayout(new BorderLayout());
284                 _useImageCheckbox = new JCheckBox(I18nManager.getText("dialog.baseimage.useimage"));
285                 _useImageCheckbox.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4));
286                 _useImageCheckbox.setHorizontalAlignment(JLabel.CENTER);
287                 _useImageCheckbox.addActionListener(new ActionListener() {
288                         public void actionPerformed(ActionEvent arg0) {
289                                 refreshDialog();
290                         }
291                 });
292                 panel.add(_useImageCheckbox, BorderLayout.NORTH);
293
294                 // Outer panel with the grid and the map preview
295                 _mainPanel = new JPanel();
296                 _mainPanel.setLayout(new BorderLayout(1, 10));
297                 // Central stuff with labels and dropdowns
298                 JPanel controlsPanel = new JPanel();
299                 controlsPanel.setLayout(new GridLayout(0, 2, 10, 4));
300                 // map source
301                 JLabel sourceLabel = new JLabel(I18nManager.getText("dialog.baseimage.mapsource") + ": ");
302                 sourceLabel.setHorizontalAlignment(JLabel.RIGHT);
303                 controlsPanel.add(sourceLabel);
304                 _mapSourceDropdown = new JComboBox<String>();
305                 _mapSourceDropdown.addItem("name of map source");
306                 // Add listener to dropdown to change zoom levels
307                 _mapSourceDropdown.addActionListener(new ActionListener() {
308                         public void actionPerformed(ActionEvent arg0) {
309                                 refreshDialog();
310                         }
311                 });
312                 controlsPanel.add(_mapSourceDropdown);
313                 // zoom level
314                 JLabel zoomLabel = new JLabel(I18nManager.getText("dialog.baseimage.zoom") + ": ");
315                 zoomLabel.setHorizontalAlignment(JLabel.RIGHT);
316                 controlsPanel.add(zoomLabel);
317                 _zoomDropdown = new JComboBox<String>();
318                 // Add action listener to enable ok button when zoom changed
319                 _zoomDropdown.addActionListener(new ActionListener() {
320                         public void actionPerformed(ActionEvent arg0) {
321                                 if (_zoomDropdown.getSelectedIndex() >= 0) {
322                                         _okButton.setEnabled(true);
323                                         updateImagePreview();
324                                 }
325                         }
326                 });
327                 controlsPanel.add(_zoomDropdown);
328                 _mainPanel.add(controlsPanel, BorderLayout.NORTH);
329
330                 JPanel imagePanel = new JPanel();
331                 imagePanel.setLayout(new BorderLayout(10, 1));
332                 // image preview
333                 _previewPanel = new ImagePreviewPanel();
334                 imagePanel.add(_previewPanel, BorderLayout.CENTER);
335
336                 // Label panel on right
337                 JPanel labelPanel = new JPanel();
338                 labelPanel.setLayout(new BorderLayout());
339                 JPanel downloadPanel = new JPanel();
340                 downloadPanel.setLayout(new BorderLayout(4, 4));
341                 _downloadTilesButton = new JButton(I18nManager.getText("button.load"));
342                 _downloadTilesButton.addActionListener(new ActionListener() {
343                         public void actionPerformed(ActionEvent arg0) {
344                                 downloadRemainingTiles();
345                         }
346                 });
347                 _downloadTilesButton.setVisible(false);
348                 downloadPanel.add(_downloadTilesButton, BorderLayout.NORTH);
349                 _progressBar = new JProgressBar();
350                 _progressBar.setIndeterminate(true);
351                 _progressBar.setVisible(false);
352                 downloadPanel.add(_progressBar, BorderLayout.SOUTH);
353                 labelPanel.add(downloadPanel, BorderLayout.NORTH);
354                 JPanel labelGridPanel = new JPanel();
355                 labelGridPanel.setLayout(new GridLayout(0, 2, 10, 4));
356                 labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.tiles") + ": "));
357                 _tilesFoundLabel = new JLabel("11 / 11");
358                 labelGridPanel.add(_tilesFoundLabel);
359                 labelGridPanel.add(new JLabel(I18nManager.getText("dialog.baseimage.size") + ": "));
360                 _imageSizeLabel = new JLabel("1430");
361                 labelGridPanel.add(_imageSizeLabel);
362                 labelGridPanel.add(new JLabel(" ")); // just for spacing
363                 labelPanel.add(labelGridPanel, BorderLayout.SOUTH);
364                 imagePanel.add(labelPanel, BorderLayout.EAST);
365
366                 _mainPanel.add(imagePanel, BorderLayout.CENTER);
367                 panel.add(_mainPanel, BorderLayout.CENTER);
368
369                 // OK, Cancel buttons
370                 JPanel buttonPanel = new JPanel();
371                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
372                 _okButton = new JButton(I18nManager.getText("button.ok"));
373                 _okButton.addActionListener(new ActionListener() {
374                         public void actionPerformed(ActionEvent e)
375                         {
376                                 // Check values, maybe don't want to exit
377                                 if (!_useImageCheckbox.isSelected()
378                                         || (_mapSourceDropdown.getSelectedIndex() >= 0 && _zoomDropdown.getSelectedIndex() >= 0))
379                                 {
380                                         storeValues();
381                                         _dialog.dispose();
382                                 }
383                         }
384                 });
385                 buttonPanel.add(_okButton);
386                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
387                 cancelButton.addActionListener(new ActionListener() {
388                         public void actionPerformed(ActionEvent e)
389                         {
390                                 _dialog.dispose();
391                         }
392                 });
393                 buttonPanel.add(cancelButton);
394                 panel.add(buttonPanel, BorderLayout.SOUTH);
395
396                 // Listener to close dialog if escape pressed
397                 KeyAdapter closer = new KeyAdapter() {
398                         public void keyReleased(KeyEvent e)
399                         {
400                                 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
401                                         _dialog.dispose();
402                                 }
403                         }
404                 };
405                 _useImageCheckbox.addKeyListener(closer);
406                 _mapSourceDropdown.addKeyListener(closer);
407                 _zoomDropdown.addKeyListener(closer);
408                 _okButton.addKeyListener(closer);
409                 cancelButton.addKeyListener(closer);
410
411                 return panel;
412         }
413
414         /**
415          * Use the selected settings to make a preview image and (asynchronously) update the preview panel
416          */
417         private void updateImagePreview()
418         {
419                 // Clear labels
420                 _downloadTilesButton.setVisible(false);
421                 _tilesFoundLabel.setText("");
422                 _imageSizeLabel.setText("");
423                 if (_useImageCheckbox.isSelected() && _mapSourceDropdown.getSelectedIndex() >= 0
424                         && _zoomDropdown.getItemCount() > 0 && _zoomDropdown.getSelectedIndex() >= 0)
425                 {
426                         _previewPanel.startLoading();
427                         // Launch a separate thread to create an image and pass it to the preview panel
428                         new Thread(this).start();
429                 }
430                 else {
431                         // clear preview
432                         _previewPanel.setImage(null);
433                 }
434         }
435
436         /**
437          * Store the selected details in the variables
438          */
439         private void storeValues()
440         {
441                 // Store values of controls in variables
442                 _imageDef.setUseImage(_useImageCheckbox.isSelected(),
443                         _mapSourceDropdown.getSelectedIndex(),
444                         getSelectedZoomLevel());
445                 // Inform parent that details have changed
446                 _parent.baseImageChanged();
447         }
448
449         /**
450          * Run method for separate thread.  Uses the current dialog parameters
451          * to trigger a call to the Grouter, and pass the image to the preview panel
452          */
453         public void run()
454         {
455                 // Remember the current dropdown indices, so we know whether they've changed or not
456                 final int mapIndex = _mapSourceDropdown.getSelectedIndex();
457                 final int zoomIndex = _zoomDropdown.getSelectedIndex();
458                 if (!_useImageCheckbox.isSelected() || mapIndex < 0 || zoomIndex < 0) {return;}
459
460                 // Get the map source from the index
461                 MapSource mapSource = MapSourceLibrary.getSource(mapIndex);
462
463                 // Use the Grouter to create an image (slow, blocks thread)
464                 GroutedImage groutedImage = _grouter.createMapImage(_track, mapSource, getSelectedZoomLevel());
465
466                 // If the dialog hasn't changed, pass the generated image to the preview panel
467                 if (_useImageCheckbox.isSelected()
468                         && _mapSourceDropdown.getSelectedIndex() == mapIndex
469                         && _zoomDropdown.getSelectedIndex() == zoomIndex
470                         && groutedImage != null)
471                 {
472                         _previewPanel.setImage(groutedImage);
473                         final int numTilesRemaining = groutedImage.getNumTilesTotal() - groutedImage.getNumTilesUsed();
474                         final boolean offerDownload = numTilesRemaining > 0 && numTilesRemaining < 50 && Config.getConfigBoolean(Config.KEY_ONLINE_MODE);
475                         // Set values of labels
476                         _downloadTilesButton.setVisible(offerDownload);
477                         _downloadTilesButton.setEnabled(offerDownload);
478                         _tilesFoundLabel.setText(groutedImage.getNumTilesUsed() + " / " + groutedImage.getNumTilesTotal());
479                         if (groutedImage.getImageSize() > 0) {
480                                 _imageSizeLabel.setText("" + groutedImage.getImageSize());
481                         }
482                         else {
483                                 _imageSizeLabel.setText("");
484                         }
485                 }
486                 else
487                 {
488                         _previewPanel.setImage(null);
489                         // Clear labels
490                         _downloadTilesButton.setVisible(false);
491                         _tilesFoundLabel.setText("");
492                         _imageSizeLabel.setText("");
493                 }
494         }
495
496         /**
497          * @return zoom level selected in the dropdown
498          */
499         private int getSelectedZoomLevel()
500         {
501                 int zoomLevel = 0;
502                 try {
503                         zoomLevel = Integer.parseInt(_zoomDropdown.getSelectedItem().toString());
504                 }
505                 catch (NullPointerException npe) {}
506                 catch (Exception e) {
507                         System.err.println("Exception: " + e.getClass().getName() + " : " + e.getMessage());
508                 }
509                 return zoomLevel;
510         }
511
512         /**
513          * @return true if any map data has been found for the image
514          */
515         public boolean getFoundData()
516         {
517                 return _imageDef.getUseImage() && _imageDef.getZoom() > 0
518                         && _previewPanel != null && _previewPanel.getTilesFound();
519         }
520
521         /**
522          * @return true if selected zoom is valid for the current track (based only on pixel size)
523          */
524         public boolean isSelectedZoomValid()
525         {
526                 final double xyExtent = Math.max(_track.getXRange().getRange(), _track.getYRange().getRange());
527                 // How many pixels does this give?
528                 final int zoomFactor = 1 << _imageDef.getZoom();
529                 final int pixCount = (int) (xyExtent * zoomFactor * 256);
530                 return (pixCount > 100     // less than this isn't worth it
531                         && pixCount < 4000);   // don't want to run out of memory
532         }
533
534         /**
535          * @return the map grouter for retrieval of generated image
536          */
537         public MapGrouter getGrouter()
538         {
539                 return _grouter;
540         }
541
542         /**
543          * @return method triggered by "download" button, to asynchronously download the missing tiles
544          */
545         private void downloadRemainingTiles()
546         {
547                 _downloadTilesButton.setEnabled(false);
548                 new Thread(new Runnable() {
549                         public void run()
550                         {
551                                 _progressBar.setVisible(true);
552                                 // Use a grouter to get all tiles from the TileManager, including downloading
553                                 MapGrouter grouter = new MapGrouter();
554                                 final int mapIndex = _mapSourceDropdown.getSelectedIndex();
555                                 if (!_useImageCheckbox.isSelected() || mapIndex < 0) {return;}
556                                 MapSource mapSource = MapSourceLibrary.getSource(mapIndex);
557                                 grouter.createMapImage(_track, mapSource, getSelectedZoomLevel(), true);
558                                 _progressBar.setVisible(false);
559                                 // And then refresh the dialog
560                                 _grouter.clearMapImage();
561                                 updateImagePreview();
562                         }
563                 }).start();
564         }
565 }