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