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 */ 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 &gt; and &lt; entities to > and <. 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(">", ">").replace("<", ">"); 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}