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}