All Downloads are FREE. Search and download functionalities are using the official Maven repository.

playn.html.HtmlAssets Maven / Gradle / Ivy

There is a newer version: 2.0.8
Show newest version
/**
 * Copyright 2010 The PlayN Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package playn.html;

import java.util.HashMap;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.resources.client.DataResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ResourcePrototype;
import com.google.gwt.user.client.Window;
import com.google.gwt.xhr.client.ReadyStateChangeHandler;
import com.google.gwt.xhr.client.XMLHttpRequest;

import playn.core.*;
import playn.html.XDomainRequest.Handler;
import react.RFuture;
import react.RPromise;

public class HtmlAssets extends Assets {

  /**
   * Whether or not to log successful progress of {@code XMLHTTPRequest} and
   * {@code XDomainRequest} requests.
   */
  private static final boolean LOG_XHR_SUCCESS = false;

  private final HtmlPlatform plat;
  private final Map clientBundles =
    new HashMap();
  private String pathPrefix = GWT.getModuleBaseForStaticFiles();
  private Scale assetScale = null;
  private ImageManifest imageManifest;

  /** See {@link #setImageManifest}. */
  public interface ImageManifest {
    /** Returns the pixel {@code {width, height}} for the image at {@code path}.
      * The path will not contain any configured path prefix; it will be the path passed
      * to {@link #getImageSync}. */
    int[] imageSize(String path);
  }

  public void setPathPrefix(String prefix) {
    pathPrefix = prefix;
  }

  /** Configures our image manifest. This is used to support {@link #getImageSync} in the HTML
   * backend. Images for which manifest entries exist will be synchronously loadable and will be
   * configured with their correct size immediately. Their image data will still be loaded
   * asynchronously, but games that were written to use sync loaded images will most likely work
   * with this small hack. */
  public void setImageManifest(ImageManifest manifest) {
    imageManifest = manifest;
  }

  /**
   * Configures the default scale to use for assets. This allows one to specify an intermediate
   * graphics scale (like 1.5) and scale the 2x imagery down to 1.5x instead of scaling the 1.5x
   * imagery up (or displaying nothing at all).
   */
  public void setAssetScale(float scaleFactor) {
    this.assetScale = new Scale(scaleFactor);
  }

  public void addClientBundle(String regExp, AutoClientBundleWithLookup clientBundle) {
    clientBundles.put(regExp, clientBundle);
  }

  @Override
  public Image getImageSync(String path) {
    if (imageManifest == null) throw new UnsupportedOperationException(
      "getImageSync(" + path + ")");
    else {
      for (Scale.ScaledResource rsrc : assetScale().getScaledResources(path)) {
        int[] size = imageManifest.imageSize(rsrc.path);
        if (size == null) continue; // try other scales
        return getImage(rsrc.path, rsrc.scale).preload(size[0], size[1]);
      }
      return new HtmlImage(plat.graphics(), new Throwable("Image missing from manifest: " + path));
    }
  }

  @Override public Image getImage (String path) {
    return getImage(path, Scale.ONE);
  }

  protected HtmlImage getImage (String path, Scale scale) {
    String url = pathPrefix + path;
    AutoClientBundleWithLookup clientBundle = getBundle(path);
    if (clientBundle != null) {
      String key = getKey(path);
      ImageResource resource = (ImageResource) getResource(key, clientBundle);
      if (resource != null) url = resource.getSafeUri().asString();
    }
    return adaptImage(url, scale);
  }

  @Override public Image getRemoteImage(String url) {
    return adaptImage(url, Scale.ONE);
  }

  @Override public Image getRemoteImage(String url, int width, int height) {
    return adaptImage(url, Scale.ONE).preload(width, height);
  }

  @Override
  public Sound getSound(String path) {
    String url = pathPrefix + path;
    AutoClientBundleWithLookup clientBundle = getBundle(path);
    if (clientBundle != null) {
      String key = getKey(path);
      DataResource resource = (DataResource) getResource(key, clientBundle);
      if (resource != null) {
        url = resource.getSafeUri().asString();
      }
    } else {
      url += ".mp3";
    }
    return plat.audio().createSound(url);
  }

  @Override
  public String getTextSync(String path) throws Exception {
    throw new UnsupportedOperationException("getTextSync(" + path + ")");
  }

  @Override
  public RFuture getText(final String path) {
    RPromise result = RPromise.create();
    final String fullPath = pathPrefix + path;
    /*
     * Except for IE, all browsers support on-domain and cross-domain XHR via
     * {@code XMLHTTPRequest}. IE, on the other hand, not only requires the use
     * of a non-standard {@code XDomainRequest} for cross-domain requests, but
     * doesn't allow on-domain requests to be issued via {@code XMLHTTPRequest},
     * even when {@code Access-Control-Allow-Origin} includes the current
     * document origin. Since we here don't always know if the current request
     * will be cross domain, we try XHR, and then fall back to XDR if the we're
     * running on IE.
     */
    try {
      doXhr(fullPath, result);
    } catch (JavaScriptException e) {
      if (Window.Navigator.getUserAgent().indexOf("MSIE") != -1) {
        doXdr(fullPath, result);
      } else {
        throw e;
      }
    }
    return result;
  }

  @Override public byte[] getBytesSync(String path) throws Exception {
    throw new UnsupportedOperationException("getByteSync(" + path + ")");
  }

  @Override public RFuture getBytes(String path) {
    return RFuture.failure(new UnsupportedOperationException("getByte(" + path + ")"));
  }

  @Override protected ImageImpl.Data load (String path) throws Exception {
    throw new UnsupportedOperationException("unused");
  }

  @Override protected ImageImpl createImage (boolean async, int rawWidth, int rawHeight) {
    throw new UnsupportedOperationException("unused");
  }

  HtmlAssets(HtmlPlatform plat) {
    super(plat.exec());
    this.plat = plat;
  }

  private Scale assetScale() {
    return (assetScale != null) ? assetScale : plat.graphics().scale();
  }

  private void doXdr(final String path, final RPromise result) {
    XDomainRequest xdr = XDomainRequest.create();
    xdr.setHandler(new Handler() {
      @Override public void onTimeout(XDomainRequest xdr) {
        plat.log().error("xdr::onTimeout[" + path + "]()");
        result.fail(new Exception("Error getting " + path + " : " + xdr.getStatus()));
      }

      @Override public void onProgress(XDomainRequest xdr) {
        if (LOG_XHR_SUCCESS) plat.log().debug("xdr::onProgress[" + path + "]()");
      }

      @Override public void onLoad(XDomainRequest xdr) {
        if (LOG_XHR_SUCCESS) plat.log().debug("xdr::onLoad[" + path + "]()");
        result.succeed(xdr.getResponseText());
      }

      @Override public void onError(XDomainRequest xdr) {
        plat.log().error("xdr::onError[" + path + "]()");
        result.fail(new Exception("Error getting " + path + " : " + xdr.getStatus()));
      }
    });
    if (LOG_XHR_SUCCESS) plat.log().debug("xdr.open('GET', '" + path + "')...");
    xdr.open("GET", path);
    if (LOG_XHR_SUCCESS) plat.log().debug("xdr.send()...");
    xdr.send();
  }

  private void doXhr(final String path, final RPromise result) {
    XMLHttpRequest xhr = XMLHttpRequest.create();
    xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
      @Override public void onReadyStateChange(XMLHttpRequest xhr) {
        int readyState = xhr.getReadyState();
        if (readyState == XMLHttpRequest.DONE) {
          int status = xhr.getStatus();
          // status code 0 will be returned for non-http requests, e.g. file://
          if (status != 0 && (status < 200 || status >= 400)) {
            plat.log().error("xhr::onReadyStateChange[" + path + "]" +
                             "(readyState = " + readyState + "; status = " + status + ")");
            result.fail(new Exception("Error getting " + path + " : " + xhr.getStatusText()));
          } else {
            if (LOG_XHR_SUCCESS) plat.log().debug("xhr::onReadyStateChange[" + path + "]" +
                                                  "(readyState = " + readyState +
                                                  "; status = " + status + ")");
            result.succeed(xhr.getResponseText());
          }
        }
      }
    });
    if (LOG_XHR_SUCCESS) plat.log().debug("xhr.open('GET', '" + path + "')...");
    xhr.open("GET", path);
    if (LOG_XHR_SUCCESS) plat.log().debug("xhr.send()...");
    xhr.send();
  }

  private String getKey (String fullPath) {
    String key = fullPath.substring(fullPath.lastIndexOf('/') + 1);
    int dotCharIdx = key.indexOf('.');
    return dotCharIdx != -1 ? key.substring(0, dotCharIdx) : key;
  }

  private ResourcePrototype getResource (String key, AutoClientBundleWithLookup clientBundle) {
    ResourcePrototype resource = clientBundle.getResource(key);
    return resource;
  }

  private AutoClientBundleWithLookup getBundle (String collection) {
    AutoClientBundleWithLookup clientBundle = null;
    for (Map.Entry entry : clientBundles.entrySet()) {
      String regExp = entry.getKey();
      if (RegExp.compile(regExp).exec(collection) != null) {
        clientBundle = entry.getValue();
      }
    }
    return clientBundle;
  }

  private HtmlImage adaptImage (String url, Scale scale) {
    ImageElement img = Document.get().createImageElement();
    // when the server provides an appropriate `Access-Control-Allow-Origin` response header,
    // allow images to be served cross origin on supported, CORS enabled, browsers
    setCrossOrigin(img, "anonymous");
    img.setSrc(url);
    return new HtmlImage(plat.graphics(), scale, img);
  }

  /**
   * Set the state of the {@code crossOrigin} attribute for CORS.
   *
   * @param elem the DOM element on which to set the {@code crossOrigin} attribute
   * @param state one of {@code "anonymous"} or {@code "use-credentials"}
   */
  private native void setCrossOrigin(Element elem, String state) /*-{
    if ('crossOrigin' in elem) elem.setAttribute('crossOrigin', state);
  }-*/;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy