001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028package edu.wisc.ssec.mcidasv; 029 030import static edu.wisc.ssec.mcidasv.McIdasPreferenceManager.PROP_HIQ_FONT_RENDERING; 031import static ucar.unidata.util.GuiUtils.makeMenu; 032import static ucar.unidata.util.MenuUtil.MENU_SEPARATOR; 033 034import java.io.BufferedReader; 035import java.io.FileReader; 036import java.io.IOException; 037import java.util.ArrayList; 038import java.util.List; 039import java.util.Map; 040import java.util.logging.Logger; 041import java.util.Comparator; 042 043import edu.wisc.ssec.mcidasv.startupmanager.options.BooleanOption; 044import org.python.util.PythonInterpreter; 045 046import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster; 047import edu.wisc.ssec.mcidasv.util.CollectionHelpers; 048 049import ucar.unidata.data.DataSource; 050import ucar.unidata.data.DescriptorDataSource; 051import ucar.unidata.idv.IntegratedDataViewer; 052import ucar.unidata.idv.ui.ImageGenerator; 053import ucar.unidata.idv.ui.JythonShell; 054import ucar.unidata.util.FileManager; 055import ucar.unidata.util.Misc; 056 057import javax.swing.*; 058import javax.swing.filechooser.FileNameExtensionFilter; 059 060/** 061 * Overrides the IDV's {@link ucar.unidata.idv.JythonManager JythonManager} to 062 * associate a {@link JythonShell} with a given {@code JythonManager}. 063 */ 064public class JythonManager extends ucar.unidata.idv.JythonManager { 065 066// /** Trusty logging object. */ 067// private static final Logger logger = LoggerFactory.getLogger(JythonManager.class); 068 069 /** Associated Jython Shell. May be {@code null}. */ 070 private JythonShell jythonShell; 071 072 private static final Logger logger = 073 Logger.getLogger(JythonManager.class.getName()); 074 075 /** 076 * Create the manager and call initPython. 077 * 078 * @param idv The IDV. 079 */ 080 public JythonManager(IntegratedDataViewer idv) { 081 super(idv); 082 } 083 084 /** 085 * Create a Jython shell, if one doesn't already exist. This will also 086 * bring the window {@literal "to the front"} of the rest of the McIDAS-V 087 * session. 088 * 089 * @return JythonShell object for interactive Jython usage. 090 */ 091 public JythonShell createShell() { 092 if (jythonShell == null) { 093 jythonShell = new JythonShell(getIdv()); 094 095 } 096 jythonShell.toFront(); 097 return jythonShell; 098 } 099 100 public JythonShell createShellWithScript(String file) { 101 if (jythonShell == null) { 102 jythonShell = new JythonShell(getIdv()); 103 104 } 105 jythonShell.toFront(); 106 String cmd = ""; 107 try (BufferedReader reader = new BufferedReader(new FileReader(file))) { 108 String line; 109 while ((line = reader.readLine()) != null) { 110 cmd += line + "\n"; 111 } 112 } catch (IOException e) { 113 logger.info("Error reading scripting file: " + e.getMessage()); 114 } 115 116 jythonShell.eval(cmd); 117 return jythonShell; 118 } 119 120 /** 121 * Returns the Jython Shell associated with this {@code JythonManager}. 122 * 123 * @return Jython Shell being used by this manager. May be {@code null}. 124 */ 125 public JythonShell getShell() { 126 return jythonShell; 127 } 128 129 /** 130 * Create and initialize a Jython interpreter. 131 * 132 * @return Newly created Jython interpreter. 133 */ 134 @Override public PythonInterpreter createInterpreter() { 135 PythonInterpreter interpreter = super.createInterpreter(); 136 return interpreter; 137 } 138 139 /** 140 * Removes the given interpreter from the list of active interpreters. 141 * 142 * <p>Also attempts to close any Jython Shell associated with the 143 * interpreter.</p> 144 * 145 * @param interpreter Interpreter to remove. Should not be {@code null}. 146 */ 147 @Override public void removeInterpreter(PythonInterpreter interpreter) { 148 super.removeInterpreter(interpreter); 149 if ((jythonShell != null) && !jythonShell.isShellResetting() && jythonShell.getInterpreter().equals(interpreter)) { 150 jythonShell.close(); 151 jythonShell = null; 152 } 153 } 154 155 /** 156 * Overridden so that McIDAS-V can add an {@code islInterpreter} object 157 * to the interpreter's locals (before executing the contents of {@code}. 158 * 159 * @param code Jython code to evaluate. {@code null} is probably a bad idea. 160 * @param properties {@code String->Object} pairs to insert into the 161 * locals. Parameter may be {@code null}. 162 */ 163 @SuppressWarnings("unchecked") // dealing with idv code that predates generics. 164 @Override public void evaluateTrusted(String code, Map<String, Object> properties) { 165 if (properties == null) { 166 properties = CollectionHelpers.newMap(); 167 } 168 properties.putIfAbsent("islInterpreter", new ImageGenerator(getIdv())); 169 properties.putIfAbsent("_idv", getIdv()); 170 properties.putIfAbsent("idv", getIdv()); 171 super.evaluateTrusted(code, properties); 172 } 173 174 /** 175 * Return the list of menu items to use when the user has clicked on a 176 * formula {@link DataSource}. 177 * 178 * @param dataSource The data source clicked on. 179 * 180 * @return {@link List} of menu items. 181 */ 182 @SuppressWarnings("unchecked") // dealing with idv code that predates generics. 183 @Override public List doMakeFormulaDataSourceMenuItems(DataSource dataSource) { 184 List menuItems = new ArrayList(100); 185 JMenuItem menuItem; 186 187 menuItem = new JMenuItem("Create Formula"); 188 menuItem.addActionListener(e -> showFormulaDialog()); 189 menuItems.add(menuItem); 190 191 List editItems; 192 if (dataSource instanceof DescriptorDataSource) { 193 editItems = doMakeEditMenuItems((DescriptorDataSource)dataSource); 194 } else { 195 editItems = doMakeEditMenuItems(); 196 } 197 // Remove any accidental top-level "Edit Formulas" header 198 editItems.removeIf(item -> (item instanceof String) && ((String)item).equalsIgnoreCase("Edit Formulas")); 199 200 // Sort within each group 201 List<Object> sortedGroupedItems = new ArrayList<>(); 202 Object currentGroupHeader = null; 203 List<JMenuItem> currentGroupItems = new ArrayList<>(); 204 205 for (Object item : editItems) { 206 if (item instanceof String) { 207 if (!currentGroupItems.isEmpty()) { 208 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 209 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 210 sortedGroupedItems.addAll(currentGroupItems); 211 currentGroupItems.clear(); 212 } 213 currentGroupHeader = item; 214 } else if (item instanceof JMenuItem) { 215 currentGroupItems.add((JMenuItem)item); 216 } else { 217 if (!currentGroupItems.isEmpty()) { 218 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 219 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 220 sortedGroupedItems.addAll(currentGroupItems); 221 currentGroupItems.clear(); 222 } 223 currentGroupHeader = null; 224 sortedGroupedItems.add(item); 225 } 226 } 227 228 if (!currentGroupItems.isEmpty()) { 229 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 230 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 231 sortedGroupedItems.addAll(currentGroupItems); 232 } 233 234 sortMenuItems(sortedGroupedItems); 235 menuItems.add(makeMenu("Edit Formulas", sortedGroupedItems)); 236 237 menuItems.add(MENU_SEPARATOR); 238 239 menuItem = new JMenuItem("Jython Library"); 240 menuItem.addActionListener(e -> showJythonEditor()); 241 menuItems.add(menuItem); 242 243 menuItem = new JMenuItem("Jython Shell"); 244 menuItem.addActionListener(e -> createShell()); 245 menuItems.add(menuItem); 246 247 menuItem = new JMenuItem("Load Jython Script"); 248 menuItem.addActionListener(e -> { 249 FileNameExtensionFilter filter = new FileNameExtensionFilter("Python Files (*.py)", "py"); 250 String file = FileManager.getReadFile("Load Script", filter); 251 createShellWithScript(file); 252 }); 253 254 menuItems.add(menuItem); 255 256 menuItems.add(MENU_SEPARATOR); 257 258 menuItem = new JMenuItem("Import"); 259 menuItem.addActionListener(e -> importFormulas()); 260 menuItems.add(menuItem); 261 262 menuItem = new JMenuItem("Export"); 263 menuItem.addActionListener(e -> exportFormulas()); 264 menuItems.add(menuItem); 265 266 return menuItems; 267 } 268 269 // Recursively sort nested JMenu children 270 private void sortMenuItems(List<?> items) { 271 items.sort((o1, o2) -> { 272 if (o1 instanceof JMenuItem && o2 instanceof JMenuItem) { 273 return ((JMenuItem) o1).getText().compareToIgnoreCase(((JMenuItem) o2).getText()); 274 } 275 return 0; 276 }); 277 278 for (Object item : items) { 279 if (item instanceof JMenu) { 280 JMenu submenu = (JMenu) item; 281 List<JMenuItem> subItems = new ArrayList<>(); 282 for (int i = 0; i < submenu.getItemCount(); i++) { 283 JMenuItem child = submenu.getItem(i); 284 if (child != null) subItems.add(child); 285 } 286 sortMenuItems(subItems); 287 submenu.removeAll(); 288 for (JMenuItem sortedChild : subItems) { 289 submenu.add(sortedChild); 290 } 291 } 292 } 293 } 294 295 /** 296 * Determine if the user should be warned about a potential bug that we've been unable to resolve. 297 * 298 * <p>The conditions for the bug to appear are: 299 * <ul> 300 * <li>In background mode (i.e. running a script).</li> 301 * <li>Geometry by reference is <b>disabled</b>.</li> 302 * <li>New font rendering is <b>enabled</b>.</li> 303 * </ul> 304 * 305 * @return {@code true} if the user's configuration has made it possible for bug to manifest. 306 */ 307 public static boolean shouldWarnImageCapturing() { 308 boolean backgroundMode = McIDASV.getStaticMcv().getArgsManager().isScriptingMode(); 309 boolean shouldWarn = false; 310 OptionMaster optMaster = OptionMaster.getInstance(); 311 BooleanOption useGeometryByRef = optMaster.getBooleanOption("USE_GEOBYREF"); 312 if (useGeometryByRef != null) { 313 shouldWarn = backgroundMode 314 && !Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef")) 315 && Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false")); 316 } 317// System.out.println("background mode: "+backgroundMode); 318// System.out.println("geo by ref: "+Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef"))); 319// System.out.println("new fonts: "+Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false"))); 320// System.out.println("shouldWarn: "+shouldWarn); 321// System.out.println("props:\n"+System.getProperties()); 322// return shouldWarn; 323 return false; 324 } 325 326}