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