/* * Open Source Physics software is free software as described near the bottom of this code file. * * For additional information and documentation on Open Source Physics please see: * <http://www.opensourcephysics.org/> */ package org.opensourcephysics.tools; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; import java.util.TreeSet; import javax.imageio.ImageIO; import javax.swing.SwingWorker; import javax.swing.filechooser.FileFilter; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import org.opensourcephysics.controls.OSPLog; import org.opensourcephysics.controls.XML; import org.opensourcephysics.media.core.VideoFileFilter; import org.opensourcephysics.media.core.VideoIO; import org.opensourcephysics.media.gif.GifDecoder; import org.opensourcephysics.tools.LibraryResource.Metadata; /** * A DefaultMutableTreeNode for a LibraryTreePanel tree, with a LibraryResource user object. * Provides convenience methods for getting, setting and displaying LibraryResource data. * * @author Douglas Brown * @version 1.0 */ public class LibraryTreeNode extends DefaultMutableTreeNode implements Comparable<LibraryTreeNode> { protected static HashMap<String, URL> htmlURLs = new HashMap<String, URL>(); protected static HashMap<String, URL> targetURLs = new HashMap<String, URL>(); protected static Dimension defaultThumbnailDimension = new Dimension(320, 240); protected LibraryResource record; protected boolean editable = true; protected LibraryTreePanel treePanel; protected ArrayList<LibraryResource> resources = new ArrayList<LibraryResource>(); protected String tooltip; protected Metadata selectedMetadata; protected String metadataSource; /** * Constructs a node with a LibraryResource. * * @param resource the resource * @param treePanel the LibraryTreePanel that will use the node */ protected LibraryTreeNode(LibraryResource resource, LibraryTreePanel treePanel) { this.record = resource; this.treePanel = treePanel; if (treePanel.tree!=null) createChildNodes(); setUserObject(this); } /** * Compares this to the specified object. Return true if both nodes have same name, target and HTML. * * @param object the object * @return <code>true</code> if this equals the specified object */ public boolean equals(Object obj) { if (obj==this) return true; if((obj==null) || (obj.getClass()!=this.getClass())) return false; LibraryTreeNode treeNode = (LibraryTreeNode)obj; String target1 = this.getAbsoluteTarget(); String target2 = treeNode.getAbsoluteTarget(); String html1 = this.getHTMLPath(); String html2 = treeNode.getHTMLPath(); return ( ((target1==null&&target2==null) || (target1!=null && target2!=null && target1.equals(target2))) && ((html1==null&&html2==null) || (html1!=null && html2!=null && html1.equals(html2))) && treeNode.getName().equals(this.getName()) ); } /** * Compares this to the specified node. * * @param node the node to compare * @return 0 if equal, otherwise alphabetical name order */ public int compareTo(LibraryTreeNode node) { final int BEFORE = -1; final int EQUAL = 0; final int AFTER = 1; if (this==node || this.equals(node)) return EQUAL; // compare names int result = this.getName().compareTo(node.getName()); if (result!=EQUAL) return result; // compare targets String tar1 = this.getAbsoluteTarget(); String tar2 = node.getAbsoluteTarget(); if (tar1!=null || tar2!=null) { if (tar1==null) return AFTER; if (tar2==null) return BEFORE; result = tar1.compareTo(tar2); } if (result!=EQUAL) return result; // compare HTML paths String html1 = this.getHTMLPath(); String html2 = node.getHTMLPath(); if (html1!=null || html2!=null) { if (html1==null) return AFTER; if (html2==null) return BEFORE; return html1.compareTo(html2); } return EQUAL; } /** * Creates the child nodes of this node if this is a collection node. * * @return true if children were added */ protected boolean createChildNodes() { ArrayList<String> children = new ArrayList<String>(); for (int i=0; i< getChildCount(); i++) { children.add(this.getChildAt(i).toString()); } boolean changed = false; if (record instanceof LibraryCollection) { LibraryCollection collection = (LibraryCollection)record; for (LibraryResource next: collection.getResources()) { if (next!=null && !children.contains(next.getName())) { LibraryTreeNode newNode = new LibraryTreeNode(next, treePanel); if (treePanel.insertChildAt(newNode, this, getChildCount())) { changed = true; } } } } if (changed) treePanel.setChanged(); return changed; } /** * Returns the name of this node's resource. * * @return the name */ protected String getName() { return record.getName(); } /** * Returns the base path of this node's resource. * * @return the base path */ protected String getBasePath() { String base = record.getBasePath(); if (!base.equals("")) //$NON-NLS-1$ return base; LibraryTreeNode parent = (LibraryTreeNode)getParent(); if (parent!=null) return parent.getBasePath(); if (treePanel!=null) { return XML.getDirectoryPath(treePanel.pathToRoot); } return ""; //$NON-NLS-1$ } /** * Returns the html path of this node's resource. * * @return the html path */ protected String getHTMLPath() { String path = record.getHTMLPath(); if (path!=null && !path.trim().equals("")) { //$NON-NLS-1$ path = XML.getResolvedPath(path, getBasePath()); return path; } return null; } /** * Returns the html URL for this node, or null if html path is empty or invalid. * If a cached file exists, this returns its URL instead of the original. * * @return the URL */ protected URL getHTMLURL() { String path = getHTMLPath(); if (path==null) return null; URL url = null; File cachedFile = ResourceLoader.getOSPCacheFile(path); boolean foundInCache = cachedFile.exists(); // see if URL is in the map if (htmlURLs.keySet().contains(path)) { url = htmlURLs.get(path); } else { String workingPath = path; if (foundInCache) { workingPath = cachedFile.toURI().toString(); } // first try to get URL with raw path Resource res = ResourceLoader.getResourceZipURLsOK(workingPath); if (res!=null) { url = res.getURL(); } else { // try with URI form of path workingPath = ResourceLoader.getURIPath(workingPath); res = ResourceLoader.getResourceZipURLsOK(workingPath); if (res!=null) { url = res.getURL(); } } } htmlURLs.put(path, url); return url; } /** * Returns an HTML string that describes this node's resource. * This is displayed if no html URL is available. * * @return the html string */ protected String getHTMLString() { if (!record.getDescription().equals("")) { //$NON-NLS-1$ return record.getDescription(); } boolean isImage = record.getType().equals(LibraryResource.IMAGE_TYPE) && record.getTarget()!=null; boolean isVideo = record.getType().equals(LibraryResource.VIDEO_TYPE) && record.getTarget()!=null; boolean isZip = record.getTarget()!=null && (record.getTarget().toLowerCase().endsWith(".zip") || record.getTarget().toLowerCase().endsWith(".trz")); //$NON-NLS-1$ //$NON-NLS-2$ boolean isThumbnailType = isVideo || isZip || isImage; String thumb = isThumbnailType? record.getThumbnail(): null; if (isThumbnailType && thumb==null) { String source = getAbsoluteTarget(); File thumbFile = getThumbnailFile(); if (thumbFile.exists()) { thumb = thumbFile.getAbsolutePath(); record.setThumbnail(thumb); } else { new ThumbnailLoader(source, thumbFile.getAbsolutePath()).execute(); } } if (thumb!=null) { thumb = XML.forwardSlash(thumb); thumb = ResourceLoader.getURIPath(thumb); } StringBuffer buffer = new StringBuffer(); String collection = " "+ToolsRes.getString("LibraryResource.Type.Collection.Description"); //$NON-NLS-1$ //$NON-NLS-2$ String title = record.getName(); if ("".equals(title) && this.isRoot()) //$NON-NLS-1$ title = record.getTitle(treePanel.pathToRoot); for (String type: LibraryResource.allResourceTypes) { if (type.equals(LibraryResource.UNKNOWN_TYPE)) continue; if (type.equals(LibraryResource.PDF_TYPE)) continue; String[] types = new String[] {type}; if (type.equals(LibraryResource.HTML_TYPE)) { type = "Other"; //$NON-NLS-1$ types = new String[] {LibraryResource.HTML_TYPE, LibraryResource.PDF_TYPE, LibraryResource.UNKNOWN_TYPE}; } ArrayList<LibraryResource> children = getChildResources(types); if (!children.isEmpty()) { // node has children String s = "LibraryResource.Type."+type+".List"; //$NON-NLS-1$ //$NON-NLS-2$ buffer.append("<p>"+ToolsRes.getString(s) //$NON-NLS-1$ +" "+title+collection+":</p>\n"); //$NON-NLS-1$//$NON-NLS-2$ buffer.append("<ol>\n"); //$NON-NLS-1$ for (LibraryResource next: children) { String name = next.getName(); if (name.equals("")) //$NON-NLS-1$ name = ToolsRes.getString("LibraryResource.Name.Default"); //$NON-NLS-1$ buffer.append("<li>"+name+"</li>\n"); //$NON-NLS-1$ //$NON-NLS-2$ } buffer.append("</ol>\n"); //$NON-NLS-1$ } } String description = buffer.toString(); String htmlCode = LibraryResource.getHTMLBody(title, record.getType(), thumb, description, null, null, null, null); return htmlCode; } /** * Returns the target of this node's resource. * The target may be absolute or relative to base path. * * @return the target */ protected String getTarget() { return record.getTarget(); } /** * Returns the absolute target path of this node's resource. * * @return the absolute target path */ protected String getAbsoluteTarget() { if (getTarget()==null) return null; if (record instanceof LibraryCollection) { return getBasePath()+getTarget(); } return XML.getResolvedPath(getTarget(), getBasePath()); } /** * Returns the target URL for this node, or null if target is empty or invalid. * If a cached file exists, this returns its URL instead of the original. * * @return the URL */ protected URL getTargetURL() { String path = getAbsoluteTarget(); if (path==null) return null; String workingPath = path; URL url = null; String filename = record.getProperty("download_filename"); //$NON-NLS-1$ File cachedFile = ResourceLoader.getOSPCacheFile(path, filename); boolean foundInCache = cachedFile.exists(); if (foundInCache) { workingPath = ResourceLoader.getURIPath(cachedFile.getAbsolutePath()); } // see if URL is in the map if (targetURLs.keySet().contains(path)) { url = targetURLs.get(path); } else { // first try to get URL with raw path Resource res = ResourceLoader.getResourceZipURLsOK(workingPath); if (res!=null) { url = res.getURL(); } else { // try with URI form of path workingPath = ResourceLoader.getURIPath(workingPath); res = ResourceLoader.getResourceZipURLsOK(workingPath); if (res!=null) { url = res.getURL(); } } } targetURLs.put(path, url); return url; } /** * Used by the tree node to get the display name. * * @return the display name of the node */ @Override public String toString() { return record.toString(); } /** * Determines if this node is editable. * Note: returns true only if this and its parent are editable. * * @return true of editable */ protected boolean isEditable() { if (isRoot()) return editable; LibraryTreeNode parent = (LibraryTreeNode)getParent(); return editable && parent.isEditable(); } /** * Sets the editable property for this node. * * @param edit true to make this node editable */ protected void setEditable(boolean edit) { editable = edit; } /** * Sets the name of this node's resource. * * @param name the name */ protected void setName(String name) { if (record.setName(name)) { treePanel.tree.getModel().valueForPathChanged(new TreePath(getPath()), name); treePanel.showInfo(this); treePanel.setChanged(); } } /** * Sets the target of this node's resource. May be absolute or relative path. * * @param path the target path * @return true if changed */ protected boolean setTarget(String path) { if (record.setTarget(path)) { // target has changed if (path==null) path = ""; //$NON-NLS-1$ if (path.toLowerCase().endsWith(".trk") || path.toLowerCase().endsWith(".trz")) //$NON-NLS-1$ //$NON-NLS-2$ setType(LibraryResource.TRACKER_TYPE); else if (path.indexOf("EJS")>-1) { //$NON-NLS-1$ setType(LibraryResource.EJS_TYPE); } else if (path.toLowerCase().endsWith(".zip")) { //$NON-NLS-1$ Runnable runner = new Runnable() { public void run() { String zipPath = getAbsoluteTarget(); Set<String> files = ResourceLoader.getZipContents(zipPath); for (String next: files) { if (next.endsWith(".trk")) { //$NON-NLS-1$ setType(LibraryResource.TRACKER_TYPE); break; } } } }; new Thread(runner).start(); } else if (path.equals("")) { //$NON-NLS-1$ if (getHTMLPath()==null) setType(LibraryResource.UNKNOWN_TYPE); else setType(LibraryResource.HTML_TYPE); } else { boolean found = false; for (FileFilter next: LibraryResource.imageFilters) { if (found) break; VideoFileFilter filter = (VideoFileFilter)next; for (String ext: filter.getExtensions()) { if (path.toUpperCase().endsWith("."+ext.toUpperCase())) { //$NON-NLS-1$ setType(LibraryResource.IMAGE_TYPE); found = true; } } } for (String ext: VideoIO.getVideoExtensions()) { if (found) break; if (path.toUpperCase().endsWith("."+ext.toUpperCase())) { //$NON-NLS-1$ setType(LibraryResource.VIDEO_TYPE); found = true; } } } LibraryTreePanel.htmlPanesByNode.remove(this); record.setThumbnail(null); treePanel.showInfo(this); treePanel.setChanged(); tooltip = null; // triggers new tooltip return true; } return false; } /** * Sets the html path of this node's resource. * * @param path the html path */ protected void setHTMLPath(String path) { if (record.setHTMLPath(path)) { treePanel.showInfo(this); treePanel.setChanged(); tooltip = null; // triggers new tooltip } } /** * Sets the base path of this node's resource. * * @param path the base path */ protected void setBasePath(String path) { if (record.setBasePath(path)) { LibraryTreePanel.htmlPanesByNode.remove(this); record.setThumbnail(null); treePanel.showInfo(this); treePanel.setChanged(); } } /** * Sets the type of this node's resource. * The types are static constants defined by LibraryResource. * * @param type the type */ protected void setType(String type) { if (record.setType(type)) { LibraryTreePanel.htmlPanesByNode.remove(this); treePanel.showInfo(this); treePanel.setChanged(); tooltip = null; // triggers new tooltip } } /** * Returns this node's child resources, if any, of a given set of types. * The types are static constants defined by LibraryResource. * * @param types an array of resource types * @return a list of LibraryResources */ protected ArrayList<LibraryResource> getChildResources(String[] types) { resources.clear(); for (String type: types) { for (int i=0; i<getChildCount(); i++) { LibraryTreeNode child = (LibraryTreeNode)getChildAt(i); if (child.record.getType().equals(type)) resources.add(child.record); } } return resources; } /** * Returns the (multiline) tooltip for this node. * * @return tooltip String */ protected String getToolTip() { if (tooltip==null) { StringBuffer buf = new StringBuffer(); // add path to collection types if (record.getType().equals(LibraryResource.COLLECTION_TYPE)) { if (isRoot()) { if (!"".equals(treePanel.pathToRoot)) { //$NON-NLS-1$ buf.append(ToolsRes.getString("LibraryTreeNode.Tooltip.CollectionPath")+": "+treePanel.pathToRoot); //$NON-NLS-1$ //$NON-NLS-2$ } } } // add metadata Set<Metadata> data = record.getMetadata(); if (data!=null) { for (Metadata next: data) { String key = next.getData()[0]; String value = next.getData()[1]; boolean breakLine = false; for (String metadataType: LibraryResource.META_TYPES) { if (metadataType.toLowerCase().contains(key.toLowerCase())) key = ToolsRes.getString("LibraryTreePanel.Label."+metadataType); //$NON-NLS-1$ breakLine = metadataType.equals(LibraryResource.META_KEYWORDS); } if (breakLine && value.length()>100) { int len = key.length(); String space = ""; //$NON-NLS-1$ for (int i=0; i<len; i++) { space += " "; //$NON-NLS-1$ } StringBuffer b = new StringBuffer(); String line = value.substring(0, 80); String remainder = value.substring(80); while (true) { String[] parts = remainder.split(" ", 2); //$NON-NLS-1$ b.append(line+parts[0]); if (parts.length==1) break; if (parts[1].length()<100) { b.append("\n"+space+parts[1]); //$NON-NLS-1$ break; } b.append("\n"+space); //$NON-NLS-1$ line = parts[1].substring(0, 80); remainder = parts[1].substring(80); } value = b.toString(); } if (buf.length()>0) buf.append("\n"); //$NON-NLS-1$ buf.append(key+": "+value); //$NON-NLS-1$ } } tooltip = buf.toString(); } return tooltip.length()>0? tooltip: ToolsRes.getString("LibraryTreeNode.Tooltip.None"); //$NON-NLS-1$ } /** * Returns the path to a source of metadata (usually HTML path) * * @return the metadata path */ protected String getMetadataSourcePath() { String path = metadataSource; if (path!=null) { return XML.getResolvedPath(path, getBasePath()); } return getHTMLPath(); } /** * Returns the metadata for this node. * * @return a Set of Metadata entries */ protected TreeSet<Metadata> getMetadata() { TreeSet<Metadata> searchData = record.getMetadata(); if (searchData==null) { searchData = new TreeSet<Metadata>(); record.setMetadata(searchData); // look for metadata in HTML code String path = getMetadataSourcePath(); if (path!=null) { Resource res = ResourceLoader.getResourceZipURLsOK(path); String code = res.getString(); if (code!=null) { boolean[] isStandardType = new boolean[LibraryResource.META_TYPES.length]; String[] parts = code.split("<meta name="); //$NON-NLS-1$ for (int i=1; i<parts.length; i++) { // ignore parts[0] // parse metadata and add to HashMap int n = parts[i].indexOf("\">"); //$NON-NLS-1$ if (n>-1) { parts[i] = parts[i].substring(0, n); String[] subparts = parts[i].split("content=\""); //$NON-NLS-1$ if (subparts.length>1) { // subparts[0] is name in quotes String name = subparts[0].trim(); if (name.startsWith("\"")) name = name.substring(1); //$NON-NLS-1$ if (name.endsWith("\"")) name = name.substring(0, name.length()-1); //$NON-NLS-1$ // assign to standard metadata type if appropriate for (int k=0; k<LibraryResource.META_TYPES.length; k++) { if (!isStandardType[k] && LibraryResource.META_TYPES[k].toLowerCase().contains(name.toLowerCase())) { name = LibraryResource.META_TYPES[k]; isStandardType[k] = true; } } // subparts[1] is value String value = subparts[1].trim(); record.addMetadata(new Metadata(name, value)); } } } } } tooltip = null; } return searchData; } /** * Returns the metadata value of a specified type. * @param key the type of the metadata * @return the value, or null if none */ protected String getMetadataValue(String key) { Set<Metadata> searchData = record.getMetadata(); if (searchData!=null) { for (Metadata next: searchData) { if (next.getData()[0].indexOf(key)>-1) return next.getData()[1]; } } return null; } /** * Returns a File that points to the cached thumbnail, if any, for this node. * Note: the thumbnail file may not exist--this just determines where it should be. * * @return the thumbnail File, whether or not it exists */ protected File getThumbnailFile() { String thumbPath = record.getThumbnail(); if (thumbPath!=null) return new File(thumbPath); String path = getAbsoluteTarget(); String name = XML.stripExtension(XML.getName(path)); String fileName = name+"_thumbnail.png"; //$NON-NLS-1$ return ResourceLoader.getOSPCacheFile(path, fileName); } /** * Creates a thumbnail image and writes it to a specified path. * @param image the full-size image from which to create the thumbnail * @param path the path for the thumbnail image file * @param maxSize the maximum size of the thumbnail image * @return the thumbnail File, or null if failed */ protected File createThumbnailFile(BufferedImage image, String path, Dimension maxSize) { // determine image resize factor double widthFactor = maxSize.getWidth()/image.getWidth(); double heightFactor = maxSize.getHeight()/image.getHeight(); double factor = Math.min(widthFactor, heightFactor); // determine dimensions of thumbnail image int w = (int)(image.getWidth()*factor); int h = (int)(image.getHeight()*factor); // create and draw thumbnail image BufferedImage thumbnailImage = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR); Graphics2D g = thumbnailImage.createGraphics(); AffineTransform transform = AffineTransform.getScaleInstance(factor, factor); g.setTransform(transform); g.drawImage(image, 0, 0, null); // write thumbnail image to file return VideoIO.writeImageFile(thumbnailImage, path); } /** * A SwingWorker class to create new thumbnails. */ class ThumbnailLoader extends SwingWorker<File, Object> { String thumbPath, sourcePath; ThumbnailLoader(String imageSource, String thumbnailPath) { thumbPath = thumbnailPath; sourcePath = imageSource; } @Override public File doInBackground() { // create a new thumbnail File thumbFile = null; String ext = XML.getExtension(sourcePath); // GIF files if (ext!=null && "GIF".equals(ext.toUpperCase())) { //$NON-NLS-1$ GifDecoder decoder = new GifDecoder(); int status = decoder.read(sourcePath); if(status!=0) { // error OSPLog.fine("failed to create thumbnail for GIF "+thumbPath); //$NON-NLS-1$ } else { BufferedImage image = decoder.getImage(); Dimension size = new Dimension(image.getWidth(), image.getHeight()); thumbFile = createThumbnailFile(image, thumbPath, size); } } // PNG and JPEG files else if (ext!=null && ("PNG".equals(ext.toUpperCase()) || ext.toUpperCase().contains("JP"))) { //$NON-NLS-1$ //$NON-NLS-2$ try { URL url = new URL(ResourceLoader.getURIPath(sourcePath)); BufferedImage image = ImageIO.read(url); Dimension size = new Dimension(image.getWidth(), image.getHeight()); thumbFile = createThumbnailFile(image, thumbPath, size); } catch (Exception e) { OSPLog.fine("failed to create thumbnail for "+thumbPath); //$NON-NLS-1$ } } // ZIP files else if (ext!=null && ("ZIP".equals(ext.toUpperCase()) || "TRZ".equals(ext.toUpperCase()))) { //$NON-NLS-1$ //$NON-NLS-2$ // look for image file in zip with name that includes "_thumbnail" for (String next: ResourceLoader.getZipContents(sourcePath)) { if (next.indexOf("_thumbnail")>-1) { //$NON-NLS-1$ String s = ResourceLoader.getURIPath(sourcePath+"!/"+next); //$NON-NLS-1$ thumbFile = JarTool.extract(s, new File(thumbPath)); } } } // video files: use Xuggle thumbnail tool, if available else { String className = "org.opensourcephysics.media.xuggle.XuggleThumbnailTool"; //$NON-NLS-1$ Class<?>[] types = new Class<?>[] {Dimension.class, String.class, String.class}; Object[] values = new Object[] {defaultThumbnailDimension, sourcePath, thumbPath}; try { Class<?> xuggleClass = Class.forName(className); Method method=xuggleClass.getMethod("createThumbnailFile", types); //$NON-NLS-1$ thumbFile = (File)method.invoke(null, values); } catch(Exception ex) { OSPLog.fine("failed to create thumbnail: "+ex.toString()); //$NON-NLS-1$ } catch(Error err) { } } return thumbFile; } @Override protected void done() { try { File thumbFile = get(); record.setThumbnail(thumbFile==null || !thumbFile.exists()? null: thumbFile.getAbsolutePath()); if (record.getThumbnail()!=null) { LibraryTreePanel.htmlPanesByNode.remove(LibraryTreeNode.this); treePanel.showInfo(treePanel.getSelectedNode()); } } catch (Exception ignore) { } } } } /* * Open Source Physics software is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License (GPL) as * published by the Free Software Foundation; either version 2 of the License, * or(at your option) any later version. * Code that uses any portion of the code in the org.opensourcephysics package * or any subpackage (subdirectory) of this package must must also be be released * under the GNU GPL license. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA * or view the license online at http://www.gnu.org/copyleft/gpl.html * * Copyright (c) 2007 The Open Source Physics project * http://www.opensourcephysics.org */