001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2026 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.FileNotFoundException; 036import java.io.FileReader; 037import java.io.IOException; 038import java.util.ArrayList; 039import java.util.List; 040import java.util.Map; 041import java.util.logging.Logger; 042import java.util.Comparator; 043 044import edu.wisc.ssec.mcidasv.startupmanager.options.BooleanOption; 045import org.python.util.PythonInterpreter; 046 047import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster; 048import edu.wisc.ssec.mcidasv.util.CollectionHelpers; 049 050import ucar.unidata.data.DataSource; 051import ucar.unidata.data.DescriptorDataSource; 052import ucar.unidata.idv.IntegratedDataViewer; 053import ucar.unidata.idv.ui.ImageGenerator; 054import ucar.unidata.idv.ui.JythonShell; 055import ucar.unidata.util.FileManager; 056import ucar.unidata.util.Misc; 057 058import javax.swing.*; 059import javax.swing.filechooser.FileNameExtensionFilter; 060 061/** 062 * Overrides the IDV's {@link ucar.unidata.idv.JythonManager JythonManager} to 063 * associate a {@link JythonShell} with a given {@code JythonManager}. 064 */ 065public class JythonManager extends ucar.unidata.idv.JythonManager { 066 067// /** Trusty logging object. */ 068// private static final Logger logger = LoggerFactory.getLogger(JythonManager.class); 069 070 /** Associated Jython Shell. May be {@code null}. */ 071 private JythonShell jythonShell; 072 073 private static final Logger logger = 074 Logger.getLogger(JythonManager.class.getName()); 075 076 /** 077 * Create the manager and call initPython. 078 * 079 * @param idv The IDV. 080 */ 081 public JythonManager(IntegratedDataViewer idv) { 082 super(idv); 083 } 084 085 /** 086 * Create a Jython shell, if one doesn't already exist. This will also 087 * bring the window {@literal "to the front"} of the rest of the McIDAS-V 088 * session. 089 * 090 * @return JythonShell object for interactive Jython usage. 091 */ 092 public JythonShell createShell() { 093 if (jythonShell == null) { 094 jythonShell = new JythonShell(getIdv()); 095 096 } 097 jythonShell.toFront(); 098 return jythonShell; 099 } 100 101 /** 102 * Returns the Jython Shell associated with this {@code JythonManager}. 103 * 104 * @return Jython Shell being used by this manager. May be {@code null}. 105 */ 106 public JythonShell getShell() { 107 return jythonShell; 108 } 109 110 /** 111 * Create and initialize a Jython interpreter. 112 * 113 * @return Newly created Jython interpreter. 114 */ 115 @Override public PythonInterpreter createInterpreter() { 116 PythonInterpreter interpreter = super.createInterpreter(); 117 return interpreter; 118 } 119 120 /** 121 * Removes the given interpreter from the list of active interpreters. 122 * 123 * <p>Also attempts to close any Jython Shell associated with the 124 * interpreter.</p> 125 * 126 * @param interpreter Interpreter to remove. Should not be {@code null}. 127 */ 128 @Override public void removeInterpreter(PythonInterpreter interpreter) { 129 super.removeInterpreter(interpreter); 130 if ((jythonShell != null) && !jythonShell.isShellResetting() && jythonShell.getInterpreter().equals(interpreter)) { 131 jythonShell.close(); 132 jythonShell = null; 133 } 134 } 135 136 /** 137 * Overridden so that McIDAS-V can add an {@code islInterpreter} object 138 * to the interpreter's locals (before executing the contents of {@code}. 139 * 140 * @param code Jython code to evaluate. {@code null} is probably a bad idea. 141 * @param properties {@code String->Object} pairs to insert into the 142 * locals. Parameter may be {@code null}. 143 */ 144 @SuppressWarnings("unchecked") // dealing with idv code that predates generics. 145 @Override public void evaluateTrusted(String code, Map<String, Object> properties) { 146 if (properties == null) { 147 properties = CollectionHelpers.newMap(); 148 } 149 properties.putIfAbsent("islInterpreter", new ImageGenerator(getIdv())); 150 properties.putIfAbsent("_idv", getIdv()); 151 properties.putIfAbsent("idv", getIdv()); 152 super.evaluateTrusted(code, properties); 153 } 154 155 /** 156 * Return the list of menu items to use when the user has clicked on a 157 * formula {@link DataSource}. 158 * 159 * @param dataSource The data source clicked on. 160 * 161 * @return {@link List} of menu items. 162 */ 163 @SuppressWarnings("unchecked") // dealing with idv code that predates generics. 164 @Override public List doMakeFormulaDataSourceMenuItems(DataSource dataSource) { 165 List menuItems = new ArrayList(100); 166 JMenuItem menuItem; 167 168 menuItem = new JMenuItem("Create Formula..."); 169 menuItem.setToolTipText("Open Formula Editor window"); 170 menuItem.addActionListener(e -> showFormulaDialog()); 171 menuItems.add(menuItem); 172 173 List editItems; 174 if (dataSource instanceof DescriptorDataSource) { 175 editItems = doMakeEditMenuItems((DescriptorDataSource)dataSource); 176 } else { 177 editItems = doMakeEditMenuItems(); 178 } 179 // Remove any accidental top-level "Edit Formulas" header 180 editItems.removeIf(item -> (item instanceof String) && ((String)item).equalsIgnoreCase("Edit Formulas")); 181 182 // Sort within each group 183 List<Object> sortedGroupedItems = new ArrayList<>(); 184 Object currentGroupHeader = null; 185 List<JMenuItem> currentGroupItems = new ArrayList<>(); 186 187 for (Object item : editItems) { 188 if (item instanceof String) { 189 if (!currentGroupItems.isEmpty()) { 190 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 191 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 192 sortedGroupedItems.addAll(currentGroupItems); 193 currentGroupItems.clear(); 194 } 195 currentGroupHeader = item; 196 } else if (item instanceof JMenuItem) { 197 currentGroupItems.add((JMenuItem)item); 198 } else { 199 if (!currentGroupItems.isEmpty()) { 200 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 201 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 202 sortedGroupedItems.addAll(currentGroupItems); 203 currentGroupItems.clear(); 204 } 205 currentGroupHeader = null; 206 sortedGroupedItems.add(item); 207 } 208 } 209 210 if (!currentGroupItems.isEmpty()) { 211 currentGroupItems.sort(Comparator.comparing(JMenuItem::getText, String.CASE_INSENSITIVE_ORDER)); 212 if (currentGroupHeader != null) sortedGroupedItems.add(currentGroupHeader); 213 sortedGroupedItems.addAll(currentGroupItems); 214 } 215 216 sortMenuItems(sortedGroupedItems); 217 menuItems.add(makeMenu("Edit Formulas...", sortedGroupedItems)); 218 219 menuItems.add(MENU_SEPARATOR); 220 221 menuItem = new JMenuItem("Jython Library..."); 222 menuItem.setToolTipText("Open Jython Library window"); 223 menuItem.addActionListener(e -> showJythonEditor()); 224 menuItems.add(menuItem); 225 226 menuItem = new JMenuItem("Jython Shell..."); 227 menuItem.setToolTipText("Open Jython Shell window"); 228 menuItem.addActionListener(e -> createShell()); 229 menuItems.add(menuItem); 230 231 // McIDAS Inquiry #2701-3141 232 menuItem = new JMenuItem("Load Jython Script..."); 233 menuItem.setToolTipText("Select a Jython script to load"); 234 menuItem.addActionListener(e -> { 235 createShell(); 236 jythonShell.loadScript(); 237 }); 238 menuItems.add(menuItem); 239 240 menuItem = new JMenuItem("Run Jython Script..."); 241 menuItem.setToolTipText("Select a Jython script to run"); 242 menuItem.addActionListener(e -> { 243 createShell(); 244 jythonShell.loadAndRunScript(); 245 }); 246 menuItems.add(menuItem); 247 248 menuItems.add(MENU_SEPARATOR); 249 250 menuItem = new JMenuItem("Import..."); 251 menuItem.setToolTipText("Import formulas"); 252 menuItem.addActionListener(e -> importFormulas()); 253 menuItems.add(menuItem); 254 255 menuItem = new JMenuItem("Export..."); 256 menuItem.setToolTipText("Export Formulas"); 257 menuItem.addActionListener(e -> exportFormulas()); 258 menuItems.add(menuItem); 259 260 menuItems.add(MENU_SEPARATOR); 261 262 menuItem = new JMenuItem("Shapefile to GeoJSON..."); 263 menuItem.setToolTipText("Convert Shapefile to a GeoJSON file"); 264 menuItem.addActionListener(e -> { 265 logger.info("did we do this?"); 266 String filePath = ShapefileToGeoJSON.getShapeFile(); 267 String jsonPath = filePath.replaceFirst("\\.shp$", ".json"); 268 boolean a = ShapefileToGeoJSON.convert(filePath, jsonPath); 269 JOptionPane.showMessageDialog(null, "Saved as " + jsonPath); 270 }); 271 menuItems.add(menuItem); 272 273 return menuItems; 274 } 275 276 // Recursively sort nested JMenu children 277 private void sortMenuItems(List<?> items) { 278 items.sort((o1, o2) -> { 279 if (o1 instanceof JMenuItem && o2 instanceof JMenuItem) { 280 return ((JMenuItem) o1).getText().compareToIgnoreCase(((JMenuItem) o2).getText()); 281 } 282 return 0; 283 }); 284 285 for (Object item : items) { 286 if (item instanceof JMenu) { 287 JMenu submenu = (JMenu) item; 288 List<JMenuItem> subItems = new ArrayList<>(); 289 for (int i = 0; i < submenu.getItemCount(); i++) { 290 JMenuItem child = submenu.getItem(i); 291 if (child != null) subItems.add(child); 292 } 293 sortMenuItems(subItems); 294 submenu.removeAll(); 295 for (JMenuItem sortedChild : subItems) { 296 submenu.add(sortedChild); 297 } 298 } 299 } 300 } 301 302 /** 303 * Determine if the user should be warned about a potential bug that we've been unable to resolve. 304 * 305 * <p>The conditions for the bug to appear are: 306 * <ul> 307 * <li>In background mode (i.e. running a script).</li> 308 * <li>Geometry by reference is <b>disabled</b>.</li> 309 * <li>New font rendering is <b>enabled</b>.</li> 310 * </ul> 311 * 312 * @return {@code true} if the user's configuration has made it possible for bug to manifest. 313 */ 314 public static boolean shouldWarnImageCapturing() { 315 boolean backgroundMode = McIDASV.getStaticMcv().getArgsManager().isScriptingMode(); 316 boolean shouldWarn = false; 317 OptionMaster optMaster = OptionMaster.getInstance(); 318 BooleanOption useGeometryByRef = optMaster.getBooleanOption("USE_GEOBYREF"); 319 if (useGeometryByRef != null) { 320 shouldWarn = backgroundMode 321 && !Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef")) 322 && Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false")); 323 } 324// System.out.println("background mode: "+backgroundMode); 325// System.out.println("geo by ref: "+Boolean.parseBoolean(System.getProperty("visad.java3d.geometryByRef"))); 326// System.out.println("new fonts: "+Boolean.parseBoolean(System.getProperty(PROP_HIQ_FONT_RENDERING, "false"))); 327// System.out.println("shouldWarn: "+shouldWarn); 328// System.out.println("props:\n"+System.getProperties()); 329// return shouldWarn; 330 return false; 331 } 332 333}