123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- import { WebRequest } from './webRequest';
- import { DomManagement } from './domManagement';
- import { Nullable } from '../types';
- import { IOfflineProvider } from '../Offline/IOfflineProvider';
- import { IFileRequest } from './fileRequest';
- import { Observable } from './observable';
- import { FilesInputStore } from './filesInputStore';
- import { RetryStrategy } from './retryStrategy';
- import { BaseError } from './baseError';
- import { StringTools } from './stringTools';
- import { ThinEngine } from '../Engines/thinEngine';
- import { ShaderProcessor } from '../Engines/Processors/shaderProcessor';
- /** @ignore */
- export class LoadFileError extends BaseError {
- public request?: WebRequest;
- public file?: File;
- /**
- * Creates a new LoadFileError
- * @param message defines the message of the error
- * @param request defines the optional web request
- * @param file defines the optional file
- */
- constructor(message: string, object?: WebRequest | File) {
- super(message);
- this.name = "LoadFileError";
- BaseError._setPrototypeOf(this, LoadFileError.prototype);
- if (object instanceof WebRequest) {
- this.request = object;
- }
- else {
- this.file = object;
- }
- }
- }
- /** @ignore */
- export class RequestFileError extends BaseError {
- /**
- * Creates a new LoadFileError
- * @param message defines the message of the error
- * @param request defines the optional web request
- */
- constructor(message: string, public request: WebRequest) {
- super(message);
- this.name = "RequestFileError";
- BaseError._setPrototypeOf(this, RequestFileError.prototype);
- }
- }
- /** @ignore */
- export class ReadFileError extends BaseError {
- /**
- * Creates a new ReadFileError
- * @param message defines the message of the error
- * @param file defines the optional file
- */
- constructor(message: string, public file: File) {
- super(message);
- this.name = "ReadFileError";
- BaseError._setPrototypeOf(this, ReadFileError.prototype);
- }
- }
- /**
- * @hidden
- */
- export class FileTools {
- /**
- * Gets or sets the retry strategy to apply when an error happens while loading an asset
- */
- public static DefaultRetryStrategy = RetryStrategy.ExponentialBackoff();
- /**
- * Gets or sets the base URL to use to load assets
- */
- public static BaseUrl = "";
- /**
- * Default behaviour for cors in the application.
- * It can be a string if the expected behavior is identical in the entire app.
- * Or a callback to be able to set it per url or on a group of them (in case of Video source for instance)
- */
- public static CorsBehavior: string | ((url: string | string[]) => string) = "anonymous";
- /**
- * Gets or sets a function used to pre-process url before using them to load assets
- */
- public static PreprocessUrl = (url: string) => {
- return url;
- }
- /**
- * Removes unwanted characters from an url
- * @param url defines the url to clean
- * @returns the cleaned url
- */
- private static _CleanUrl(url: string): string {
- url = url.replace(/#/mg, "%23");
- return url;
- }
- /**
- * Sets the cors behavior on a dom element. This will add the required Tools.CorsBehavior to the element.
- * @param url define the url we are trying
- * @param element define the dom element where to configure the cors policy
- */
- public static SetCorsBehavior(url: string | string[], element: { crossOrigin: string | null }): void {
- if (url && url.indexOf("data:") === 0) {
- return;
- }
- if (FileTools.CorsBehavior) {
- if (typeof (FileTools.CorsBehavior) === 'string' || this.CorsBehavior instanceof String) {
- element.crossOrigin = <string>FileTools.CorsBehavior;
- }
- else {
- var result = FileTools.CorsBehavior(url);
- if (result) {
- element.crossOrigin = result;
- }
- }
- }
- }
- /**
- * Loads an image as an HTMLImageElement.
- * @param input url string, ArrayBuffer, or Blob to load
- * @param onLoad callback called when the image successfully loads
- * @param onError callback called when the image fails to load
- * @param offlineProvider offline provider for caching
- * @param mimeType optional mime type
- * @returns the HTMLImageElement of the loaded image
- */
- public static LoadImage(input: string | ArrayBuffer | ArrayBufferView | Blob, onLoad: (img: HTMLImageElement | ImageBitmap) => void, onError: (message?: string, exception?: any) => void, offlineProvider: Nullable<IOfflineProvider>, mimeType: string = ""): Nullable<HTMLImageElement> {
- let url: string;
- let usingObjectURL = false;
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
- if (typeof Blob !== 'undefined') {
- url = URL.createObjectURL(new Blob([input], { type: mimeType }));
- usingObjectURL = true;
- } else {
- url = `data:${mimeType};base64,` + StringTools.EncodeArrayBufferToBase64(input);
- }
- }
- else if (input instanceof Blob) {
- url = URL.createObjectURL(input);
- usingObjectURL = true;
- }
- else {
- url = FileTools._CleanUrl(input);
- url = FileTools.PreprocessUrl(input);
- }
- if (typeof Image === "undefined") {
- FileTools.LoadFile(url, (data) => {
- createImageBitmap(new Blob([data], { type: mimeType })).then((imgBmp) => {
- onLoad(imgBmp);
- if (usingObjectURL) {
- URL.revokeObjectURL(url);
- }
- }).catch((reason) => {
- if (onError) {
- onError("Error while trying to load image: " + input, reason);
- }
- });
- }, undefined, offlineProvider || undefined, true, (request, exception) => {
- if (onError) {
- onError("Error while trying to load image: " + input, exception);
- }
- });
- return null;
- }
- var img = new Image();
- FileTools.SetCorsBehavior(url, img);
- const loadHandler = () => {
- img.removeEventListener("load", loadHandler);
- img.removeEventListener("error", errorHandler);
- onLoad(img);
- // Must revoke the URL after calling onLoad to avoid security exceptions in
- // certain scenarios (e.g. when hosted in vscode).
- if (usingObjectURL && img.src) {
- URL.revokeObjectURL(img.src);
- }
- };
- const errorHandler = (err: any) => {
- img.removeEventListener("load", loadHandler);
- img.removeEventListener("error", errorHandler);
- if (onError) {
- const inputText = input.toString();
- onError("Error while trying to load image: " + (inputText.length < 32 ? inputText : inputText.slice(0, 32) + "..."), err);
- }
- if (usingObjectURL && img.src) {
- URL.revokeObjectURL(img.src);
- }
- };
- img.addEventListener("load", loadHandler);
- img.addEventListener("error", errorHandler);
- var noOfflineSupport = () => {
- img.src = url;
- };
- var loadFromOfflineSupport = () => {
- if (offlineProvider) {
- offlineProvider.loadImage(url, img);
- }
- };
- if (url.substr(0, 5) !== "data:" && offlineProvider && offlineProvider.enableTexturesOffline) {
- offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
- }
- else {
- if (url.indexOf("file:") !== -1) {
- var textureName = decodeURIComponent(url.substring(5).toLowerCase());
- if (FilesInputStore.FilesToLoad[textureName]) {
- try {
- var blobURL;
- try {
- blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
- }
- catch (ex) {
- // Chrome doesn't support oneTimeOnly parameter
- blobURL = URL.createObjectURL(FilesInputStore.FilesToLoad[textureName]);
- }
- img.src = blobURL;
- usingObjectURL = true;
- }
- catch (e) {
- img.src = "";
- }
- return img;
- }
- }
- noOfflineSupport();
- }
- return img;
- }
- /**
- * Reads a file from a File object
- * @param file defines the file to load
- * @param onSuccess defines the callback to call when data is loaded
- * @param onProgress defines the callback to call during loading process
- * @param useArrayBuffer defines a boolean indicating that data must be returned as an ArrayBuffer
- * @param onError defines the callback to call when an error occurs
- * @returns a file request object
- */
- public static ReadFile(file: File, onSuccess: (data: any) => void, onProgress?: (ev: ProgressEvent) => any, useArrayBuffer?: boolean, onError?: (error: ReadFileError) => void): IFileRequest {
- let reader = new FileReader();
- let request: IFileRequest = {
- onCompleteObservable: new Observable<IFileRequest>(),
- abort: () => reader.abort(),
- };
- reader.onloadend = (e) => request.onCompleteObservable.notifyObservers(request);
- if (onError) {
- reader.onerror = (e) => {
- onError(new ReadFileError(`Unable to read ${file.name}`, file));
- };
- }
- reader.onload = (e) => {
- //target doesn't have result from ts 1.3
- onSuccess((<any>e.target)['result']);
- };
- if (onProgress) {
- reader.onprogress = onProgress;
- }
- if (!useArrayBuffer) {
- // Asynchronous read
- reader.readAsText(file);
- }
- else {
- reader.readAsArrayBuffer(file);
- }
- return request;
- }
- /**
- * Loads a file from a url
- * @param url url to load
- * @param onSuccess callback called when the file successfully loads
- * @param onProgress callback called while file is loading (if the server supports this mode)
- * @param offlineProvider defines the offline provider for caching
- * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
- * @param onError callback called when the file fails to load
- * @returns a file request object
- */
- public static LoadFile(url: string, onSuccess: (data: string | ArrayBuffer, responseURL?: string) => void, onProgress?: (ev: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (request?: WebRequest, exception?: LoadFileError) => void): IFileRequest {
- // If file and file input are set
- if (url.indexOf("file:") !== -1) {
- let fileName = decodeURIComponent(url.substring(5).toLowerCase());
- if (fileName.indexOf('./') === 0) {
- fileName = fileName.substring(2);
- }
- const file = FilesInputStore.FilesToLoad[fileName];
- if (file) {
- return FileTools.ReadFile(file, onSuccess, onProgress, useArrayBuffer, onError ? (error) => onError(undefined, new LoadFileError(error.message, error.file)) : undefined);
- }
- }
- return FileTools.RequestFile(url, (data, request) => {
- onSuccess(data, request ? request.responseURL : undefined);
- }, onProgress, offlineProvider, useArrayBuffer, onError ? (error) => {
- onError(error.request, new LoadFileError(error.message, error.request));
- } : undefined);
- }
- /**
- * Loads a file
- * @param url url to load
- * @param onSuccess callback called when the file successfully loads
- * @param onProgress callback called while file is loading (if the server supports this mode)
- * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
- * @param onError callback called when the file fails to load
- * @param onOpened callback called when the web request is opened
- * @returns a file request object
- */
- public static RequestFile(url: string, onSuccess: (data: string | ArrayBuffer, request?: WebRequest) => void, onProgress?: (event: ProgressEvent) => void, offlineProvider?: IOfflineProvider, useArrayBuffer?: boolean, onError?: (error: RequestFileError) => void, onOpened?: (request: WebRequest) => void): IFileRequest {
- url = FileTools._CleanUrl(url);
- url = FileTools.PreprocessUrl(url);
- const loadUrl = FileTools.BaseUrl + url;
- let aborted = false;
- const fileRequest: IFileRequest = {
- onCompleteObservable: new Observable<IFileRequest>(),
- abort: () => aborted = true,
- };
- const requestFile = () => {
- let request = new WebRequest();
- let retryHandle: Nullable<number> = null;
- fileRequest.abort = () => {
- aborted = true;
- if (request.readyState !== (XMLHttpRequest.DONE || 4)) {
- request.abort();
- }
- if (retryHandle !== null) {
- clearTimeout(retryHandle);
- retryHandle = null;
- }
- };
- const retryLoop = (retryIndex: number) => {
- request.open('GET', loadUrl);
- if (onOpened) {
- onOpened(request);
- }
- if (useArrayBuffer) {
- request.responseType = "arraybuffer";
- }
- if (onProgress) {
- request.addEventListener("progress", onProgress);
- }
- const onLoadEnd = () => {
- request.removeEventListener("loadend", onLoadEnd);
- fileRequest.onCompleteObservable.notifyObservers(fileRequest);
- fileRequest.onCompleteObservable.clear();
- };
- request.addEventListener("loadend", onLoadEnd);
- const onReadyStateChange = () => {
- if (aborted) {
- return;
- }
- // In case of undefined state in some browsers.
- if (request.readyState === (XMLHttpRequest.DONE || 4)) {
- // Some browsers have issues where onreadystatechange can be called multiple times with the same value.
- request.removeEventListener("readystatechange", onReadyStateChange);
- if ((request.status >= 200 && request.status < 300) || (request.status === 0 && (!DomManagement.IsWindowObjectExist() || FileTools.IsFileURL()))) {
- onSuccess(useArrayBuffer ? request.response : request.responseText, request);
- return;
- }
- let retryStrategy = FileTools.DefaultRetryStrategy;
- if (retryStrategy) {
- let waitTime = retryStrategy(loadUrl, request, retryIndex);
- if (waitTime !== -1) {
- // Prevent the request from completing for retry.
- request.removeEventListener("loadend", onLoadEnd);
- request = new WebRequest();
- retryHandle = setTimeout(() => retryLoop(retryIndex + 1), waitTime);
- return;
- }
- }
- const error = new RequestFileError("Error status: " + request.status + " " + request.statusText + " - Unable to load " + loadUrl, request);
- if (onError) {
- onError(error);
- }
- }
- };
- request.addEventListener("readystatechange", onReadyStateChange);
- request.send();
- };
- retryLoop(0);
- };
- // Caching all files
- if (offlineProvider && offlineProvider.enableSceneOffline) {
- const noOfflineSupport = (request?: any) => {
- if (request && request.status > 400) {
- if (onError) {
- onError(request);
- }
- } else {
- requestFile();
- }
- };
- const loadFromOfflineSupport = () => {
- // TODO: database needs to support aborting and should return a IFileRequest
- if (offlineProvider) {
- offlineProvider.loadFile(FileTools.BaseUrl + url, (data) => {
- if (!aborted) {
- onSuccess(data);
- }
- fileRequest.onCompleteObservable.notifyObservers(fileRequest);
- }, onProgress ? (event) => {
- if (!aborted) {
- onProgress(event);
- }
- } : undefined, noOfflineSupport, useArrayBuffer);
- }
- };
- offlineProvider.open(loadFromOfflineSupport, noOfflineSupport);
- }
- else {
- requestFile();
- }
- return fileRequest;
- }
- /**
- * Checks if the loaded document was accessed via `file:`-Protocol.
- * @returns boolean
- */
- public static IsFileURL(): boolean {
- return location.protocol === "file:";
- }
- }
- ThinEngine._FileToolsLoadImage = FileTools.LoadImage.bind(FileTools);
- ThinEngine._FileToolsLoadFile = FileTools.LoadFile.bind(FileTools);
- ShaderProcessor._FileToolsLoadFile = FileTools.LoadFile.bind(FileTools);
|