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.setToolTipText("Open Formula Editor window"); 189 menuItem.addActionListener(e -> showFormulaDialog()); 190 menuItems.add(menuItem); 191 192 List editItems; 193 if (dataSource instanceof DescriptorDataSource) { 194 editItems = doMakeEditMenuItems((DescriptorDataSource)dataSource); 195 } else { 196 editItems = doMakeEditMenuItems(); 197 } 198 // Remove any accidental top-level "Edit Formulas" header 199 editItems.removeIf(item -> (item instanceof String) && ((String)item).equalsIgnoreCase("Edit Formulas")); 200 201 // Sort within each group 202 List<Object> sortedGroupedItems = new ArrayList<>(); 203 Object currentGroupHeader = null; 204 List<JMenuItem> currentGroupItems = new ArrayList<>(); 205 206 for (Object item : editItems) { 207 if (item instanceof String) { 208 if (!currentGroupItems.isEmpty()) { 209 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 210 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 211 sortedGroupedItems.addAll(currentGroupItems); 212 currentGroupItems.clear(); 213 } 214 currentGroupHeader = item; 215 } else if (item instanceof JMenuItem) { 216 currentGroupItems.add((JMenuItem)item); 217 } else { 218 if (!currentGroupItems.isEmpty()) { 219 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 220 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 221 sortedGroupedItems.addAll(currentGroupItems); 222 currentGroupItems.clear(); 223 } 224 currentGroupHeader = null; 225 sortedGroupedItems.add(item); 226 } 227 } 228 229 if (!currentGroupItems.isEmpty()) { 230 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 231 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 232 sortedGroupedItems.addAll(currentGroupItems); 233 } 234 235 sortMenuItems(sortedGroupedItems); 236 menuItems.add(makeMenu("Edit Formulas", sortedGroupedItems)); 237 238 menuItems.add(MENU_SEPARATOR); 239 240 menuItem = new JMenuItem("Jython Library"); 241 menuItem.setToolTipText("Open Jython Library window"); 242 menuItem.addActionListener(e -> showJythonEditor()); 243 menuItems.add(menuItem); 244 245 menuItem = new JMenuItem("Jython Shell"); 246 menuItem.setToolTipText("Open Jython Shell window"); 247 menuItem.addActionListener(e -> createShell()); 248 menuItems.add(menuItem); 249 250 menuItem = new JMenuItem("Load Jython Script"); 251 menuItem.setToolTipText("Select a Jython script to run"); 252 menuItem.addActionListener(e -> { 253 FileNameExtensionFilter filter = new FileNameExtensionFilter("Python Files (*.py)", "py"); 254 String file = FileManager.getReadFile("Load Script", filter); 255 createShellWithScript(file); 256 }); 257 258 menuItems.add(menuItem); 259 260 menuItems.add(MENU_SEPARATOR); 261 262 menuItem = new JMenuItem("Import"); 263 menuItem.setToolTipText("Import formulas"); 264 menuItem.addActionListener(e -> importFormulas()); 265 menuItems.add(menuItem); 266 267 menuItem = new JMenuItem("Export"); 268 menuItem.setToolTipText("Export Formulas"); 269 menuItem.addActionListener(e -> exportFormulas()); 270 menuItems.add(menuItem); 271 272 return menuItems; 273 } 274 275 // Recursively sort nested JMenu children 276 private void sortMenuItems(List<?> items) { 277 items.sort((o1, o2) -> { 278 if (o1 instanceof JMenuItem && o2 instanceof JMenuItem) { 279 return ((JMenuItem) o1).getText().compareToIgnoreCase(((JMenuItem) o2).getText()); 280 } 281 return 0; 282 }); 283 284 for (Object item : items) { 285 if (item instanceof JMenu) { 286 JMenu submenu = (JMenu) item; 287 List<JMenuItem> subItems = new ArrayList<>(); 288 for (int i = 0; i < submenu.getItemCount(); i++) { 289 JMenuItem child = submenu.getItem(i); 290 if (child != null) subItems.add(child); 291 } 292 sortMenuItems(subItems); 293 submenu.removeAll(); 294 for (JMenuItem sortedChild : subItems) { 295 submenu.add(sortedChild); 296 } 297 } 298 } 299 } 300 301 /** 302 * Determine if the user should be warned about a potential bug that we've been unable to resolve. 303 * 304 * <p>The conditions for the bug to appear are: 305 * <ul> 306 * <li>In background mode (i.e. running a script).</li> 307 * <li>Geometry by reference is <b>disabled</b>.</li> 308 * <li>New font rendering is <b>enabled</b>.</li> 309 * </ul> 310 * 311 * @return {@code true} if the user's configuration has made it possible for bug to manifest. 312 */ 313 public static boolean shouldWarnImageCapturing() { 314 boolean backgroundMode = McIDASV.getStaticMcv().getArgsManager().isScriptingMode(); 315 boolean shouldWarn = false; 316 OptionMaster optMaster = OptionMaster.getInstance(); 317 BooleanOption useGeometryByRef = optMaster.getBooleanOption("USE_GEOBYREF"); 318 if (useGeometryByRef != null) { 319 shouldWarn = backgroundMode 320 && !Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef")) 321 && Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false")); 322 } 323// System.out.println("background mode: "+backgroundMode); 324// System.out.println("geo by ref: "+Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef"))); 325// System.out.println("new fonts: "+Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false"))); 326// System.out.println("shouldWarn: "+shouldWarn); 327// System.out.println("props:\n"+System.getProperties()); 328// return shouldWarn; 329 return false; 330 } 331 332}