001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.imagery; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.GridBagLayout; 007import java.awt.event.ActionEvent; 008import java.io.File; 009import java.util.ArrayList; 010import java.util.Comparator; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Map.Entry; 015import java.util.Set; 016import java.util.concurrent.ConcurrentHashMap; 017 018import javax.swing.AbstractAction; 019import javax.swing.JLabel; 020import javax.swing.JPanel; 021import javax.swing.JScrollPane; 022import javax.swing.JSpinner; 023import javax.swing.JTable; 024import javax.swing.SpinnerNumberModel; 025import javax.swing.table.DefaultTableModel; 026import javax.swing.table.TableColumn; 027import javax.swing.table.TableModel; 028 029import org.apache.commons.jcs.access.CacheAccess; 030import org.apache.commons.jcs.engine.stats.behavior.ICacheStats; 031import org.apache.commons.jcs.engine.stats.behavior.IStatElement; 032import org.apache.commons.jcs.engine.stats.behavior.IStats; 033import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 034import org.openstreetmap.josm.data.cache.JCSCacheManager; 035import org.openstreetmap.josm.data.imagery.CachedTileLoaderFactory; 036import org.openstreetmap.josm.gui.MainApplication; 037import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 038import org.openstreetmap.josm.gui.layer.TMSLayer; 039import org.openstreetmap.josm.gui.layer.WMSLayer; 040import org.openstreetmap.josm.gui.layer.WMTSLayer; 041import org.openstreetmap.josm.gui.util.GuiHelper; 042import org.openstreetmap.josm.gui.widgets.ButtonColumn; 043import org.openstreetmap.josm.gui.widgets.JosmTextField; 044import org.openstreetmap.josm.tools.GBC; 045import org.openstreetmap.josm.tools.Logging; 046import org.openstreetmap.josm.tools.Pair; 047import org.openstreetmap.josm.tools.Utils; 048 049/** 050 * Panel for cache size, location and content management. 051 * 052 * @author Wiktor Niesiobędzki 053 * 054 */ 055public class CacheSettingsPanel extends JPanel { 056 057 private final JosmTextField cacheDir = new JosmTextField(11); 058 private final JSpinner maxElementsOnDisk = new JSpinner(new SpinnerNumberModel( 059 AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get().intValue(), 0, Integer.MAX_VALUE, 1)); 060 061 /** 062 * Creates cache content panel 063 */ 064 public CacheSettingsPanel() { 065 super(new GridBagLayout()); 066 067 add(new JLabel(tr("Tile cache directory: ")), GBC.std()); 068 add(GBC.glue(5, 0), GBC.std()); 069 add(cacheDir, GBC.eol().fill(GBC.HORIZONTAL)); 070 071 add(new JLabel(tr("Maximum size of disk cache (per imagery) in MB: ")), GBC.std()); 072 add(GBC.glue(5, 0), GBC.std()); 073 add(maxElementsOnDisk, GBC.eop()); 074 075 MainApplication.worker.submit(() -> { 076 addToPanel(TMSLayer.getCache(), "TMS"); 077 addToPanel(WMSLayer.getCache(), "WMS"); 078 addToPanel(WMTSLayer.getCache(), "WMTS"); 079 }); 080 } 081 082 private void addToPanel(final CacheAccess<String, BufferedImageCacheEntry> cache, final String name) { 083 final Long cacheSize = getCacheSize(cache); 084 final String sizeString = Utils.getSizeString(cacheSize, Locale.getDefault()); 085 final TableModel tableModel = getTableModel(cache); 086 087 GuiHelper.runInEDT(() -> { 088 /* I18n: {0} is cache name (TMS/WMS/WMTS), {1} is size string */ 089 add(new JLabel(tr("{0} cache, total cache size: {1}", name, sizeString)), 090 GBC.eol().insets(5, 5, 0, 0)); 091 add(new JScrollPane(getTableForCache(cache, tableModel)), 092 GBC.eol().fill(GBC.BOTH)); 093 }); 094 } 095 096 private static Long getCacheSize(CacheAccess<String, BufferedImageCacheEntry> cache) { 097 ICacheStats stats = cache.getStatistics(); 098 for (IStats cacheStats: stats.getAuxiliaryCacheStats()) { 099 for (IStatElement<?> statElement: cacheStats.getStatElements()) { 100 if ("Data File Length".equals(statElement.getName())) { 101 Object val = statElement.getData(); 102 if (val instanceof Long) { 103 return (Long) val; 104 } 105 } 106 } 107 } 108 return Long.valueOf(-1); 109 } 110 111 /** 112 * Returns the cache stats. 113 * @param cache imagery cache 114 * @return the cache stats 115 */ 116 public static String[][] getCacheStats(CacheAccess<String, BufferedImageCacheEntry> cache) { 117 Set<String> keySet = cache.getCacheControl().getKeySet(); 118 Map<String, int[]> temp = new ConcurrentHashMap<>(); // use int[] as a Object reference to int, gives better performance 119 for (String key: keySet) { 120 String[] keyParts = key.split(":", 2); 121 if (keyParts.length == 2) { 122 int[] counter = temp.get(keyParts[0]); 123 if (counter == null) { 124 temp.put(keyParts[0], new int[]{1}); 125 } else { 126 counter[0]++; 127 } 128 } else { 129 Logging.warn("Could not parse the key: {0}. No colon found", key); 130 } 131 } 132 133 List<Pair<String, Integer>> sortedStats = new ArrayList<>(); 134 for (Entry<String, int[]> e: temp.entrySet()) { 135 sortedStats.add(new Pair<>(e.getKey(), e.getValue()[0])); 136 } 137 sortedStats.sort(Comparator.comparing(o -> o.b, Comparator.reverseOrder())); 138 String[][] ret = new String[sortedStats.size()][3]; 139 int index = 0; 140 for (Pair<String, Integer> e: sortedStats) { 141 ret[index] = new String[]{e.a, e.b.toString(), tr("Clear")}; 142 index++; 143 } 144 return ret; 145 } 146 147 private static JTable getTableForCache(final CacheAccess<String, BufferedImageCacheEntry> cache, final TableModel tableModel) { 148 final JTable ret = new JTable(tableModel); 149 150 ButtonColumn buttonColumn = new ButtonColumn( 151 new AbstractAction() { 152 @Override 153 public void actionPerformed(ActionEvent e) { 154 int row = ret.convertRowIndexToModel(ret.getEditingRow()); 155 tableModel.setValueAt("0", row, 1); 156 cache.remove(ret.getValueAt(row, 0).toString() + ':'); 157 } 158 }); 159 TableColumn tableColumn = ret.getColumnModel().getColumn(2); 160 tableColumn.setCellRenderer(buttonColumn); 161 tableColumn.setCellEditor(buttonColumn); 162 return ret; 163 } 164 165 private static DefaultTableModel getTableModel(final CacheAccess<String, BufferedImageCacheEntry> cache) { 166 return new DefaultTableModel( 167 getCacheStats(cache), 168 new String[]{tr("Cache name"), tr("Object Count"), tr("Clear")}) { 169 @Override 170 public boolean isCellEditable(int row, int column) { 171 return column == 2; 172 } 173 }; 174 } 175 176 /** 177 * Loads the common settings. 178 */ 179 void loadSettings() { 180 this.cacheDir.setText(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get()); 181 this.maxElementsOnDisk.setValue(AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get()); 182 } 183 184 /** 185 * Saves the common settings. 186 * @return true when restart is required 187 */ 188 boolean saveSettings() { 189 boolean restartRequired = removeCacheFiles(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get(), 190 1024L * 1024L * ((Integer) this.maxElementsOnDisk.getValue())); 191 192 if (!AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.get().equals(this.maxElementsOnDisk.getValue())) { 193 AbstractCachedTileSourceLayer.MAX_DISK_CACHE_SIZE.put((Integer) this.maxElementsOnDisk.getValue()); 194 restartRequired = true; 195 } 196 197 198 if (!CachedTileLoaderFactory.PROP_TILECACHE_DIR.get().equals(this.cacheDir.getText())) { 199 restartRequired = true; 200 removeCacheFiles(CachedTileLoaderFactory.PROP_TILECACHE_DIR.get(), 0); // clear old cache directory 201 CachedTileLoaderFactory.PROP_TILECACHE_DIR.put(this.cacheDir.getText()); 202 } 203 204 return restartRequired; 205 } 206 207 private static boolean removeCacheFiles(String path, long maxSize) { 208 File directory = new File(path); 209 File[] cacheFiles = directory.listFiles((dir, name) -> name.endsWith(".data") || name.endsWith(".key")); 210 boolean restartRequired = false; 211 if (cacheFiles != null) { 212 for (File cacheFile: cacheFiles) { 213 if (cacheFile.length() > maxSize) { 214 if (!restartRequired) { 215 JCSCacheManager.shutdown(); // shutdown Cache - so files can by safely deleted 216 restartRequired = true; 217 } 218 Utils.deleteFile(cacheFile); 219 File otherFile = null; 220 if (cacheFile.getName().endsWith(".data")) { 221 otherFile = new File(cacheFile.getPath().replaceAll("\\.data$", ".key")); 222 } else if (cacheFile.getName().endsWith(".key")) { 223 otherFile = new File(cacheFile.getPath().replaceAll("\\.key$", ".data")); 224 } 225 if (otherFile != null) { 226 Utils.deleteFileIfExists(otherFile); 227 } 228 } 229 } 230 } 231 return restartRequired; 232 } 233}