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}