001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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 */
028
029package edu.wisc.ssec.mcidasv.ui;
030
031import java.awt.Component;
032import java.awt.event.ActionEvent;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036
037import javax.swing.ImageIcon;
038import javax.swing.JComponent;
039import javax.swing.event.HyperlinkEvent;
040import javax.swing.event.HyperlinkListener;
041import javax.swing.event.HyperlinkEvent.EventType;
042
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045import org.w3c.dom.Element;
046import org.w3c.dom.NamedNodeMap;
047import org.w3c.dom.Node;
048import org.w3c.dom.NodeList;
049
050import ucar.unidata.idv.IntegratedDataViewer;
051import ucar.unidata.idv.ViewManager;
052import ucar.unidata.idv.ui.IdvComponentGroup;
053import ucar.unidata.idv.ui.IdvComponentHolder;
054import ucar.unidata.idv.ui.IdvUIManager;
055import ucar.unidata.idv.ui.IdvWindow;
056import ucar.unidata.idv.ui.IdvXmlUi;
057import ucar.unidata.ui.ComponentHolder;
058import ucar.unidata.ui.HtmlComponent;
059import ucar.unidata.util.GuiUtils;
060import ucar.unidata.util.IOUtil;
061import ucar.unidata.xml.XmlUtil;
062
063import edu.wisc.ssec.mcidasv.util.TreePanel;
064
065/**
066 * <p>
067 * McIDAS-V mostly extends this class to preempt the IDV. McIDAS-V needs to
068 * control some HTML processing, ensure that
069 * {@link McvComponentGroup McvComponentGroups} and
070 * {@link McvComponentHolder McvComponentHolders} are created, and handle some
071 * special problems that occur when attempting to load bundles that do not
072 * contain component groups.
073 * </p>
074 */
075@SuppressWarnings("unchecked") 
076public class McIDASVXmlUi extends IdvXmlUi {
077    
078    /** Logging object. */
079    private static final Logger logger = 
080        LoggerFactory.getLogger(McIDASVXmlUi.class);
081    
082    /** Maps a {@code String} ID to an {@link Element}. */
083    private Map<String, Element> idToElement;
084    
085    /** Avoids unneeded getIdv() calls. */
086    private IntegratedDataViewer idv;
087    
088    /** See {@link #getElapsedGuiTime()}. */
089    private final Map<String, Long> xmlUiTimes = 
090        new HashMap<>(1024);
091    
092    /**
093     * Keep around a reference to the window we were built for, useful for
094     * associated component groups with the appropriate window.
095     */
096    private IdvWindow window;
097
098    public McIDASVXmlUi(IntegratedDataViewer idv, Element root) {
099        super(idv, root);
100        if (idToElement == null) {
101            idToElement = new HashMap<>();
102        }
103    }
104
105    public McIDASVXmlUi(IdvWindow window, List viewManagers,
106        IntegratedDataViewer idv, Element root) 
107    {
108        super(window, viewManagers, idv, root);
109        this.idv = idv;
110        this.window = window;
111        if (idToElement == null) {
112            idToElement = new HashMap<>();
113        }
114    }
115
116    /**
117     * Convert the &amp;gt; and &amp;lt; entities to &gt; and &lt;.
118     * 
119     * @param text The text you'd like to convert.
120     * 
121     * @return The converted text!
122     */
123    private static String decodeHtml(String text) {
124        return text.replace("&gt", ">").replace("&lt;", ">");
125    }
126
127    /**
128     * Add the component.
129     * 
130     * @param id id
131     * @param component component
132     */
133    @Override public void addComponent(String id, Element component) {
134        // this needs to be here because even if you create idToElement in the
135        // constructor, this method will get called from 
136        // ucar.unidata.xml.XmlUi#initialize(Element) before control has 
137        // returned to the McIDASVXmlUi constructor!
138        if (idToElement == null) {
139            idToElement = new HashMap<>();
140        }
141        super.addComponent(id, component);
142        idToElement.put(id, component);
143    }
144
145    /**
146     * Overridden so that any attempts to generate
147     * {@link IdvComponentGroup IdvComponentGroups} or
148     * {@link IdvComponentHolder IdvComponentHolders} will return the
149     * respective McIDAS-V equivalents.
150     * 
151     * <p>
152     * It makes things like the draggable tabs possible.
153     * </p>
154     * 
155     * @param node XML representation of the desired component group.
156     * 
157     * @return An {@code McvComponentGroup} based upon the contents of {@code node}.
158     */
159    @Override protected IdvComponentGroup makeComponentGroup(Element node) {
160        McvComponentGroup group = new McvComponentGroup(idv, "", window);
161        group.initWith(node);
162
163        NodeList elements = XmlUtil.getElements(node);
164        for (int i = 0; i < elements.getLength(); i++) {
165            Element child = (Element)elements.item(i);
166
167            String tag = child.getTagName();
168
169            if (tag.equals(IdvUIManager.COMP_MAPVIEW)
170                || tag.equals(IdvUIManager.COMP_VIEW)) 
171            {
172                ViewManager viewManager = getViewManager(child);
173                group.addComponent(new McvComponentHolder(idv, viewManager));
174            } 
175            else if (tag.equals(IdvUIManager.COMP_COMPONENT_CHOOSERS)) {
176                IdvComponentHolder comp =
177                    new McvComponentHolder(idv, "choosers");
178                comp.setType(IdvComponentHolder.TYPE_CHOOSERS);
179                comp.setName(XmlUtil.getAttribute(child, "name", "Choosers"));
180                group.addComponent(comp);
181            } 
182            else if (tag.equals(IdvUIManager.COMP_COMPONENT_SKIN)) {
183                IdvComponentHolder comp = new McvComponentHolder(idv, 
184                    XmlUtil.getAttribute(child, "url"));
185
186                comp.setType(IdvComponentHolder.TYPE_SKIN);
187                comp.setName(XmlUtil.getAttribute(child, "name", "UI"));
188                group.addComponent(comp);
189            } 
190            else if (tag.equals(IdvUIManager.COMP_COMPONENT_HTML)) {
191                String text = XmlUtil.getChildText(child);
192                text = new String(XmlUtil.decodeBase64(text.trim()));
193                ComponentHolder comp = new HtmlComponent("Html Text", text);
194                comp.setShowHeader(false);
195                comp.setName(XmlUtil.getAttribute(child, "name", "HTML"));
196                group.addComponent(comp);
197            } 
198            else if (tag.equals(IdvUIManager.COMP_DATASELECTOR)) {
199                group.addComponent(new McvComponentHolder(idv,
200                    idv.getIdvUIManager().createDataSelector(false, false)));
201            } 
202            else if (tag.equals(IdvUIManager.COMP_COMPONENT_GROUP)) {
203                group.addComponent(makeComponentGroup(child));
204            } 
205            else {
206                System.err.println("Unknown component element:"
207                                   + XmlUtil.toString(child));
208            }
209        }
210        return group;
211    }
212
213    /**
214     * McIDAS-V overrides this so that it can seize control of some HTML
215     * processing in addition to attempting to associate newly-created
216     * {@link ViewManager ViewManagers} with ViewManagers found in a bundle.
217     * 
218     * <p>
219     * The latter is done so that McIDAS-V can load bundles that do not use
220     * component groups. A {@literal "dynamic skin"} is built with ViewManagers
221     * for each ViewManager in the bundle. The {@literal "viewid"} attribute of
222     * the dynamic skin ViewManager is the name of the
223     * {@link ucar.unidata.idv.ViewDescriptor} from the bundled ViewManager.
224     * {@code createViewManager()} is used to actually associate the new
225     * ViewManager with its bundled ViewManager.
226     * </p>
227     * 
228     * @param node The XML describing the component to be created.
229     * @param id ID of {@code node}.
230     * 
231     * @return The {@link java.awt.Component} described by {@code node}.
232     *
233     * @see edu.wisc.ssec.mcidasv.ui.McIDASVXmlUi#createViewManager(Element)
234     */
235    @Override public Component createComponent(Element node, String id) {
236        long start = System.nanoTime();
237        Component comp = null;
238        String tagName = node.getTagName();
239        if (tagName.equals(TAG_HTML)) {
240            String text = getAttr(node, ATTR_TEXT, NULLSTRING);
241            text = decodeHtml(text);
242            if (text == null) {
243                String url = getAttr(node, ATTR_URL, NULLSTRING);
244                if (url != null) {
245                    text = IOUtil.readContents(url, (String)null);
246                }
247                if (text == null) {
248                    text = XmlUtil.getChildText(node);
249                }
250            }
251            HyperlinkListener linkListener = new HyperlinkListener() {
252                public void hyperlinkUpdate(HyperlinkEvent e) {
253                    if (e.getEventType() != EventType.ACTIVATED) {
254                        return;
255                    }
256                    String url;
257                    if (e.getURL() == null) {
258                        url = e.getDescription();
259                    } else {
260                        url = e.getURL().toString();
261                    }
262                    actionPerformed(new ActionEvent(this, 0, url));
263                }
264            };
265            Component[] comps =
266                GuiUtils.getHtmlComponent(text, linkListener, getAttr(node,
267                    ATTR_WIDTH, 200), getAttr(node, ATTR_HEIGHT, 200));
268            comp = comps[1];
269        } else if (tagName.equals(UIManager.COMP_MAPVIEW)
270                   || tagName.equals(UIManager.COMP_VIEW)) {
271
272            // if we're creating a VM for a dynamic skin that was created for
273            // a bundle, createViewManager() will return the bundled VM.
274            ViewManager vm = createViewManager(node);
275            if (vm != null) {
276                comp = vm.getContents();
277            } else {
278                comp = super.createComponent(node, id);
279            }
280        } else if (tagName.equals(TAG_TREEPANEL)) {
281            comp = createTreePanel(node, id);
282        } else {
283            comp = super.createComponent(node, id);
284        }
285        long stop = System.nanoTime();
286        
287        // trying to get an *idea* of which parts of mcv are slow
288        // JMH is the correct approach
289        logger.trace("xmlui '{}' component '{}': took {} ms to finish",
290                     Integer.toHexString(hashCode()),
291                     id,
292                     String.format("%.2f", (stop - start) / 1.0e6));
293        
294        if (xmlUiTimes.containsKey(id)) {
295            xmlUiTimes.put(id, xmlUiTimes.get(id) + (stop - start));
296        } else {
297            xmlUiTimes.put(id, stop - start);
298        }
299        return comp;
300    }
301    
302    /**
303     * Return the total amount of time spent in 
304     * {@link #createComponent(Element, String)}.
305     * 
306     * <p>Be aware that each McV {@link IdvWindow window} should have its own 
307     * {@code McIDASXmlUi} instance, so in order to determine the total time, 
308     * iterate over the results from {@link IdvWindow#getWindows()}.</p>
309     *
310     * <p><b>The elapsed time is merely a quick estimate.</b> The only way to
311     * obtain accurate timing information with the JVM is using
312     * <a href="https://openjdk.java.net/projects/code-tools/jmh/">JMH</a>.</p>
313     * 
314     * @return Nanoseconds spent creating GUI components in this window.
315     */
316    public long getElapsedGuiTime() {
317        return xmlUiTimes.values().stream().mapToLong(l -> l).sum();
318    }
319    
320    /**
321     * <p>
322     * Attempts to build a {@link ucar.unidata.idv.ViewManager} based upon
323     * {@code node}. If the XML has a {@literal "viewid"} attribute, the
324     * value will be used to search for a ViewManager that has been cached by
325     * the McIDAS-V {@link UIManager}. If the UIManager has a matching
326     * ViewManager, we'll use the cached ViewManager to initialize a
327     * {@literal "blank"} ViewManager. The cached ViewManager is then removed
328     * from the cache and deleted. This method will return {@code null} if
329     * no cached ViewManager was found.
330     * </p>
331     * 
332     * <p>
333     * The ViewManager {@literal "cache"} will only contain bundled ViewManagers
334     * that were not held in a component holder. This means that any 
335     * ViewManager returned was created for a dynamic skin, but initialized 
336     * with the contents of the corresponding bundled ViewManager.
337     * </p>
338     * 
339     * @param node XML description of the ViewManager that needs building.
340     * 
341     * @return {@code null} if there was no cached ViewManager, otherwise a
342     * {@code ViewManager} that has been initialized with a bundled ViewManager.
343     */
344    private ViewManager createViewManager(final Element node) {
345        final String viewId = getAttr(node, "viewid", NULLSTRING);
346        ViewManager vm = null;
347        if (viewId != null) {
348            ViewManager old = UIManager.savedViewManagers.remove(viewId);
349            if (old != null) {
350                vm = getViewManager(node);
351                vm.initWith(old);
352                old.destroy();
353            }
354        }
355        return vm;
356    }
357
358    private TreePanel createTreePanel(final Element node, final String id) {
359
360        TreePanel treePanel = 
361            new TreePanel(getAttr(node, ATTR_USESPLITPANE, false), 
362                getAttr(node, ATTR_TREEWIDTH, -1));
363
364        List<Element> kids = XmlUtil.getListOfElements(node);
365
366        for (Element kid : kids) {
367            Component comp = xmlToUi(kid);
368            if (comp == null) {
369                continue;
370            }
371
372            String label = getAttr(kid, ATTR_TITLE, "");
373
374            ImageIcon icon = getAttr(kid, ATTR_ICON, (ImageIcon)null);
375            String cat = getAttr(kid, ATTR_CATEGORY, (String)null);
376            if (XmlUtil.getAttribute(kid, ATTR_CATEGORYCOMPONENT, false)) {
377                treePanel.addCategoryComponent(cat, (JComponent)comp);
378            } else {
379                treePanel.addComponent((JComponent)comp, cat, label, icon);
380            }
381        }
382        treePanel.closeAll();
383        treePanel.showPersistedSelection();
384        return treePanel;
385    }
386
387    /**
388     * The xml nodes can contain an idref field. If so this returns the
389     * node that that id defines
390     * 
391     * @param node node
392     * 
393     * @return The node or the referenced node
394     */
395    private Element getReffedNode(Element node) {
396        String idRef = getAttr(node, ATTR_IDREF, NULLSTRING);
397        if (idRef == null) {
398            return node;
399        }
400        
401        Element reffedNode = idToElement.get(idRef);
402        if (reffedNode == null) {
403            throw new IllegalStateException("Could not find idref=" + idRef);
404        }
405
406        // TODO(unidata): Make a new copy of the node 
407        // reffedNode = reffedNode.copy();
408        NamedNodeMap map = node.getAttributes();
409        for (int i = 0; i < map.getLength(); i++) {
410            Node n = map.item(i);
411            if (!n.getNodeName().equals(ATTR_IDREF)) {
412                reffedNode.setAttribute(n.getNodeName(), n.getNodeValue());
413            }
414        }
415        return reffedNode;
416    }
417}