/* * $RCSfile: SVGImageImpl.java,v $ * * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program 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 version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ package com.sun.perseus.model; import com.sun.perseus.platform.GZIPSupport; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.svg.SVGElement; import org.w3c.dom.DOMException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Vector; import com.sun.perseus.util.SVGConstants; import com.sun.perseus.builder.ModelBuilder; import com.sun.perseus.builder.SVGTinyModelFactory; import javax.microedition.m2g.SVGImage; import javax.microedition.m2g.ScalableImage; import javax.microedition.m2g.ExternalResourceHandler; import org.xml.sax.SAXParseException; import com.sun.perseus.j2d.ImageLoaderUtil; import com.sun.perseus.j2d.RasterImage; import com.sun.perseus.model.DocumentNode; import com.sun.perseus.model.ElementNode; import com.sun.perseus.model.ModelEvent; import com.sun.perseus.model.ModelNode; import com.sun.perseus.model.SVG; import com.sun.perseus.model.Time; import com.sun.perseus.model.UpdateAdapter; /** * */ public class SVGImageImpl extends SVGImage { /** * Size of the byte buffer used to read image input streams handed * by ExternalResourceHandler implementations. */ static final int DEFAULT_IMAGE_READ_BUFFER_LENGTH = 64; /** * The image document */ DocumentNode documentNode = null; /** * The default width for an empty SVG image */ final static public int DEFAULT_WIDTH = 100; /** * The default height for an empty SVG image */ final static public int DEFAULT_HEIGHT = 100; /** * The SVGImageLoader */ SVGImageLoader svgImageLoader; /** * The current SVGElement with the focus */ SVGElement lastElement = null; /** * URI requested by SVGImageLoader's getImageAndWait(). */ String waitURI = new String(); /** * Received null resourceData in requestCompleted(). Used in * getImageAndWait() case. */ boolean isBrokenImage = false; /** * Returns the associated <code>Document</code>. * * @return the associated <code>Document</code>. */ public Document getDocument() { return (Document) documentNode; } /** * Private constructor. Requires a non null DocumentNode. * * @param documentNode the associated DocumentNode. Should not be null. * @param ExternalResourceHandler the associated handler. */ public SVGImageImpl(final DocumentNode documentNode, final ExternalResourceHandler handler) { this.documentNode = documentNode; if (handler != null) { svgImageLoader = new SVGImageLoader(this, handler); documentNode.setImageLoader(svgImageLoader); } else { documentNode.setImageLoader(new DefaultImageLoader()); } } /** * The method creates an empty SVGImage that can be used to build a SVG * image. * * Please keep in the mind that a root <svg> element with default * viewport size (100x100) is also created by the method. * * <p>The engine will invoke a hanlder (if any specifed) for each external * resource referenced in the document. If the handler is null, the * SVGImage will try to load images using the engine default * implementation, but might fail while loading some of them.</p> * * <p>Note: the handler is also called by the application when xlink:href * attribute on <image> is set or changed, anyway the * invocation is done only if the element is hooked into the document tree. * * @param handler implementation of the ExternalResourceHandler interface * @see javax.microedition.m2g.ExternalResourceHandler * * @return an newly created empty SVGImage */ public static SVGImage createEmptyImage(ExternalResourceHandler handler) { DocumentNode documentNode = new DocumentNode(); SVG svg = new SVG(documentNode); svg.setWidth((float) DEFAULT_WIDTH); svg.setHeight((float) DEFAULT_HEIGHT); documentNode.add(svg); documentNode.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); SVGImageImpl sii = new SVGImageImpl(documentNode, handler); // Add prototypes for the SVG Tiny elements. Vector prototypes = SVGTinyModelFactory.getPrototypes(documentNode); int n = prototypes.size(); for (int i = 0; i < n; i++) { documentNode.addPrototype((ElementNode) prototypes.elementAt(i)); } // Initialize the timing engine and sample at time zero. documentNode.initializeTimingEngine(); documentNode.sample(new Time(0)); documentNode.setLoaded(true); return sii; } /** * This method is used to dispatch a mouse event of the specified * <code>type</code> to the document. The mouse position is given as screen * coordinates <code>x, y</code>. The only mouse event type supported is * "click". Note that when a "click" event is dispatched, a "DOMActivate" is * automatically dispatched by the underlying implementation. If a different * type is specified, a DOMException with error code NOT_SUPPORTED_ERR is * thrown. In the case, where x, y values are outside the viewport area or * no target is available for the x, y coordinates, the event is not * dispatched. * * * @param type the type of mouse event. * @param x the x location of the mouse/pointer in viewport coordinate * system. * @param y the y location of the mouse/pointer in viewport coordinate * system. * * @throws DOMException with error code NOT_SUPPORTED_ERR: if the event * <code>type</code> is not supported. * @throws NullPointerException if <code>type</code> is null. * @throws IllegalArgumentException if the x or y values are negative. * */ public void dispatchMouseEvent(String type, int x, int y) throws NullPointerException, IllegalArgumentException, DOMException { if (type == null) { throw new NullPointerException(); } if (x < 0 || y < 0) { throw new IllegalArgumentException(); } if (!type.equals(SVGConstants.SVG_CLICK_EVENT_TYPE)) { throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Event type is NOT click."); } // CHECK x,y outside the viewport area if (x > getViewportWidth() || y > getViewportHeight()) { return; } // Get the target hit by the mouse event float[] pt = {x, y}; ModelNode target = documentNode.nodeHitAt(pt); if (target == null) { return; } // Dispatch an activate on the element that was clicked on. documentNode.dispatchEvent(documentNode.initEngineEvent( SVGConstants.SVG_DOMACTIVATE_EVENT_TYPE, target)); // Now, dispatch the click event. documentNode.dispatchEvent (documentNode.initEngineEvent(SVGConstants.SVG_CLICK_EVENT_TYPE, target)); }; /** * */ public void activate() { if (lastElement == null) { return; } documentNode.dispatchEvent(documentNode.initEngineEvent( SVGConstants.SVG_DOMACTIVATE_EVENT_TYPE, (ModelNode) lastElement)); }; /** * */ public void focusOn(SVGElement element) throws DOMException { if (element == null) { if (lastElement != null) { // remove current focus documentNode.dispatchEvent(documentNode.initEngineEvent( SVGConstants.SVG_DOMFOCUSOUT_EVENT_TYPE, (ModelNode) lastElement)); lastElement = null; } return; } DocumentNode ownerDocument = ((ModelNode) element).getOwnerDocument(); if (!ownerDocument.equals(documentNode)) { throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, "Invalid element."); } // If this element is different from the previous element focused, // dispatch a "DOMFocusOut" event to the previous element. if (lastElement != element) { if (lastElement != null) { documentNode.dispatchEvent(documentNode.initEngineEvent( SVGConstants.SVG_DOMFOCUSOUT_EVENT_TYPE, (ModelNode) lastElement)); } documentNode.dispatchEvent(documentNode.initEngineEvent( SVGConstants.SVG_DOMFOCUSIN_EVENT_TYPE, (ModelNode) element)); lastElement = element; } }; /** * */ public void incrementTime(final float seconds) { if (documentNode.updateQueue == null) { // We are not running with an update queue, which means we are // not running withing an SVGAnimator. We increment time // and apply animations. documentNode.incrementTime(seconds); documentNode.applyAnimations(); } else { // The impact of changing the time must be made visible immediately // because the document is being displayed in an // SVGAnimator. Therefore, we force the document to sample and apply // animations so that resulting rendering changes get displayed // immediately. // We are running in an update queue. Check whether or not we are // already in the update thread. if (Thread.currentThread() == documentNode.updateQueue.getThread()) { // We are already in the right thread. documentNode.incrementTime(seconds); documentNode.applyAnimations(); } else { // We are in the wrong thread. Move to the right thread. documentNode.safeInvokeAndWait(new Runnable() { public void run() { documentNode.incrementTime(seconds); documentNode.applyAnimations(); } }); } } } // Inherited from ScalableImage... /** * */ public static ScalableImage createImage (InputStream stream, ExternalResourceHandler handler) throws IOException { if (stream == null) { throw new NullPointerException(); } DocumentNode documentNode = new DocumentNode(); return loadDocument(documentNode, stream, handler); } /** * Implementation helper. * * @param documentNode the <code>DocumentNode</code> object into which the * content of the stream should be loaded. * @param is the <code>InputStream</code> to the content to be loaded. * @param handler the <code>ExternalResourceHandler</code>. May be null. */ private static ScalableImage loadDocument( final DocumentNode documentNode, final InputStream is, final ExternalResourceHandler handler) throws IOException { UpdateAdapter updateAdapter = new UpdateAdapter(); documentNode.setUpdateListener(updateAdapter); SVGImageImpl sii = new SVGImageImpl(documentNode, handler); ModelBuilder.loadDocument(is, documentNode); if (updateAdapter.hasLoadingFailed()) { if (updateAdapter.getLoadingFailedException() != null) { throw new IOException (updateAdapter.getLoadingFailedException().getMessage()); } throw new IOException(); } // Now, get image width/height from <svg> element and set it in // DocumentNode Element root = documentNode.getDocumentElement(); if (!(root instanceof SVG)) { throw new IOException(Messages.formatMessage( Messages.ERROR_NON_SVG_RESOURCE, new String[] {documentNode.getURIBase()})); } SVG svg = (SVG) root; int width = (int) svg.getWidth(); int height = (int) svg.getHeight(); documentNode.setSize(width, height); // Now, initialize the timing engine and sample at zero. documentNode.initializeTimingEngine(); documentNode.sample(new Time(0)); return sii; } /** * */ public static ScalableImage createImage( String URL, ExternalResourceHandler handler) throws IOException, SecurityException { if (URL == null) { throw new NullPointerException(); } DocumentNode documentNode = new DocumentNode(); documentNode.setDocumentURI(URL); InputStream is = null; try { is = GZIPSupport.openHandleGZIP(URL); } catch (IOException ioe) { throw new IllegalArgumentException(ioe.getMessage()); } return loadDocument(documentNode, is, handler); } /** * An area where the ScalableImage is rendered is called viewport. * If a part of the viewport lays outside of the target clipping * rectangle it is clipped. The viewport coordinates are given * relative to the target rendering surface. * */ public void setViewportWidth(int width) { if (width < 0) { throw new IllegalArgumentException(); } documentNode.setSize(width, documentNode.getHeight()); } /** * */ public void setViewportHeight(int height) { if (height < 0) { throw new IllegalArgumentException(); } documentNode.setSize(documentNode.getWidth(), height); } /** * The initial viewport width is the "width" value specified in * the Scalable Image. The returned value is always in pixels. If the * width is given in percentages, the values are mapped to the * default viewport size (100x100). * If the application changes the width of viewport explicitly, * then the percentages are ignored and the content is fit to new * viewport width. * */ public int getViewportWidth() { return documentNode.getWidth(); } /** * The initial viewport height is the "height" value specified in * the Scalable Image. The returned value is always in pixels. If the * the height is given in percentages, the values are mapped to the * default viewport size (100x100). * If the application changes the height of viewport explicitly, * then the percentages are ignored and the content is fit to new * viewport height. * */ public int getViewportHeight() { return documentNode.getHeight(); } /** * */ public void requestCompleted(String uri, InputStream resourceData) throws IOException { System.err.println(">>>>> requestCompleted : " + uri + " / " + resourceData); if (uri == null) { throw new NullPointerException(); } synchronized (this) { // set in getImageAndWait() boolean isWaitURI = waitURI.equals(uri); RasterImage img; // null resourceData... if (resourceData == null) { img = svgImageLoader.getBrokenImage(); isBrokenImage = true; } else { // we got a fresh, new image... ByteArrayOutputStream bos = new ByteArrayOutputStream( DEFAULT_IMAGE_READ_BUFFER_LENGTH); byte[] ib = new byte[DEFAULT_IMAGE_READ_BUFFER_LENGTH]; int byteRead = -1; int totalByteRead = 0; while ((byteRead = resourceData.read(ib, 0, ib.length)) != -1) { bos.write(ib, 0, byteRead); totalByteRead += byteRead; } img = svgImageLoader.loaderUtil.createImage(bos.toByteArray()); } // the new image is added to cache... svgImageLoader.addToCache(uri, img); svgImageLoader.setRasterImageConsumerImage(uri, img); // request was initiated by getImageAndWait if (isWaitURI) { notifyAll(); } } }; synchronized RasterImage waitOnRequestCompleted(final String uri) { waitURI = uri; try { while (isBrokenImage == false && svgImageLoader.getImageFromCache(uri) == null) { wait(); } } catch (InterruptedException ie) { return svgImageLoader.getBrokenImage(); } waitURI = new String(); if (isBrokenImage) { isBrokenImage = false; return svgImageLoader.getBrokenImage(); } else { return svgImageLoader.getImageFromCache(uri); } } }