001/* An Action that works with BasicGraphFrame to export HTML. 002 003 Copyright (c) 1998-2016 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 */ 027package ptolemy.vergil.basic.export.html; 028 029import java.awt.Color; 030import java.awt.event.ActionEvent; 031import java.awt.geom.AffineTransform; 032import java.awt.geom.Rectangle2D; 033import java.awt.print.PrinterException; 034import java.io.BufferedReader; 035import java.io.File; 036import java.io.FileOutputStream; 037import java.io.FileReader; 038import java.io.FileWriter; 039import java.io.IOException; 040import java.io.OutputStream; 041import java.io.PrintWriter; 042import java.io.Writer; 043import java.net.URL; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.Iterator; 047import java.util.LinkedList; 048import java.util.List; 049import java.util.Locale; 050import java.util.Map; 051import java.util.Set; 052 053import javax.swing.AbstractAction; 054import javax.swing.SwingUtilities; 055 056import diva.canvas.Figure; 057import diva.canvas.JCanvas; 058import diva.graph.GraphController; 059import ptolemy.actor.CompositeActor; 060import ptolemy.actor.Manager; 061import ptolemy.actor.TypedActor; 062import ptolemy.actor.gui.BrowserEffigy; 063import ptolemy.actor.gui.Configuration; 064import ptolemy.actor.gui.EditParametersDialog; 065import ptolemy.actor.gui.Effigy; 066import ptolemy.actor.gui.PtolemyEffigy; 067import ptolemy.actor.gui.PtolemyFrame; 068import ptolemy.actor.gui.Tableau; 069import ptolemy.data.expr.StringParameter; 070import ptolemy.domains.modal.kernel.State; 071import ptolemy.kernel.CompositeEntity; 072import ptolemy.kernel.Entity; 073import ptolemy.kernel.attributes.URIAttribute; 074import ptolemy.kernel.attributes.VersionAttribute; 075import ptolemy.kernel.util.Attribute; 076import ptolemy.kernel.util.IllegalActionException; 077import ptolemy.kernel.util.InternalErrorException; 078import ptolemy.kernel.util.KernelException; 079import ptolemy.kernel.util.Locatable; 080import ptolemy.kernel.util.NameDuplicationException; 081import ptolemy.kernel.util.NamedObj; 082import ptolemy.util.CancelException; 083import ptolemy.util.FileUtilities; 084import ptolemy.util.MessageHandler; 085import ptolemy.util.StringUtilities; 086import ptolemy.vergil.actor.ActorGraphTableau; 087import ptolemy.vergil.basic.BasicGraphFrame; 088import ptolemy.vergil.basic.ExportParameters; 089import ptolemy.vergil.basic.HTMLExportable; 090import ptolemy.vergil.basic.export.web.DefaultIconLink; 091import ptolemy.vergil.basic.export.web.DefaultIconScript; 092import ptolemy.vergil.basic.export.web.WebAttribute; 093import ptolemy.vergil.basic.export.web.WebElement; 094import ptolemy.vergil.basic.export.web.WebExportParameters; 095import ptolemy.vergil.basic.export.web.WebExportable; 096import ptolemy.vergil.basic.export.web.WebExporter; 097 098/** An Action that works with BasicGraphFrame to export HTML. 099 * Given a directory, this action creates an image of the 100 * currently visible portion of the BasicGraphFrame and an 101 * HTML page that displays that image. In addition, it 102 * creates a map of the locations of actors in the image 103 * and actions associated with each of the actors. 104 * The default content of the web page and the actions 105 * associated with the image map are defined by instances 106 * of {@link WebExportable} that have been inserted at 107 * the top level of the current {@link Configuration}. 108 * The model may customize both the web page content and 109 * the actions in the image map by inserting into the model 110 * instances of {@link WebExportable}. 111 * <p> 112 * If the model contains an instance of 113 * {@link WebExportParameters}, then that instance 114 * defines parameters of the export. If not, but 115 * the current configuration contains one, then that 116 * instance defines the the parameters. Otherwise, 117 * the defaults in {@link WebExportParameters} 118 * are used. 119 * 120 * <p>The following JVM properties affect the output:</p> 121 * <dl> 122 * <dt> -Dptolemy.ptII.exportHTML.usePtWebsite=true</dt> 123 * <dd> Include Ptolemy Website (<a href="http://ptolemy.org#in_browser" target="_top">http://ptolemy.org</a>) 124 * specific Side Includes (SSI) and use JavaScript libraries from the 125 * Ptolemy website.</dd> 126 * <dt> -Dptolemy.ptII.exportHTML.linkToJNLP=true</dt> 127 * <dd> Include a link to the a <code><i>sanitizedModelName</i>.jnlp</code> file.</dd> 128 * </dl> 129 * 130 * <p>Typically, JVM properties are set when Java is invoked. 131 * {@link ptolemy.vergil.basic.export.ExportModel} can be called with these 132 * properties set to create Ptolemy website specific web pages.</p> 133 * 134 * <p> See <a href="https://wiki.eecs.berkeley.edu/ptexternal/Main/Main/HTMLExport#in_browser" target="_top">https://wiki.eecs.berkeley.edu/ptexternal/Main/Main/HTMLExport</a> 135 * for detailed instructions about how to create web pages on the 136 * Ptolemy website for models.</p> 137 * 138 * @author Christopher Brooks and Edward A. Lee 139 * @version $Id$ 140 * @since Ptolemy II 10.0 141 * @Pt.ProposedRating Yellow (eal) 142 * @Pt.AcceptedRating Red (eal) 143 */ 144@SuppressWarnings("serial") 145public class ExportHTMLAction extends AbstractAction 146 implements HTMLExportable, WebExporter { 147 148 /** Create a new action to export HTML. 149 * @param basicGraphFrame The Vergil window to export. 150 */ 151 public ExportHTMLAction(BasicGraphFrame basicGraphFrame) { 152 super("Export to Web"); 153 _basicGraphFrame = basicGraphFrame; 154 putValue("tooltip", "Export HTML and image files showing this model."); 155 // putValue(GUIUtilities.MNEMONIC_KEY, Integer.valueOf(KeyEvent.VK_G)); 156 } 157 158 /////////////////////////////////////////////////////////////////// 159 //// public methods //// 160 161 /** Export a web page. 162 * @param event The event that triggered this action. 163 */ 164 @Override 165 public void actionPerformed(ActionEvent event) { 166 NamedObj model = _basicGraphFrame.getModel(); 167 WebExportParameters defaultParameters = null; 168 try { 169 List<WebExportParameters> defaultParameterList = model 170 .attributeList(WebExportParameters.class); 171 if (defaultParameterList == null 172 || defaultParameterList.size() == 0) { 173 defaultParameterList = _basicGraphFrame.getConfiguration() 174 .attributeList(WebExportParameters.class); 175 if (defaultParameterList == null 176 || defaultParameterList.size() == 0) { 177 defaultParameters = new WebExportParameters(model, 178 model.uniqueName("_defaultWebExportParameters")); 179 // We want this new attribute to look as if it were part of 180 // its container's class definition so that it does not get 181 // exported to MoML unless it changes in some way, e.g. one 182 // of the parameter values it contains changes. 183 defaultParameters.setDerivedLevel(1); 184 } 185 } 186 if (defaultParameters == null) { 187 defaultParameters = defaultParameterList.get(0); 188 } 189 EditParametersDialog dialog = new EditParametersDialog( 190 _basicGraphFrame, defaultParameters, 191 "Export to Web for " + model.getName()); 192 if (!dialog.buttonPressed().equals("Commit")) { 193 return; 194 } 195 196 ExportParameters parameters = defaultParameters 197 .getExportParameters(); 198 // Set the copy directory target to null to indicate that no copying 199 // of files has happened. 200 parameters.setJSCopier(null); 201 exportToWeb(_basicGraphFrame, parameters); 202 } catch (KernelException ex) { 203 MessageHandler.error("Unable to export HTML.", ex); 204 } 205 } 206 207 /** If parameters.copyJavaScriptFiles is true and the Java 208 * property ptolemy.ptII.exportHTML.usePtWebsite is false, 209 * then copy the required JavaScript files into the target directory 210 * given in the parameters argument. 211 * @param graphFrame The frame being exported. 212 * @param parameters The export parameters. 213 * @return False if something went wrong and the user requested 214 * canceling the export. True otherwise. 215 */ 216 public static boolean copyJavaScriptFilesIfNeeded( 217 final BasicGraphFrame graphFrame, 218 final ExportParameters parameters) { 219 // First, if appropriate, copy needed files. 220 boolean usePtWebsite = Boolean.valueOf(StringUtilities 221 .getProperty("ptolemy.ptII.exportHTML.usePtWebsite")); 222 usePtWebsite = usePtWebsite || parameters.usePtWebsite; 223 if (parameters.copyJavaScriptFiles && !usePtWebsite) { 224 // Copy Javascript source files into destination directory, 225 // if they are available. The files are under an MIT license, 226 // which is compatible with the Ptolemy license. 227 // For jquery, we could use a CDS (content delivery service) instead 228 // of copying the file. 229 String jsDirectoryName = "$CLASSPATH/ptolemy/vergil/basic/export/html/javascript/"; 230 File jsDirectory = FileUtilities.nameToFile(jsDirectoryName, null); 231 // We assume that if the directory exists, then the files exist. 232 if (jsDirectory.isDirectory()) { 233 // Copy files into the "javascript" directory. 234 File jsTargetDirectory = new File( 235 parameters.directoryToExportTo, "javascript"); 236 if (jsTargetDirectory.exists() 237 && !jsTargetDirectory.isDirectory()) { 238 File jsBackupDirectory = new File( 239 parameters.directoryToExportTo, "javascript.bak"); 240 if (!jsTargetDirectory.renameTo(jsBackupDirectory)) { 241 // It is ok to ignore this. 242 System.out.println("Failed to rename \"" 243 + jsTargetDirectory + "\" to \"" 244 + jsBackupDirectory + "\""); 245 } 246 } 247 248 if (!jsTargetDirectory.exists() && !jsTargetDirectory.mkdir()) { 249 try { 250 MessageHandler.warning( 251 "Warning: Cannot find required JavaScript, CSS, and image files" 252 + " for lightbox effect implemented by the fancybox" 253 + " package. Perhaps your Ptolemy II" 254 + " installation does not include them." 255 + " Will use the files on ptolemy.org."); 256 } catch (CancelException e) { 257 // Cancel the action. 258 return false; 259 } 260 parameters.copyJavaScriptFiles = false; 261 } else { 262 // If deleteFilesOnExit is selected, mark the new 263 // Javscript directory for deletion. Mark it first so 264 // that it will be deleted after its contained files have 265 // been deleted. Files/directories are deleted in the 266 // reverse order that they are registered. 267 if (parameters.deleteFilesOnExit) { 268 jsTargetDirectory.deleteOnExit(); 269 } 270 271 // Copy css, JavaScript, and image files. 272 for (String filename : FILENAMES) { 273 try { 274 URL lightboxFile = FileUtilities.nameToURL( 275 jsDirectoryName + filename, null, null); 276 File file = new File(jsTargetDirectory, filename); 277 if (parameters.deleteFilesOnExit) { 278 file.deleteOnExit(); 279 } 280 FileUtilities.binaryCopyURLToFile(lightboxFile, 281 file); 282 } catch (IOException e) { 283 try { 284 MessageHandler.warning( 285 "Warning: failed to copy required files." 286 + " Use the files on ptolemy.org? " 287 + e.getMessage()); 288 } catch (CancelException e1) { 289 // Cancel the action. 290 return false; 291 } 292 parameters.copyJavaScriptFiles = false; 293 } 294 } 295 parameters.setJSCopier(graphFrame.getModel()); 296 } 297 } 298 } 299 return true; 300 } 301 302 /** Define an attribute to be included in the HTML area element 303 * corresponding to the region of the image map covered by 304 * the specified object. For example, if an <i>attribute</i> "href" 305 * is added, where the <i>value</i> is a URI, then the 306 * area in the image map for the specified object will include 307 * a hyperlink to the specified URI. If the specified object 308 * already has a value for the specified attribute, then 309 * the previous value is replaced by the new one. 310 * If the specified attribute is "default", then all attributes 311 * associated with the object are cleared. 312 * <p> 313 * This method is a callback method that may be performed 314 * by attributes of class 315 * {@link ptolemy.vergil.basic.export.web.WebExportable} 316 * when their 317 * {@link ptolemy.vergil.basic.export.web.WebExportable#provideContent(WebExporter)} 318 * method is called by this exporter.</p> 319 * 320 * @param webAttribute The attribute to be included. 321 * @param overwrite If true, overwrite any previously defined value for 322 * the specified attribute. If false, then do nothing if there is already 323 * an attribute with the specified name. 324 * @return True if the specified attribute and value was defined (i.e., 325 * if there was a previous value, it was overwritten). 326 */ 327 @Override 328 public boolean defineAttribute(WebAttribute webAttribute, 329 boolean overwrite) { 330 if (webAttribute.getContainer() != null) { 331 NamedObj object = webAttribute.getContainer(); 332 HashMap<String, String> areaTable = _areaAttributes.get(object); 333 if (areaTable == null) { 334 // No previously defined table. Add one. 335 areaTable = new HashMap<String, String>(); 336 _areaAttributes.put(object, areaTable); 337 } 338 if (overwrite || areaTable.get(webAttribute.getWebName()) == null) { 339 areaTable.put(webAttribute.getWebName(), 340 _escapeString(webAttribute.getExpression())); 341 return true; 342 } 343 } 344 return false; 345 } 346 347 /** Define an element. 348 * If <i>onceOnly</i> is true, then if identical content has 349 * already been added to the specified position, then it is not 350 * added again. 351 * @param webElement The element. 352 * @param onceOnly True to prevent duplicate content. 353 */ 354 @Override 355 public void defineElement(WebElement webElement, boolean onceOnly) { 356 357 List<StringBuffer> contents = _contents.get(webElement.getParent()); 358 if (contents == null) { 359 contents = new LinkedList<StringBuffer>(); 360 _contents.put(webElement.getParent(), contents); 361 } 362 StringBuffer webElementBuffer = new StringBuffer( 363 webElement.getExpression()); 364 // Check to see whether contents are already present. 365 if (onceOnly) { 366 // FIXME: Will List.contains() work if two StringBuffers 367 // are constructed from the same String? 368 if (contents.contains(webElementBuffer)) { 369 return; 370 } 371 } 372 contents.add(new StringBuffer(webElementBuffer)); 373 } 374 375 /** Export an HTML page and associated subpages for the specified 376 * graph frame as given by the parameters. After setting everything 377 * up, this method will delegate to the {@link BasicGraphFrame#writeHTML(ExportParameters, Writer)} 378 * method, which in turn will delegate back to an instance of this class, ExportHTMLAction. 379 * <p> 380 * This method should be invoked in the swing thread. 381 * It will invoke a separate thread to run the model (if so 382 * specified in the parameters). 383 * When that thread completes the run, it will delegate 384 * back to the swing thread to do the export. 385 * Note that this method will return before the export 386 * is completed. If another thread needs to wait for 387 * this complete, then it can call {@link #waitForExportToComplete()}. 388 * This is synchronized to ensure that only one export can be in progress at a time. 389 * </p> 390 * @param graphFrame The frame containing a model to export. 391 * @param parameters The parameters that control the export. 392 * making the exported web page independent of the ptolemy.org site. 393 */ 394 public static synchronized void exportToWeb( 395 final BasicGraphFrame graphFrame, 396 final ExportParameters parameters) { 397 try { 398 399 if (parameters.directoryToExportTo == null) { 400 MessageHandler.error("No directory specified."); 401 return; 402 } 403 404 // See whether the directory has a file called index.html. 405 final File indexFile = new File(parameters.directoryToExportTo, 406 "index.html"); 407 if (parameters.directoryToExportTo.exists()) { 408 // Previously, if directory existed and was a directory, we would always pop 409 // up a dialog stating that the directory existed and that the contents would 410 // be overwritten. This seems excessive because the dialog will always 411 // be shown. 412 if (indexFile.exists()) { 413 if (!MessageHandler.yesNoQuestion("\"" 414 + parameters.directoryToExportTo 415 + "\" exists and contains an index.html file. Overwrite contents?")) { 416 MessageHandler.message("HTML export canceled."); 417 return; 418 } 419 } 420 if (!parameters.directoryToExportTo.isDirectory()) { 421 if (!MessageHandler.yesNoQuestion("\"" 422 + parameters.directoryToExportTo 423 + "\" is a file, not a directory. Delete the file named \"" 424 + parameters.directoryToExportTo 425 + "\" and create a directory with that name?")) { 426 MessageHandler.message("HTML export canceled."); 427 return; 428 } 429 if (!parameters.directoryToExportTo.delete()) { 430 MessageHandler.message("Unable to delete file \"" 431 + parameters.directoryToExportTo + "\"."); 432 return; 433 } 434 if (!parameters.directoryToExportTo.mkdir()) { 435 MessageHandler.message("Unable to create directory \"" 436 + parameters.directoryToExportTo + "\"."); 437 return; 438 } 439 } 440 } else { 441 if (!parameters.directoryToExportTo.mkdir()) { 442 MessageHandler.message("Unable to create directory \"" 443 + parameters.directoryToExportTo + "\"."); 444 return; 445 } 446 } 447 // We now have a directory and permission to write to it. 448 if (!copyJavaScriptFilesIfNeeded(graphFrame, parameters)) { 449 // Canceled the export. 450 return; 451 } 452 453 // Using a null here causes the write to occur to an index.html file. 454 final PrintWriter writer = null; 455 456 openRunAndWriteHTML(graphFrame, parameters, indexFile, writer, 457 false); 458 } catch (Exception ex) { 459 MessageHandler.error("Unable to export to web.", ex); 460 throw new RuntimeException(ex); 461 } 462 } 463 464 /** During invocation of {@link #writeHTML(ExportParameters, Writer)}, 465 * return the parameters being used. 466 * @return The parameters of the current export, or null if there 467 * is not one in progress. 468 */ 469 @Override 470 public ExportParameters getExportParameters() { 471 return _parameters; 472 } 473 474 /** The frame (window) being exported to HTML. 475 * @return The frame provided to the constructor. 476 */ 477 @Override 478 public PtolemyFrame getFrame() { 479 return _basicGraphFrame; 480 } 481 482 /** Depending on the export parameters (see {@link ExportParameters}), 483 * open submodels, run the model, and export HTML. 484 * @param graphFrame The frame being exported. 485 * @param parameters The export parameters. 486 * @param indexFile If you wish to show the exported page in a browser, 487 * then this parameter must specify the file to which the write occurs 488 * and parameters.showInBrowser must be true. Otherwise, this parameter 489 * should be null. 490 * @param writer The writer to write to, or null to write to the default 491 * index.html file. 492 * @param waitForCompletion If true, then do not return until the export 493 * is complete. In this case, everything is run in the calling thread, 494 * which is required to be the Swing event thread. 495 * @exception IllegalActionException If something goes wrong. 496 */ 497 public static void openRunAndWriteHTML(final BasicGraphFrame graphFrame, 498 final ExportParameters parameters, final File indexFile, 499 final Writer writer, final boolean waitForCompletion) 500 throws IllegalActionException { 501 if (graphFrame == null) { 502 throw new IllegalActionException( 503 "Cannot export without a graphFrame."); 504 } 505 // Open submodels, if appropriate. 506 final Set<Tableau> tableauxToClose = new HashSet<Tableau>(); 507 if (parameters.openCompositesBeforeExport) { 508 NamedObj model = graphFrame.getModel(); 509 Effigy masterEffigy = Configuration 510 .findEffigy(graphFrame.getModel()); 511 if (model instanceof CompositeEntity) { 512 // graphFrame.getModel() might return a 513 // PteraController, which is not a CompositeActor. 514 List<Entity> entities = ((CompositeEntity) model).entityList(); 515 for (Entity entity : entities) { 516 _openEntity(entity, tableauxToClose, masterEffigy, 517 graphFrame); 518 } 519 } 520 } 521 // Running the model has to occur in a new thread, or the whole 522 // process could hang (if the model doesn't return). So finish in a new thread. 523 // That thread will, in turn, have to again invoke the swing event thread 524 // to close any tableaux that were opened above. 525 // It does not wait for the close to complete before finishing itself. 526 Runnable exportAction = new Runnable() { 527 @Override 528 public void run() { 529 try { 530 // graphFrame.getModel() might return a 531 // PteraController, which is not a CompositeActor. 532 NamedObj model = graphFrame.getModel(); 533 534 // If parameters are set to run the model, then do that. 535 if (parameters.runBeforeExport 536 && model instanceof CompositeActor) { 537 // Run the model. 538 Manager manager = ((CompositeActor) model).getManager(); 539 if (manager == null) { 540 manager = new Manager( 541 ((CompositeActor) model).workspace(), 542 "MyManager"); 543 ((CompositeActor) model).setManager(manager); 544 } 545 manager.execute(); 546 } 547 } catch (Exception ex) { 548 MessageHandler.error("Model execution failed.", ex); 549 throw new RuntimeException(ex); 550 } finally { 551 // The rest of the export has to occur in the 552 // swing event thread. We do this whether the 553 // run succeeded or not. 554 Runnable finishExport = new Runnable() { 555 @Override 556 public void run() { 557 try { 558 // -------- Finally, actually export to web. 559 graphFrame.writeHTML(parameters, writer); 560 561 // Finally, if requested, show the exported page. 562 if (parameters.showInBrowser 563 && indexFile != null) { 564 Configuration configuration = graphFrame 565 .getConfiguration(); 566 try { 567 URL indexURL = new URL(indexFile.toURI() 568 .toURL().toString() 569 + "#in_browser"); 570 configuration.openModel(indexURL, 571 indexURL, 572 indexURL.toExternalForm(), 573 BrowserEffigy.staticFactory); 574 } catch (Throwable throwable) { 575 MessageHandler.error("Failed to open \"" 576 + indexFile + "\".", throwable); 577 throw new RuntimeException(throwable); 578 } 579 } 580 } catch (Exception ex) { 581 MessageHandler.error("Unable to export to web.", 582 ex); 583 throw new RuntimeException(ex); 584 } finally { 585 // Export is finally finished. 586 _exportInProgress = false; 587 synchronized (ExportHTMLAction.class) { 588 ExportHTMLAction.class.notifyAll(); 589 } 590 for (Tableau tableau : tableauxToClose) { 591 tableau.close(); 592 } 593 } 594 595 } 596 }; 597 if (waitForCompletion) { 598 finishExport.run(); 599 } else { 600 SwingUtilities.invokeLater(finishExport); 601 } 602 } 603 } 604 }; 605 // Invoke the new thread. First make sure the flag is set 606 // to indicate that an export is in progress. 607 _exportInProgress = true; 608 if (waitForCompletion) { 609 exportAction.run(); 610 } else { 611 Thread result = new Thread(exportAction); 612 result.start(); 613 } 614 } 615 616 /** Set the title to be used for the page being exported. 617 * @param title The title. 618 * @param showInHTML True to produce an HTML title prior to the model image. 619 */ 620 // FIXME: Replaced- a WebExportable will add the title, if any. If it does not 621 // add a title, then there will be no title. 622 623 @Override 624 public void setTitle(String title, boolean showInHTML) { 625 _title = StringUtilities.escapeForXML(title); 626 _showTitleInHTML = showInHTML; 627 } 628 629 /** Wait for the current invocation of {@link #exportToWeb(BasicGraphFrame, ExportParameters)} 630 * to complete. If there is not one in progress, return immediately. 631 */ 632 public static synchronized void waitForExportToComplete() { 633 while (_exportInProgress) { 634 try { 635 ExportHTMLAction.class.wait(); 636 } catch (InterruptedException e) { 637 // Ignore and return. 638 return; 639 } 640 } 641 } 642 643 /** Write an HTML page based on the current view of the model 644 * to the specified destination directory. The file will be 645 * named "index.html," and supporting files, including at 646 * least an image showing the contents currently visible in 647 * the graph frame, will be created. Any instances of 648 * {@link WebExportable} in the configuration are first 649 * cloned into the model, so these provide default behavior, 650 * for example defining links to any open composite actors 651 * or plot windows. 652 * <p> 653 * If the "ptolemy.ptII.exportHTML.usePtWebsite" property is set to true, 654 * e.g. by invoking with -Dptolemy.ptII.usePtWebsite=true, 655 * then the html files will have Ptolemy website specific Server Side Includes (SSI) 656 * code and use the JavaScript and fancybox files from the Ptolemy website. 657 * In addition, a toc.htm file will be created to aid in navigation. 658 * This facility is not likely to be portable to other websites. 659 * </p> 660 * 661 * @param parameters The parameters that control the export. 662 * @param writer The writer to use the write the HTML. If this is null, 663 * then create an index.html file in the 664 * directory given by the directoryToExportTo field of the parameters. 665 * @exception IOException If unable to write associated files. 666 * @exception PrinterException If unable to write associated files. 667 * @exception IllegalActionException If reading parameters fails. 668 */ 669 @Override 670 public void writeHTML(ExportParameters parameters, Writer writer) 671 throws PrinterException, IOException, IllegalActionException { 672 // Invoke with -Dptolemy.ptII.usePtWebsite=true to get Server 673 // Side Includes (SSI). FIXME: this is a bit of a hack, we should 674 // use templates instead. 675 boolean usePtWebsite = Boolean.valueOf(StringUtilities 676 .getProperty("ptolemy.ptII.exportHTML.usePtWebsite")); 677 usePtWebsite = usePtWebsite || parameters.usePtWebsite; 678 679 File indexFile = null; 680 PrintWriter printWriter = null; 681 682 // The following try...finally block ensures that the index and toc files 683 // get closed even if an exception occurs. It also resets _parameters. 684 try { 685 _parameters = parameters; 686 687 // First, create the image file showing whatever the current 688 // view in this frame shows. 689 NamedObj model = _basicGraphFrame.getModel(); 690 691 // $PTII/bin/ptinvoke ptolemy.vergil.basic.export.ExportModel -force htm -run -openComposites -timeOut 30000 -whiteBackground ptolemy/domains/ptera/demo/CarWash/CarWash.xml 692 // needs this. 693 if (model.getName().equals("_Controller")) { 694 model = model.getContainer(); 695 } 696 // Use a sanitized model name and avoid problems with special characters in file names. 697 _sanitizedModelName = StringUtilities.sanitizeName(model.getName()); 698 File imageFile = new File(parameters.directoryToExportTo, 699 _sanitizedModelName + "." + _parameters.imageFormat); 700 if (parameters.deleteFilesOnExit) { 701 imageFile.deleteOnExit(); 702 } 703 OutputStream out = new FileOutputStream(imageFile); 704 try { 705 _basicGraphFrame.writeImage(out, _parameters.imageFormat, 706 parameters.backgroundColor); 707 } finally { 708 out.close(); 709 } 710 // Initialize the data structures into which content is collected. 711 _areaAttributes = new HashMap<NamedObj, HashMap<String, String>>(); 712 _contents = new HashMap<String, List<StringBuffer>>(); 713 _end = new LinkedList<StringBuffer>(); 714 _head = new LinkedList<StringBuffer>(); 715 _start = new LinkedList<StringBuffer>(); 716 _contents.put("head", _head); 717 _contents.put("start", _start); 718 _contents.put("end", _end); 719 720 // Clone instances of WebExportable from the Configuration 721 // into the model. These are removed in the finally clause 722 // of the try block. 723 _provideDefaultContent(); 724 725 // Next, collect the web content specified by the instances 726 // of WebExportable contained by the model. 727 List<WebExportable> exportables = model 728 .attributeList(WebExportable.class); 729 730 // Plus, collect the web content specified by the contained 731 // objects of the model. 732 Iterator<NamedObj> contentsIterator = model 733 .containedObjectsIterator(); 734 while (contentsIterator.hasNext()) { 735 NamedObj containedObject = contentsIterator.next(); 736 exportables.addAll( 737 containedObject.attributeList(WebExportable.class)); 738 } 739 740 // Then, iterate through the list of exportables and extract 741 // content from each. 742 // Use the class of exportable to determine whether to insert 743 // content as an attribute or a seperate element 744 for (WebExportable exportable : exportables) { 745 exportable.provideContent(this); 746 } 747 748 // If a title has been specified and set to show, then 749 // add it to the start HTML section at the beginning. 750 if (_showTitleInHTML) { 751 _start.add(0, new StringBuffer("<h1>")); 752 _start.add(1, new StringBuffer(_title)); 753 _start.add(2, new StringBuffer("</h1>\n")); 754 } 755 756 // System.out.println("Location of index.html: "+parameters.directoryToExportTo); 757 758 // Next, create an HTML file. 759 if (writer == null) { 760 indexFile = new File(parameters.directoryToExportTo, 761 "index.html"); 762 if (parameters.deleteFilesOnExit) { 763 indexFile.deleteOnExit(); 764 } 765 Writer indexWriter = new FileWriter(indexFile); 766 printWriter = new PrintWriter(indexWriter); 767 } else { 768 printWriter = new PrintWriter(writer); 769 } 770 771 // Generate a header that will pass the HTML validator at 772 // http://validator.w3.org/ 773 // Use HTML5 tags. Use charset utf-8 to support extended characters 774 // We use println so as to get the correct eol character for 775 // the local platform. 776 printWriter.println("<!DOCTYPE html>"); 777 printWriter.println("<html>"); 778 printWriter.println("<head>"); 779 printWriter.println("<meta charset=utf-8>"); 780 781 // Define the path to the SSI files on the ptolemy site. 782 // ssiRoot always has a trailing slash. 783 final String ssiRoot = "https://ptolemy.org/"; 784 785 // Reference required script files. 786 // If the model contains an instance of CopyJavaScriptFiles, then 787 // the required files will have been copied into a directory called 788 // "javascript" in the top-level directory of the export. 789 // Otherwise, we want to reference these files at http://ptolemy.org/. 790 // If the usePtWebsite property is true, then reference the files 791 // at http://ptolemy.org/ whether the property is true or not. 792 String jsLibrary = ssiRoot; 793 if (!usePtWebsite) { 794 // If the model or a container above it in the hierarchy has 795 // copyJavaScriptFiles set to true, then set up the 796 // references to refer to the copied files rather than the 797 // website files. 798 // FIXME: This can fail if we export a submodel only but 799 // the enclosing model has its copyJavaScriptFiles parameter 800 // set to true! 801 String copiedLibrary = _findCopiedLibrary(model, "", 802 parameters.getJSCopier()); 803 if (copiedLibrary != null) { 804 jsLibrary = copiedLibrary; 805 } 806 } 807 808 // In HTML5, can omit "type" attributes for scripts and stylesheets 809 printWriter.println("<link rel=\"stylesheet\" href=\"" + jsLibrary 810 + "js/" + FILENAMES[2] + "\" media=\"screen\"/>"); 811 printWriter.println("<link rel=\"stylesheet\" href=\"" + jsLibrary 812 + "js/" + FILENAMES[4] + "\" media=\"screen\"/>"); 813 if (usePtWebsite) { 814 // FIXME: this absolute path is not very safe. The 815 // problem is that we don't know where $PTII is located on 816 // the website. 817 printWriter.println("<link href=\"" + ssiRoot 818 + "ptolemyII/ptIIlatest/ptII/doc/default.css\" rel=\"stylesheet\" type=\"text/css\"/>"); 819 } 820 821 // Title needed for the HTML validator. 822 printWriter.println("<title>" + _title + "</title>"); 823 824 // In HTML5, can omit "type" attributes for scripts and stylesheets 825 // NOTE: Due to a bug somewhere (browser, Javascript, etc.), can't end this with />. Have to use </script>. 826 printWriter.println("<script src=\"" + jsLibrary + "js/" 827 + FILENAMES[0] + "\"></script>"); 828 printWriter.println("<script src=\"" + jsLibrary + "js/" 829 + FILENAMES[1] + "\"></script>"); 830 831 // FILENAMES[2] is a stylesheet <link, so it goes in the head, see above. 832 833 printWriter.println("<script src=\"" + jsLibrary + "js/" 834 + FILENAMES[3] + "\"></script>"); 835 printWriter.println("<script src=\"" + jsLibrary + "js/" 836 + FILENAMES[5] + "\"></script>"); 837 // Could alternatively use a CDS (Content Delivery Service) for the JavaScript library for jquery. 838 // index.println("<script type=\"text/js\" src=\"http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js\"></script>"); 839 840 // Next, create the image map. 841 String map = _createImageMap(parameters.directoryToExportTo); 842 843 // Write the main part of the HTML file. 844 // 845 _printHTML(printWriter, "head"); 846 847 //------ <head>...</head> above here 848 if (usePtWebsite) { 849 // Reference the server-side includes. 850 // toppremenu.htm includes </head>...<body> 851 printWriter.println( 852 "<!--#include virtual=\"/ssi/toppremenu.htm\" -->"); 853 printWriter.println("<!--#include virtual=\"toc.htm\" -->"); 854 printWriter.println( 855 "<!--#include virtual=\"/ssi/toppostmenu.htm\" -->"); 856 } else { 857 // The Ptolemy website headers include the closing </head> and <body tag> 858 printWriter.println("</head>"); 859 // Place </head> and <body> on separate lines so that 860 // tools like the TerraSwarm website can easily find 861 // them. 862 // Match the background color of the canvas, unless an explicit 863 // background color is given. 864 Color background = parameters.backgroundColor; 865 if (parameters.backgroundColor == null) { 866 JCanvas canvas = _basicGraphFrame.getJGraph().getGraphPane() 867 .getCanvas(); 868 background = canvas.getBackground(); 869 } 870 String color = "#" + String.format("%02x", background.getRed()) 871 + String.format("%02x", background.getGreen()) 872 + String.format("%02x", background.getBlue()); 873 874 printWriter.println("<body>"); 875 printWriter.println( 876 "<div style=\"background-color:" + color + "\">"); 877 } 878 879 _printHTML(printWriter, "start"); 880 881 boolean linkToJNLP = Boolean.valueOf(StringUtilities 882 .getProperty("ptolemy.ptII.exportHTML.linkToJNLP")); 883 //System.out.println("ExportHTMLAction: model: " + model + " model name: " + model.getName() + " " + model.getContainer() + " isClassDef: " + ((ptolemy.kernel.InstantiableNamedObj)model).isClassDefinition()); 884 //if (model.getContainer() != null) { 885 // System.out.println("ExportHTMLAction: name: " + model.getContainer().getName() + model.getContainer().getContainer()); 886 //} 887 if (linkToJNLP && ((model.getContainer() == null 888 && model instanceof CompositeEntity 889 // Don't include links to the .xml of class definitions 890 && !((CompositeEntity) model).isClassDefinition()) 891 || (model.getContainer() != null 892 && /* Ptera */model.getContainer() 893 .getContainer() == null 894 && model.getName().equals("_Controller")))) { 895 String linkToHelp = "<a href=\"" + ssiRoot 896 + "ptolemyII/ptIIlatest/ptII/doc/webStartHelp_index.htm\"><img src=\"" 897 + ssiRoot 898 + "image/question.png\" alt=\"What is Web Start\"></a> (<i>Java Plug-in Required</i>)"; 899 900 printWriter.println("<div id=\"inlineImg\">" // Defined in UCB.css 901 + "<p>Below is a browsable image of the model.</p> " 902 + "<ul>\n"); 903 904 StringParameter noJNLPLinkParameter = (StringParameter) model 905 .getAttribute("_noJNLPLink", StringParameter.class); 906 if (linkToJNLP && noJNLPLinkParameter != null) { 907 System.out.println( 908 "The ptolemy.ptII.exportHTML.linkToJNLP JVM property was set, " 909 + "but the _noJNLPLink parameter was set, so this model " 910 + model.getFullName() 911 + " will not have a link to the JNLP version. Typically models that don't run well, " 912 + "like the BCVTB models have this parameter set."); 913 printWriter.println( 914 "<!-- The model had a _noJNLPLink parameter set, so we are not " 915 + "linking to the JNLP files. -->\n"); 916 } else { 917 printWriter.println("<li>For an executable version," 918 + "<!-- We use the deployJava.js script so that Java " 919 + "will be installed if necessary -->\n" 920 + "<script src=\"http://www.java.com/js/deployJava.js\"></script>\n" 921 + "<script>\n" 922 + " var dir = location.href.substring(0,location.href.lastIndexOf('/'));\n" 923 + " var parentDir = dir.substring(0,dir.lastIndexOf('/')+1);\n" 924 + " var url = parentDir + \"" + _sanitizedModelName 925 + ".jnlp\";\n" 926 + " deployJava.createWebStartLaunchButton(url);\n" 927 + " document.write(\" the WebStart version. " 928 + linkToHelp.replace("\"", "\\\"") + "\");\n" 929 + "</script>\n" + "<noscript>\n" + "<a href=\"../" 930 + _sanitizedModelName 931 + ".jnlp\">WebStart version</a>. \n" + linkToHelp 932 + "</noscript>\n" + "</li>\n"); 933 } 934 printWriter.println( 935 "<li>To view or save the MoML file for this model, " 936 + "<a href=\"../" + _sanitizedModelName 937 + ".xml\">click here</a>.</li>"); 938 if (usePtWebsite) { 939 if (_isInDomains(model)) { 940 printWriter.println("<li>For a domain overview, " 941 + "<a href=\"../../../doc/\">click here</a>.</li>"); 942 } 943 } 944 printWriter.println("</ul>\n" + "</div> <!-- inlineImg -->\n"); 945 946 } 947 // Put the image in. 948 printWriter.println("<img src=\"" + _sanitizedModelName + "." 949 + _parameters.imageFormat + "\" usemap=\"#iconmap\" " 950 // The HTML Validator at http://validator.w3.org/check wants an alt tag 951 + "alt=\"" + _sanitizedModelName + "model\"/>"); 952 printWriter.println(map); 953 _printHTML(printWriter, "end"); 954 955 if (!usePtWebsite) { 956 printWriter.println("</div>"); 957 printWriter.println("</body>"); 958 printWriter.println("</html>"); 959 } else { 960 printWriter.println("<!-- /body -->"); 961 printWriter.println("<!-- /html -->"); 962 printWriter.println( 963 "<!--#include virtual=\"/ssi/bottom.htm\" -->"); 964 965 ExportHTMLAction._findToc(model); 966 //if (tocContents != "") { 967 // _addContent("toc.htm", false, tocContents); 968 //} else { 969 // Start the top of the toc.htm file. 970 _addContent("toc.htm", false, "<div id=\"menu\">"); 971 _addContent("toc.htm", false, "<ul>"); 972 _addContent("toc.htm", false, 973 "<li><a href=\"/index.htm\">Ptolemy Home</a></li>"); 974 975 // The URL of the current release. 976 String ptURL = (usePtWebsite ? "http://ptolemy.org" : "") 977 + "/ptolemyII/ptII" 978 + VersionAttribute.majorCurrentVersion() + "/ptII" 979 + VersionAttribute.CURRENT_VERSION.getExpression() 980 + "/"; 981 982 _addContent("toc.htm", false, 983 "<li><a href=\"" + ptURL + "doc/index.htm\">Ptolemy " 984 + VersionAttribute.majorCurrentVersion() 985 + "</a></li>"); 986 _addContent("toc.htm", false, "</ul>"); 987 _addContent("toc.htm", false, ""); 988 989 String upHTML = null; 990 if (_isInDomains(model)) { 991 upHTML = "<li><a href=\"../../../doc/\">Up</a></li>"; 992 } else { 993 // If there is a _upHTML parameter, use its value. 994 StringParameter upHTMLParameter = (StringParameter) model 995 .getAttribute("_upHTML", StringParameter.class); 996 if (upHTMLParameter != null) { 997 upHTML = upHTMLParameter.stringValue(); 998 } else { 999 //if (!usePtWebsite) { 1000 // upHTML = " <li><a href=\"../index.html\">Up</a></li>"; 1001 //} else { 1002 // Generate links to the domain docs. 1003 String domains[] = { "Continuous", "DDF", "DE", "Modal", 1004 "PN", "Rendezvous", "SDF", "SR", "Wireless" }; 1005 StringBuffer buffer = new StringBuffer(); 1006 for (int i = 0; i < domains.length; i++) { 1007 buffer.append("<li><a href=\"" + ptURL 1008 + "ptolemy/domains/" 1009 + domains[i].toLowerCase() 1010 + "/doc/index.htm\">" + domains[i] 1011 + "</a></li>"); 1012 } 1013 upHTML = buffer.toString(); 1014 //} 1015 } 1016 } 1017 1018 // Only add <ul> if we have upHTML 1019 if (upHTML != null) { 1020 _addContent("toc.htm", false, "<ul>"); 1021 _addContent("toc.htm", false, upHTML); 1022 _addContent("toc.htm", false, "</ul>"); 1023 } 1024 1025 // Get the toc contents and stuff it into toc.htm. 1026 List<StringBuffer> contents = _contents.get("tocContents"); 1027 if (contents != null) { 1028 _addContent("toc.htm", false, "<ul>"); 1029 for (StringBuffer line : contents) { 1030 _addContent("toc.htm", false, line.toString()); 1031 } 1032 _addContent("toc.htm", false, "</ul>"); 1033 } 1034 _addContent("toc.htm", false, "</div><!-- /#menu -->"); 1035 //} 1036 } 1037 1038 // If _contents contains any entry other than head, start, or end, 1039 // then interpret that entry as a file name to write to. 1040 for (String key : _contents.keySet()) { 1041 if (!key.equals("end") && !key.equals("head") 1042 && !key.equals("start") && !key.equals("tocContents")) { 1043 if (key.equals("")) { 1044 // FIXME: I'm not sure why the key would be 1045 // empty but the command below requires it: 1046 1047 // (cd $PTII/doc/papers/y12/designContracts; $PTII/bin/ptinvoke -Dptolemy.ptII.exportHTML.linkToJNLP=true -Dptolemy.ptII.exportHTML.usePtWebsite=true ptolemy.vergil.basic.export.ExportModel -run -whiteBackground -openComposites htm DCMotorTol.xml) 1048 1049 System.out.println( 1050 "Warning, key of _contents was empty?"); 1051 continue; 1052 } 1053 // NOTE: A RESTful version of this would create a resource 1054 // that could be addressed by a URL. For now, we just 1055 // write to a file. Java documentation doesn't say 1056 // whether the following overwrites a pre-existing file, 1057 // but it does seem to do that, so I assume that's what it does. 1058 Writer fileWriter = null; 1059 try { 1060 File file = new File(parameters.directoryToExportTo, 1061 key); 1062 if (parameters.deleteFilesOnExit) { 1063 file.deleteOnExit(); 1064 } 1065 fileWriter = new FileWriter(file); 1066 } catch (IOException ex) { 1067 throw new IllegalActionException(model, ex, 1068 "Could not open a FileWriter " 1069 + "in directory \"" 1070 + parameters.directoryToExportTo 1071 + "\" and file \"" + key + "\"."); 1072 1073 } 1074 PrintWriter otherWriter = new PrintWriter(fileWriter); 1075 List<StringBuffer> contents = _contents.get(key); 1076 for (StringBuffer line : contents) { 1077 otherWriter.println(line); 1078 } 1079 otherWriter.close(); 1080 } 1081 } 1082 } finally { 1083 _parameters = null; 1084 _removeDefaultContent(); 1085 if (printWriter != null) { 1086 printWriter.close(); // Without this, the output file may be empty 1087 } 1088 if (usePtWebsite && indexFile != null) { 1089 if (!indexFile.setExecutable(true, false /*ownerOnly*/)) { 1090 System.err.println( 1091 "Could not make " + indexFile + "executable."); 1092 } 1093 } 1094 } 1095 } 1096 1097 /////////////////////////////////////////////////////////////////// 1098 //// package protected methods //// 1099 1100 /** List of filenames needed by jquery and fancybox. 1101 * These are automatically provided to every exported web page 1102 * either by referencing the ptolemy.org website (the default) 1103 * or by copying the files into the target directory (if the 1104 * model contains an instance of WebExportParameters with 1105 * copyJavaScriptFiles set to true). 1106 * The first three of these should be the JavaScript files to include, 1107 * and the fourth should be the CSS file. 1108 * The rest are image files to copy over. 1109 */ 1110 // FIXME: I don't like the hardwired version numbers here. 1111 // Findbugs wants this package protected and final. 1112 final static String[] FILENAMES = { "jquery-1.7.2.min.js", 1113 "jquery.fancybox-1.3.4.pack.js", "jquery.fancybox-1.3.4.css", 1114 "pt-1.0.0.js", "tooltipster.css", "jquery.tooltipster.min.js", 1115 // The ones above this line must be in exactly the order given 1116 // They are referenced below by index. 1117 "blank.gif", "fancybox.png", "fancybox-y.png", "fancybox-x.png", 1118 "fancy_title_right.png", "fancy_title_over.png", 1119 "fancy_title_main.png", "fancy_title_left.png", 1120 "fancy_shadow_w.png", "fancy_shadow_sw.png", "fancy_shadow_se.png", 1121 "fancy_shadow_s.png", "fancy_shadow_nw.png", "fancy_shadow_ne.png", 1122 "fancy_shadow_n.png", "fancy_shadow_e.png", "fancy_nav_right.png", 1123 "fancy_nav_left.png", "fancy_loading.png", "fancy_close.png", 1124 "javascript-license.htm" }; 1125 1126 /////////////////////////////////////////////////////////////////// 1127 //// protected methods //// 1128 1129 /** Create the image map. As a side effect, this may create other 1130 * HTML files or subdirectories. 1131 * @param directory The directory into which to write any HTML 1132 * that is created as a side effect. 1133 * @return HTML that describes the image map. 1134 * @exception PrinterException If writing to the toc file fails. 1135 * @exception IOException If IO fails. 1136 * @exception IllegalActionException If reading parameters fails. 1137 */ 1138 protected String _createImageMap(File directory) 1139 throws IllegalActionException, IOException, PrinterException { 1140 StringBuffer result = new StringBuffer(); 1141 // The HTML Validator at http://validator.w3.org/check wants an id tag. 1142 // For HTML5, the name and id must match 1143 result.append("<map name=\"iconmap\" id=\"iconmap\">\n"); 1144 1145 // Iterate over the icons. 1146 List<IconVisibleLocation> iconLocations = _getIconVisibleLocations(); 1147 for (IconVisibleLocation location : iconLocations) { 1148 // This string will have at least one space at the start and the end. 1149 StringBuffer attributeString = new StringBuffer(" "); 1150 String title = "Actor"; // Default in case there is no title key. 1151 HashMap<String, String> areaAttributes = _areaAttributes 1152 .get(location.object); 1153 // If areaAttributes is null, omit the entry, since an HTML area 1154 // element is required to have an href attribute 1155 if (areaAttributes != null) { 1156 for (Map.Entry<String, String> entry : areaAttributes 1157 .entrySet()) { 1158 String key = entry.getKey(); 1159 String value = entry.getValue(); 1160 // If the value is empty, omit the entry. 1161 if (value != null && !value.trim().equals("")) { 1162 if (key.equals("title")) { 1163 title = StringUtilities.escapeString(value); 1164 } 1165 attributeString.append(key); 1166 attributeString.append("=\""); 1167 attributeString 1168 .append(StringUtilities.escapeString(value)); 1169 attributeString.append("\" "); 1170 } 1171 } 1172 1173 // Write the name of the actor followed by the table. 1174 result.append("<area shape=\"rect\" coords=\"" 1175 + (int) location.topLeftX + "," 1176 + (int) location.topLeftY + "," 1177 + (int) location.bottomRightX + "," 1178 + (int) location.bottomRightY + "\"\n" + attributeString 1179 + "alt=\"" + title + "\"/>\n"); 1180 } 1181 1182 } 1183 result.append("</map>\n"); 1184 return result.toString(); 1185 } 1186 1187 /** Return a list of data structures with one entry for each visible 1188 * entity and attribute. Each data structure contains 1189 * a reference to the entity and the coordinates 1190 * of the upper left corner and lower right corner of the main 1191 * part of its icon (not including decorations like the name 1192 * and any highlights it may have). The coordinates are relative 1193 * to the current visible rectangle, where the upper left corner 1194 * of the visible rectangle has coordinates (0,0), and the lower 1195 * right corner has coordinates (w,h), where w is the width 1196 * and h is the height (in pixels). 1197 * @return A list representing the space occupied by each 1198 * visible icon for the entities in the model, or an empty 1199 * list if no icons are visible. 1200 */ 1201 protected List<IconVisibleLocation> _getIconVisibleLocations() { 1202 List<IconVisibleLocation> result = new LinkedList<IconVisibleLocation>(); 1203 Rectangle2D viewSize = _basicGraphFrame.getVisibleRectangle(); 1204 JCanvas canvas = _basicGraphFrame.getJGraph().getGraphPane() 1205 .getCanvas(); 1206 AffineTransform transform = canvas.getCanvasPane().getTransformContext() 1207 .getTransform(); 1208 double scaleX = transform.getScaleX(); 1209 double scaleY = transform.getScaleY(); 1210 double translateX = transform.getTranslateX(); 1211 double translateY = transform.getTranslateY(); 1212 1213 NamedObj model = _basicGraphFrame.getModel(); 1214 if (model instanceof CompositeEntity) { 1215 List<Entity> entities = ((CompositeEntity) model).entityList(); 1216 for (Entity entity : entities) { 1217 _addRectangle(result, viewSize, scaleX, scaleY, translateX, 1218 translateY, entity); 1219 } 1220 } 1221 List<Attribute> attributes = ((CompositeEntity) model).attributeList(); 1222 for (Attribute attribute : attributes) { 1223 _addRectangle(result, viewSize, scaleX, scaleY, translateX, 1224 translateY, attribute); 1225 } 1226 return result; 1227 } 1228 1229 /** Provide default HTML content by cloning any 1230 * default WebExportable attributes provided by 1231 * the configuration into the model. In the case 1232 * of {@link DefaultIconScript} and {@link DefaultIconLink} 1233 * objects, if the model contains one with the same event 1234 * type, then the one from the configuration is not used. 1235 * @exception IllegalActionException If cloning a configuration attribute fails. 1236 */ 1237 protected void _provideDefaultContent() throws IllegalActionException { 1238 Configuration configuration = _basicGraphFrame.getConfiguration(); 1239 if (configuration != null) { 1240 // Any instances of WebExportable contained by the 1241 // configuration are cloned into the model. 1242 NamedObj model = _basicGraphFrame.getModel(); 1243 List<WebExportable> exportables = configuration 1244 .attributeList(WebExportable.class); 1245 for (WebExportable exportable : exportables) { 1246 if (exportable instanceof Attribute) { 1247 boolean foundOverride = false; 1248 if (exportable instanceof DefaultIconScript) { 1249 // Check whether the script provided by the model overrides the 1250 // one given in the configurations. It does if the eventType matches 1251 // and it either includes the same objects (Entities or Attributes) or 1252 // it includes all objects, and the instancesOf that is specifies matches. 1253 String eventType = ((DefaultIconScript) exportable).eventType 1254 .stringValue(); 1255 String include = ((DefaultIconScript) exportable).include 1256 .stringValue(); 1257 String instancesOf = ((DefaultIconScript) exportable).instancesOf 1258 .stringValue(); 1259 List<DefaultIconScript> defaults = model 1260 .attributeList(DefaultIconScript.class); 1261 for (DefaultIconScript script : defaults) { 1262 if (script.eventType.stringValue().equals(eventType) 1263 && (script.include.stringValue() 1264 .equals(include) 1265 || script.include.stringValue() 1266 .toLowerCase( 1267 Locale.getDefault()) 1268 .equals("all")) 1269 && script.instancesOf.stringValue() 1270 .equals(instancesOf)) { 1271 // Skip this default from the configuration. 1272 foundOverride = true; 1273 break; 1274 } 1275 } 1276 } else if (exportable instanceof DefaultIconLink) { 1277 // Check whether the link default provided by the model overrides the 1278 // one given in the configurations. It does if 1279 // it either includes the same objects (Entities or Attributes) or 1280 // it includes all objects, and the instancesOf that is specifies matches. 1281 String include = ((DefaultIconLink) exportable).include 1282 .stringValue(); 1283 String instancesOf = ((DefaultIconLink) exportable).instancesOf 1284 .stringValue(); 1285 List<DefaultIconLink> defaults = model 1286 .attributeList(DefaultIconLink.class); 1287 for (DefaultIconLink script : defaults) { 1288 if ((script.include.stringValue().equals(include) 1289 || script.include.stringValue() 1290 .toLowerCase(Locale.getDefault()) 1291 .equals("all")) 1292 && script.instancesOf.stringValue() 1293 .equals(instancesOf)) { 1294 // Skip this default from the configuration. 1295 foundOverride = true; 1296 break; 1297 } 1298 } 1299 } 1300 if (foundOverride) { 1301 continue; 1302 } 1303 try { 1304 Attribute clone = (Attribute) ((Attribute) exportable) 1305 .clone(model.workspace()); 1306 clone.setName(model.uniqueName(clone.getName())); 1307 clone.setContainer(model); 1308 clone.setPersistent(false); 1309 // Make sure this appears earlier in the list of attributes 1310 // than any contained by the model. The ones in the model should 1311 // override the ones provided by the configuration. 1312 clone.moveToFirst(); 1313 } catch (CloneNotSupportedException e) { 1314 throw new InternalErrorException( 1315 "Can't clone WebExportable attribute in Configuration: " 1316 + ((Attribute) exportable).getName()); 1317 } catch (NameDuplicationException e) { 1318 throw new InternalErrorException( 1319 "Failed to generate unique name for attribute in Configuration: " 1320 + ((Attribute) exportable).getName()); 1321 } 1322 } 1323 } 1324 } 1325 } 1326 1327 /** Remove default HTML content, which includes all instances of 1328 * WebExportable that are not persistent. 1329 * @exception IllegalActionException If removing the attribute fails. 1330 */ 1331 protected void _removeDefaultContent() throws IllegalActionException { 1332 NamedObj model = _basicGraphFrame.getModel(); 1333 List<WebExportable> exportables = model 1334 .attributeList(WebExportable.class); 1335 for (WebExportable exportable : exportables) { 1336 if (exportable instanceof Attribute) { 1337 Attribute attribute = (Attribute) exportable; 1338 if (!attribute.isPersistent()) { 1339 try { 1340 attribute.setContainer(null); 1341 } catch (NameDuplicationException e) { 1342 throw new InternalErrorException(e); 1343 } 1344 } 1345 } 1346 } 1347 } 1348 1349 /////////////////////////////////////////////////////////////////// 1350 //// protected methods //// 1351 1352 /** The associated Vergil frame. */ 1353 protected final BasicGraphFrame _basicGraphFrame; 1354 1355 /////////////////////////////////////////////////////////////////// 1356 //// private methods //// 1357 1358 /** Add HTML content at the specified position. This content is not 1359 * associated with any NamedObj. 1360 * The position is expected to be one of "head", "start", "end", 1361 * or anything else. In the latter case, the value 1362 * of the position attribute is a filename 1363 * into which the content is written. 1364 * If <i>onceOnly</i> is true, then if identical content has 1365 * already been added to the specified position, then it is not 1366 * added again. 1367 * @param position The position for the content. 1368 * @param onceOnly True to prevent duplicate content. 1369 * @param content The content to add. 1370 */ 1371 1372 private void _addContent(String position, boolean onceOnly, 1373 String content) { 1374 List<StringBuffer> contents = _contents.get(position); 1375 if (contents == null) { 1376 contents = new LinkedList<StringBuffer>(); 1377 _contents.put(position, contents); 1378 } 1379 StringBuffer contentsBuffer = new StringBuffer(content); 1380 // Check to see whether contents are already present. 1381 if (onceOnly) { 1382 // FIXME: Will List.contains() work if two StringBuffers 1383 // are constructed from the same String? 1384 if (contents.contains(contentsBuffer)) { 1385 return; 1386 } 1387 } 1388 contents.add(contentsBuffer); 1389 } 1390 1391 /** Add to the specified result list the bounds of the icon 1392 * for the specified object. 1393 * @param result The list to add to. 1394 * @param viewSize The view size. 1395 * @param scaleX The x scaling factor. 1396 * @param scaleY The y scaling factor. 1397 * @param translateX The x translation. 1398 * @param translateY The y translation. 1399 * @param object The object to add. 1400 */ 1401 private void _addRectangle(List<IconVisibleLocation> result, 1402 Rectangle2D viewSize, double scaleX, double scaleY, 1403 double translateX, double translateY, NamedObj object) { 1404 Locatable location = null; 1405 try { 1406 location = (Locatable) object.getAttribute("_location", 1407 Locatable.class); 1408 } catch (IllegalActionException e1) { 1409 // NOTE: What to do here? For now, ignoring the node. 1410 } 1411 if (location != null) { 1412 GraphController controller = _basicGraphFrame.getJGraph() 1413 .getGraphPane().getGraphController(); 1414 Figure figure = controller.getFigure(location); 1415 1416 if (figure != null) { 1417 // NOTE: Calling getBounds() on the figure itself yields an 1418 // inaccurate bounds, for some reason. 1419 // Weirdly, to get the size right, we need to use the shape. 1420 // But to get the location right, we need the other! 1421 Rectangle2D figureBounds = figure.getShape().getBounds2D(); 1422 1423 // If the figure is composite, use the background figure 1424 // for the bounds instead. NOTE: This seems to be a mistake. 1425 // The size and position information yielded appears to have 1426 // no relationship to reality. 1427 /* 1428 if (figure instanceof CompositeFigure) { 1429 figure = ((CompositeFigure) figure).getBackgroundFigure(); 1430 figureBounds = figure.getShape().getBounds2D(); 1431 } 1432 */ 1433 // Populate the data structure with bound information 1434 // relative to the visible rectangle. 1435 // Sadly, neither the figureOrigin nor the figureBounds 1436 // tells us where the figure is. So we have quite a bit 1437 // of work to do. 1438 // First, get the width of the figure. 1439 // This is the only variable that does not depend 1440 // on the anchor. 1441 double width = figureBounds.getWidth(); 1442 double height = figureBounds.getHeight(); 1443 IconVisibleLocation i = new IconVisibleLocation(); 1444 i.object = object; 1445 i.topLeftX = figureBounds.getX() * scaleX + translateX 1446 - _PADDING; 1447 i.topLeftY = figureBounds.getY() * scaleY + translateY 1448 - _PADDING; 1449 i.bottomRightX = i.topLeftX + width * scaleX + 2 * _PADDING; 1450 i.bottomRightY = i.topLeftY + height * scaleY + 2 * _PADDING; 1451 1452 // If the rectangle is not visible, no more to do. 1453 if (i.bottomRightX < 0.0 || i.bottomRightY < 0.0 1454 || i.topLeftX > viewSize.getWidth() 1455 || i.topLeftY > viewSize.getHeight()) { 1456 return; 1457 } else { 1458 // Clip the rectangle so it does not include any portion 1459 // that is not in the visible rectangle. 1460 if (i.topLeftX < 0.0) { 1461 i.topLeftX = 0.0; 1462 } 1463 if (i.topLeftY < 0.0) { 1464 i.topLeftY = 0.0; 1465 } 1466 if (i.bottomRightX > viewSize.getWidth()) { 1467 i.bottomRightX = viewSize.getWidth(); 1468 } 1469 if (i.bottomRightY > viewSize.getHeight()) { 1470 i.bottomRightY = viewSize.getHeight(); 1471 } 1472 // Add the data to the result list. 1473 // This is inserted at the start, not the end of the 1474 // list so that in the image map, items in front appear 1475 // earlier rather than later. This ensures that items 1476 // in front take precedence. 1477 result.add(0, i); 1478 } 1479 } 1480 } 1481 } 1482 1483 /** Escape strings for inclusion as the value of HTML attribute. 1484 * @param string The string to escape. 1485 * @return Escaped string. 1486 */ 1487 private String _escapeString(String string) { 1488 // This method is abstracted because it's not really clear 1489 // what should be escaped. 1490 String result = StringUtilities.escapeForXML(string); 1491 // Bizarrely, escaping all characters except newlines work. 1492 // Newlines need to be converted to \n. 1493 // No idea why so many backslashes are required below. 1494 // result = result.replaceAll(" ", "\\\\\\n"); 1495 return result; 1496 } 1497 1498 /** Construct a path the form "../../", for example, from the specified 1499 * model to the specified copier, where the specified copier is either 1500 * null or a any container above the specified model in the hierarchy. 1501 * If the specified copier is null or is not a container above the 1502 * specified model, then return null. 1503 * @param model The model. 1504 * @param path The path so far. 1505 * @param copier The model responsible for the copying, which should be 1506 * null if no copying is being done, equal to model if the model is 1507 * responsible for copying, or a container of model if a container of model 1508 * is doing the copying. 1509 */ 1510 private String _findCopiedLibrary(NamedObj model, String path, 1511 NamedObj copier) { 1512 if (model == copier) { 1513 return path; 1514 } 1515 NamedObj container = model.getContainer(); 1516 if (container == null) { 1517 // Got to the top level without finding an instance of CopyJavaScriptFiles. 1518 return null; 1519 } 1520 return _findCopiedLibrary(container, "../" + path, copier); 1521 } 1522 1523 /** Return the contents of a toc.htm file 1524 * that is located in either the current directory 1525 * or ../../doc/ 1526 * @param model The model to be checked 1527 * @return the contents of the toc.htm file or the empty string. 1528 */ 1529 private static String _findToc(NamedObj model) { 1530 try { 1531 URIAttribute modelURI = (URIAttribute) model.getAttribute("_uri", 1532 URIAttribute.class); 1533 if (modelURI != null) { 1534 // If a model is remote, then don't look for toc.htm. 1535 if (!modelURI.getURI().toString().startsWith("file:")) { 1536 System.out.println("Can't find toc: " + model.getFullName() 1537 + ": _uri is " + modelURI.getURI() 1538 + ", which does not start with \"file:\""); 1539 return ""; 1540 } 1541 1542 // Look in the current directory for toc.htm or toc.html, then 1543 // look for ../../doc/toc.htm and then ../../doc/toc.html. 1544 File modelFile = new File(modelURI.getURI()); 1545 File tocFile = new File(modelFile.getParent(), "toc.htm"); 1546 if (!tocFile.exists()) { 1547 tocFile = new File(modelFile.getParent(), "toc.html"); 1548 if (!tocFile.exists()) { 1549 File docDirectory = new File(modelFile.getParent(), 1550 "../../doc/"); 1551 if (docDirectory.exists() 1552 && docDirectory.isDirectory()) { 1553 tocFile = new File(docDirectory, "toc.htm"); 1554 if (!tocFile.exists()) { 1555 tocFile = new File(docDirectory, "toc.html"); 1556 if (!tocFile.exists()) { 1557 tocFile = null; 1558 } 1559 } 1560 } else { 1561 tocFile = null; 1562 } 1563 } 1564 } 1565 if (tocFile == null) { 1566 return ""; 1567 } else { 1568 // Read the contents and return it. 1569 System.out.println("Copying the contents of " + tocFile); 1570 StringBuffer result = new StringBuffer(); 1571 FileReader fileReader = null; 1572 BufferedReader bufferedReader = null; 1573 try { 1574 fileReader = new FileReader(tocFile); 1575 bufferedReader = new BufferedReader(fileReader); 1576 String line = null; 1577 while ((line = bufferedReader.readLine()) != null) { 1578 result.append(line + "\n"); 1579 } 1580 } catch (IOException x) { 1581 System.err.format("IOException: %s%n", x); 1582 } finally { 1583 if (bufferedReader != null) { 1584 bufferedReader.close(); 1585 } 1586 } 1587 return result.toString(); 1588 } 1589 } 1590 } catch (Throwable throwable) { 1591 System.out.println("Failed to find toc for " + model.getFullName() 1592 + ": " + throwable); 1593 throwable.printStackTrace(); 1594 return ""; 1595 } 1596 return ""; 1597 } 1598 1599 /** Return true if the model is in the domains demo directory 1600 * and ../../../doc exists and is a directory and 1601 * either ../../doc/index.htm or index.html exist 1602 * @param model The model to be checked 1603 * @return true if it is in the domains directory and doc exists. 1604 */ 1605 private static boolean _isInDomains(NamedObj model) { 1606 try { 1607 URIAttribute modelURI = (URIAttribute) model.getAttribute("_uri", 1608 URIAttribute.class); 1609 if (modelURI != null) { 1610 String modelURIString = modelURI.getURI().toString(); 1611 if (modelURIString.contains("/domains")) { 1612 try { 1613 File modelFile = new File(modelURI.getURI()); 1614 File docDirectory = new File(modelFile, 1615 "../../../doc/"); 1616 if (docDirectory.exists() && docDirectory.isDirectory() 1617 && (new File(docDirectory, "index.htm").exists() 1618 || new File(docDirectory, "index.html") 1619 .exists())) { 1620 return true; 1621 } 1622 } catch (Throwable throwable) { 1623 return false; 1624 } 1625 } 1626 } 1627 } catch (IllegalActionException ex) { 1628 return false; 1629 } 1630 return false; 1631 } 1632 1633 /** Open a composite entity, if it is not already open, 1634 * and recursively open any composite 1635 * entities or state refinements that it contains. 1636 * @param entity The entity to open. 1637 * @param tableauxToClose A list of tableaux are newly opened. 1638 * @param masterEffigy The top-level effigy for the modeling being exported. 1639 * @param graphFrame The graph frame. 1640 * @exception IllegalActionException If opening fails. 1641 * @exception NameDuplicationException Not thrown. 1642 */ 1643 private static void _openComposite(CompositeEntity entity, 1644 Set<Tableau> tableauxToClose, Effigy masterEffigy, 1645 BasicGraphFrame graphFrame) throws IllegalActionException { 1646 1647 Configuration configuration = graphFrame.getConfiguration(); 1648 Effigy effigy = configuration.getEffigy(entity); 1649 1650 Tableau tableau; 1651 if (effigy != null) { 1652 // Effigy exists. See whether it has an open tableau. 1653 List<Tableau> tableaux = effigy.entityList(Tableau.class); 1654 if (tableaux == null || tableaux.size() == 0) { 1655 // No open tableau. Open one. 1656 tableau = configuration.createPrimaryTableau(effigy); 1657 tableauxToClose.add(tableau); 1658 } else { 1659 // The first tablequ is sufficient to retrieve the model. 1660 tableau = tableaux.get(0); 1661 } 1662 } else { 1663 // No pre-existing effigy. 1664 try { 1665 tableau = configuration.openModel(entity); 1666 tableauxToClose.add(tableau); 1667 } catch (NameDuplicationException e) { 1668 // This should not occur. 1669 throw new InternalErrorException(e); 1670 } 1671 } 1672 // NOTE: The entity that was opened may not actually be entity 1673 // because if it was an instance of a class, then class definition 1674 // will have been opened. 1675 CompositeEntity actualEntity = entity; 1676 if (tableau instanceof ActorGraphTableau) { 1677 PtolemyEffigy actualEffigy = (PtolemyEffigy) tableau.getContainer(); 1678 actualEntity = (CompositeEntity) actualEffigy.getModel(); 1679 } 1680 List<Entity> entities = actualEntity.entityList(); 1681 for (Entity inside : entities) { 1682 _openEntity(inside, tableauxToClose, masterEffigy, graphFrame); 1683 } 1684 } 1685 1686 /** Open the specified entity using the specified configuration. 1687 * This method will recursively descend through the model, opening 1688 * every composite actor and every state refinement. 1689 * @param entity The entity to open. 1690 * @param tableauxToClose A list of tableaux are newly opened. 1691 * @param masterEffigy The top-level effigy for the modeling being exported. 1692 * @param graphFrame The graph frame. 1693 */ 1694 private static void _openEntity(Entity entity, Set<Tableau> tableauxToClose, 1695 Effigy masterEffigy, BasicGraphFrame graphFrame) 1696 throws IllegalActionException { 1697 if (entity instanceof CompositeEntity) { 1698 _openComposite((CompositeEntity) entity, tableauxToClose, 1699 masterEffigy, graphFrame); 1700 } else if (entity instanceof State) { 1701 TypedActor[] refinements = ((State) entity).getRefinement(); 1702 // refinements could be null, see ptolemy/domains/ptides/demo/PtidesBasicPowerPlant/PtidesBasicPowerPlant.xml 1703 if (refinements != null) { 1704 for (TypedActor refinement : refinements) { 1705 _openComposite((CompositeEntity) refinement, 1706 tableauxToClose, masterEffigy, graphFrame); 1707 } 1708 } 1709 } 1710 } 1711 1712 /** Print the HTML in the _contents structure corresponding to the 1713 * specified position to the specified writer. Each item in the 1714 * _contents structure is written on one line. 1715 * @param writer The writer to print to. 1716 * @param position The position. 1717 */ 1718 private void _printHTML(PrintWriter writer, String position) { 1719 List<StringBuffer> contents = _contents.get(position); 1720 for (StringBuffer content : contents) { 1721 writer.println(content); 1722 } 1723 } 1724 1725 /////////////////////////////////////////////////////////////////// 1726 //// private fields //// 1727 1728 /** Data structure storing area attributes to for each Ptolemy II object. */ 1729 private HashMap<NamedObj, HashMap<String, String>> _areaAttributes; 1730 1731 /** Content added by position. */ 1732 private HashMap<String, List<StringBuffer>> _contents; 1733 1734 /** Content of the end section. */ 1735 private LinkedList<StringBuffer> _end; 1736 1737 /** Indicator that an export is in progress. */ 1738 private static boolean _exportInProgress = false; 1739 1740 /** Content of the head section. */ 1741 private LinkedList<StringBuffer> _head; 1742 1743 /** Padding around figures for bounding box. */ 1744 private static double _PADDING = 4.0; 1745 1746 /** The parameters of the current export, if there is one. */ 1747 private ExportParameters _parameters; 1748 1749 /** Indicator of whether title should be shown in HTML. */ 1750 private boolean _showTitleInHTML = false; 1751 1752 /** Content of the start section. */ 1753 private LinkedList<StringBuffer> _start; 1754 1755 /** The sanitized modelName */ 1756 private String _sanitizedModelName; 1757 1758 /** The title of the page. */ 1759 private String _title = "Ptolemy II model"; 1760 1761 /////////////////////////////////////////////////////////////////// 1762 //// IconVisibleLocation 1763 1764 /** A data structure consisting of a NamedObj and the coordinates 1765 * of the upper left corner and lower right corner of the main 1766 * part of its icon (not including decorations like the name 1767 * and any highlights it may have). The coordinates are relative 1768 * to the current visible rectangle, where the upper left corner 1769 * of the visible rectangle has coordinates (0,0), and the lower 1770 * right corner has coordinates (w,h), where w is the width 1771 * and h is the height (in pixels). 1772 */ 1773 static private class IconVisibleLocation { 1774 1775 /** The object with a visible icon. */ 1776 public NamedObj object; 1777 1778 /** The top left X coordinate. */ 1779 public double topLeftX; 1780 1781 /** The top left Y coordinate. */ 1782 public double topLeftY; 1783 1784 /** The bottom right X coordinate. */ 1785 public double bottomRightX; 1786 1787 /** The bottom right Y coordinate. */ 1788 public double bottomRightY; 1789 1790 /** String representation. */ 1791 @Override 1792 public String toString() { 1793 return object.getName() + " from (" + topLeftX + ", " + topLeftY 1794 + ") to (" + bottomRightX + ", " + bottomRightY + ")"; 1795 } 1796 } 1797}