1 /* 2 * URLContent.js 3 * 4 * Sweet Home 3D, Copyright (c) 2024 Space Mushrooms <[email protected]> 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA02111-1307USA 19 */ 20 21 /** 22 * Content wrapper for strings used as URLs. 23 * @param {string} url the URL from which this content will be read 24 * @constructor 25 * @author Emmanuel Puybaret 26 */ 27 function URLContent(url) { 28 this.url = url; 29 } 30 31 URLContent["__class"] = "com.eteks.sweethome3d.tools.URLContent"; 32 URLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 33 34 URLContent.urlContents = {}; 35 36 /** 37 * Returns an instance of <code>URLContent</code> matching the given <code>url</code>. 38 * @param {string} url 39 * @return {URLContent} 40 */ 41 URLContent.fromURL = function(url) { 42 var urlContent = URLContent.urlContents [url]; 43 if (urlContent == null) { 44 if (url.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) === 0) { 45 urlContent = new LocalStorageURLContent(url); 46 } else if (url.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) === 0) { 47 urlContent = new IndexedDBURLContent(url); 48 } else { 49 urlContent = new URLContent(url); 50 } 51 // Keep content in cache 52 URLContent.urlContents [url] = urlContent; 53 } 54 return urlContent; 55 } 56 57 /** 58 * Returns the URL of this content. 59 * @return {string} 60 */ 61 URLContent.prototype.getURL = function() { 62 if (typeof document !== "undefined") { 63 var httpsSchemeIndex = this.url.indexOf("https://"); 64 var httpSchemeIndex = this.url.indexOf("http://"); 65 if (httpsSchemeIndex !== -1 66 || httpSchemeIndex !== -1) { 67 var scripts = document.getElementsByTagName("script"); 68 if (scripts && scripts.length > 0) { 69 var scriptUrl = document.getElementsByTagName("script") [0].src; 70 var scriptColonSlashIndex = scriptUrl.indexOf("://"); 71 var scriptScheme = scriptUrl.substring(0, scriptColonSlashIndex); 72 var scheme = httpsSchemeIndex !== -1 ? "https" : "http"; 73 // If scheme is different from script one, replace scheme and port with script ones to avoid CORS issues 74 if (scriptScheme != scheme) { 75 var scriptServer = scriptUrl.substring(scriptColonSlashIndex + "://".length, scriptUrl.indexOf("/", scriptColonSlashIndex + "://".length)); 76 var scriptPort = ""; 77 var colonIndex = scriptServer.indexOf(":"); 78 if (colonIndex > 0) { 79 scriptPort = scriptServer.substring(colonIndex); 80 scriptServer = scriptServer.substring(0, colonIndex); 81 } 82 var schemeIndex = httpsSchemeIndex !== -1 ? httpsSchemeIndex : httpSchemeIndex; 83 var colonSlashIndex = this.url.indexOf("://", schemeIndex); 84 var fileIndex = this.url.indexOf("/", colonSlashIndex + "://".length); 85 var server = this.url.substring(colonSlashIndex + "://".length, fileIndex); 86 if (server.indexOf(":") > 0) { 87 server = server.substring(0, server.indexOf(":")); 88 } 89 if (scriptServer == server) { 90 return this.url.substring(0, schemeIndex) + scriptScheme + "://" + scriptServer + scriptPort + this.url.substring(fileIndex); 91 } 92 } 93 } 94 } 95 } 96 97 return this.url; 98 } 99 100 /** 101 * Retrieves asynchronously an URL of this content usable for JavaScript functions with URL paramater. 102 * @param {{urlReady: function, urlError: function}} observer optional observer 103 which <code>urlReady</code> function will be called asynchronously once URL is available. 104 */ 105 URLContent.prototype.getStreamURL = function(observer) { 106 observer.urlReady(this.getURL()); 107 } 108 109 /** 110 * Returns <code>true</code> if this content URL is available to be read. 111 */ 112 URLContent.prototype.isStreamURLReady = function() { 113 return true; 114 } 115 116 /** 117 * Returns <code>true</code> if the URL stored by this content 118 * references an entry in a JAR. 119 * @return {boolean} 120 */ 121 URLContent.prototype.isJAREntry = function() { 122 return this.url.indexOf("jar:") === 0 && this.url.indexOf("!/") !== -1; 123 } 124 125 /** 126 * Returns the URL base of a JAR entry. 127 * @return {string} 128 */ 129 URLContent.prototype.getJAREntryURL = function() { 130 if (!this.isJAREntry()) { 131 throw new IllegalStateException("Content isn't a JAR entry"); 132 } 133 // Use URL returned by getURL() rather that this.url to get adjusted URL 134 var url = this.getURL(); 135 return url.substring("jar:".length, url.indexOf("!/")); 136 } 137 138 /** 139 * Returns the name of a JAR entry. 140 * If the JAR entry in the URL given at creation time was encoded in application/x-www-form-urlencoded format, 141 * this method will return it unchanged and not decoded. 142 * @return {string} 143 * @throws IllegalStateException if the URL of this content 144 * doesn't reference an entry in a JAR URL. 145 */ 146 URLContent.prototype.getJAREntryName = function() { 147 if (!this.isJAREntry()) { 148 throw new IllegalStateException("Content isn't a JAR entry"); 149 } 150 return this.url.substring(this.url.indexOf("!/") + 2); 151 } 152 153 /** 154 * Returns <code>true</code> if the object in parameter is an URL content 155 * that references the same URL as this object. 156 * @return {boolean} 157 */ 158 URLContent.prototype.equals = function(obj) { 159 if (obj === this) { 160 return true; 161 } else if (obj instanceof URLContent) { 162 return obj.url == this.url; 163 } else { 164 return false; 165 } 166 } 167 168 /** 169 * Returns a hash code for this object. 170 * @return {Number} 171 */ 172 URLContent.prototype.hashCode = function() { 173 return this.url.split("").reduce(function(a, b) { 174 a = ((a << 5) - a) + b.charCodeAt(0); 175 return a & a; 176 }, 0); 177 } 178 179 180 /** 181 * An URL content read from a home stream. 182 * @param {string} url the URL from which this content will be read 183 * @constructor 184 * @ignore 185 * @author Emmanuel Puybaret 186 */ 187 function HomeURLContent(url) { 188 URLContent.call(this, url); 189 } 190 HomeURLContent.prototype = Object.create(URLContent.prototype); 191 HomeURLContent.prototype.constructor = HomeURLContent; 192 193 HomeURLContent["__class"] = "com.eteks.sweethome3d.io.HomeURLContent"; 194 HomeURLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 195 196 197 /** 198 * Content read from a URL with no dependency on other content when this URL is a JAR entry. 199 * @constructor 200 * @ignore 201 * @author Emmanuel Puybaret 202 */ 203 function SimpleURLContent(url) { 204 URLContent.call(this, url); 205 } 206 SimpleURLContent.prototype = Object.create(URLContent.prototype); 207 SimpleURLContent.prototype.constructor = SimpleURLContent; 208 209 SimpleURLContent["__class"] = "com.eteks.sweethome3d.tools.SimpleURLContent"; 210 SimpleURLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 211 212 213 /** 214 * Content read from local data. 215 * Abstract base class for blobs, files, local storage and indexedDB content. 216 * @constructor 217 * @author Emmanuel Puybaret 218 */ 219 function LocalURLContent(url) { 220 URLContent.call(this, url); 221 this.savedContent = null; 222 } 223 LocalURLContent.prototype = Object.create(URLContent.prototype); 224 LocalURLContent.prototype.constructor = LocalURLContent; 225 226 /** 227 * Returns the content saved on server. 228 * @return {URLContent} content on server or <code>null</code> if not saved on server yet 229 */ 230 LocalURLContent.prototype.getSavedContent = function() { 231 return this.savedContent; 232 } 233 234 /** 235 * Sets the content saved on server. 236 * @param {URLContent} savedContent content on server 237 */ 238 LocalURLContent.prototype.setSavedContent = function(savedContent) { 239 this.savedContent = savedContent; 240 } 241 242 /** 243 * Retrieves asynchronously an URL of this content usable for JavaScript functions with URL paramater. 244 * @param {{urlReady: function, urlError: function}} observer optional observer 245 which <code>urlReady</code> function will be called asynchronously once URL is available. 246 */ 247 LocalURLContent.prototype.getStreamURL = function(observer) { 248 throw new UnsupportedOperationException("LocalURLContent abstract class"); 249 } 250 251 /** 252 * Returns the blob stored by this content, possibly asynchronously if <code>observer</code> parameter is given. 253 * @param {{blobReady: function, blobError: function}} [observer] optional observer 254 which blobReady function will be called asynchronously once blob is available. 255 * @return {Blob} blob content 256 */ 257 LocalURLContent.prototype.getBlob = function(observer) { 258 throw new UnsupportedOperationException("LocalURLContent abstract class"); 259 } 260 261 /** 262 * Writes the blob bound to this content with the request matching <code>writeBlobUrl</code>. 263 * @param {string} writeBlobUrl the URL used to save the blob 264 (containing possibly %s which will be replaced by <code>blobName</code>) 265 * @param {string|[string]} blobName the name or path used to save the blob, 266 or an array of values used to format <code>writeBlobUrl</code> including blob name 267 * @param {blobSaved: function(LocalURLContent, blobName) 268 blobError: function} observer called when content is saved or if writing fails 269 * @return {abort: function} an object containing <code>abort</code> method to abort the write operation 270 */ 271 LocalURLContent.prototype.writeBlob = function(writeBlobUrl, blobName, observer) { 272 var content = this; 273 var abortableOperations = []; 274 this.getBlob({ 275 blobReady: function(blob) { 276 var formatArguments; 277 if (Array.isArray(blobName)) { 278 var firstArg = blobName[0]; 279 formatArguments = new Array(blobName.length); 280 for (var i = 0; i < blobName.length; i++) { 281 formatArguments [i] = encodeURIComponent(blobName [i]); 282 } 283 blobName = firstArg; 284 } else { 285 formatArguments = encodeURIComponent(blobName); 286 } 287 var url = CoreTools.format(writeBlobUrl.replace(/(%[^s^\d])/g, "%$1"), formatArguments); 288 if (url.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) === 0) { 289 var path = url.substring(url.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) + LocalStorageURLContent.LOCAL_STORAGE_PREFIX.length); 290 var storageKey = decodeURIComponent(path.indexOf('?') > 0 ? path.substring(0, path.indexOf('?')) : path); 291 return LocalURLContent.convertBlobToBase64(blob, function(data) { 292 try { 293 localStorage.setItem(storageKey, data); 294 observer.blobSaved(content, blobName); 295 } catch (ex) { 296 if (observer.blobError !== undefined) { 297 observer.blobError(ex, ex.message); 298 } 299 } 300 }); 301 } else if (url.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) === 0) { 302 // Parse URL of the form indexeddb://database/objectstore?keyPathField=name&contentField=content&dateField=date&name=key 303 var databaseNameIndex = url.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) + IndexedDBURLContent.INDEXED_DB_PREFIX.length; 304 var slashIndex = url.indexOf('/', databaseNameIndex); 305 var questionMarkIndex = url.indexOf('?', slashIndex + 1); 306 var databaseName = url.substring(databaseNameIndex, slashIndex); 307 var objectStore = url.substring(slashIndex + 1, questionMarkIndex); 308 var fields = url.substring(questionMarkIndex + 1).split('&'); 309 var key = null; 310 var keyPathField = null; 311 var contentField = null; 312 var dateField = null; 313 for (var i = 0; i < fields.length; i++) { 314 var equalIndex = fields [i].indexOf('='); 315 var parameter = fields [i].substring(0, equalIndex); 316 var value = fields [i].substring(equalIndex + 1); 317 switch (parameter) { 318 case "keyPathField": 319 keyPathField = value; 320 break; 321 case "contentField": 322 contentField = value; 323 break; 324 case "dateField": 325 dateField = value; 326 break; 327 } 328 } 329 // Parse a second time fields to retrieve parameters value (key and other other ones) 330 var otherFields = {}; 331 for (var i = 0; i < fields.length; i++) { 332 var equalIndex = fields [i].indexOf('='); 333 var parameter = fields [i].substring(0, equalIndex); 334 var value = fields [i].substring(equalIndex + 1); 335 if (keyPathField === parameter) { 336 key = decodeURIComponent(value); 337 } else if (parameter.indexOf("Field", parameter.length - "Field".length) === -1) { 338 otherFields [parameter] = decodeURIComponent(value); 339 } 340 } 341 342 var databaseUpgradeNeeded = function(ev) { 343 var database = ev.target.result; 344 if (!database.objectStoreNames.contains(objectStore)) { 345 database.createObjectStore(objectStore, {keyPath: keyPathField}); 346 } 347 }; 348 var databaseError = function(ev) { 349 if (observer.blobError !== undefined) { 350 observer.blobError(ev.target.errorCode, "Can't connect to database " + databaseName); 351 } 352 }; 353 var databaseSuccess = function(ev) { 354 var database = ev.target.result; 355 try { 356 if (!database.objectStoreNames.contains(objectStore)) { 357 // Reopen the database to create missing object store 358 database.close(); 359 var requestUpgrade = indexedDB.open(databaseName, database.version + 1); 360 requestUpgrade.addEventListener("upgradeneeded", databaseUpgradeNeeded); 361 requestUpgrade.addEventListener("error", databaseError); 362 requestUpgrade.addEventListener("success", databaseSuccess); 363 } else { 364 var transaction = database.transaction(objectStore, 'readwrite'); 365 var store = transaction.objectStore(objectStore); 366 var storedResource = {}; 367 storedResource [keyPathField] = key; 368 storedResource [contentField] = blob; 369 if (dateField != null) { 370 storedResource [dateField] = Date.now(); 371 } 372 for (var i in otherFields) { 373 storedResource [i] = otherFields [i]; 374 } 375 var query = store.put(storedResource); 376 query.addEventListener("error", function(ev) { 377 if (observer.blobError !== undefined) { 378 observer.blobError(ev.target.errorCode, "Can't store item in " + objectStore); 379 } 380 }); 381 query.addEventListener("success", function(ev) { 382 observer.blobSaved(content, blobName); 383 }); 384 transaction.addEventListener("complete", function(ev) { 385 database.close(); 386 }); 387 abortableOperations.push(transaction); 388 } 389 } catch (ex) { 390 if (observer.blobError !== undefined) { 391 observer.blobError(ex, ex.message); 392 } 393 } 394 }; 395 396 if (indexedDB != null) { 397 var request = indexedDB.open(databaseName); 398 request.addEventListener("upgradeneeded", databaseUpgradeNeeded); 399 request.addEventListener("error", databaseError); 400 request.addEventListener("success", databaseSuccess); 401 } else { 402 observer.blobError(new Error("indexedDB"), "indexedDB unavailable"); 403 } 404 } else { 405 var request = new XMLHttpRequest(); 406 request.open("POST", url, true); 407 request.addEventListener('load', function (ev) { 408 if (request.readyState === XMLHttpRequest.DONE) { 409 if (request.status === 200) { 410 observer.blobSaved(content, blobName); 411 } else if (observer.blobError !== undefined) { 412 observer.blobError(request.status, request.responseText); 413 } 414 } 415 }); 416 var errorListener = function(ev) { 417 if (observer.blobError !== undefined) { 418 observer.blobError(0, "Can't post " + url); 419 } 420 }; 421 request.addEventListener("error", errorListener); 422 request.addEventListener("timeout", errorListener); 423 request.send(blob); 424 abortableOperations.push(request); 425 } 426 }, 427 blobError: function(status, error) { 428 if (observer.blobError !== undefined) { 429 observer.blobError(status, error); 430 } 431 } 432 }); 433 434 return { 435 abort: function() { 436 for (var i = 0; i < abortableOperations.length; i++) { 437 abortableOperations [i].abort(); 438 } 439 } 440 }; 441 } 442 443 /** 444 * @param {Blob} blob 445 * @param {function} observer 446 * @return {abort: function} an object containing <code>abort</code> method to abort the conversion 447 * @private 448 */ 449 LocalURLContent.convertBlobToBase64 = function(blob, observer) { 450 var reader = new FileReader(); 451 // Use onload rather that addEventListener for Cordova support 452 reader.onload = function() { 453 observer(reader.result); 454 }; 455 reader.readAsDataURL(blob); 456 return reader; 457 } 458 459 /** 460 * Content read from the URL of a <code>Blob</code> instance. 461 * Note that this class may also handle a <code>File</code> instance which is a sub type of <code>Blob</code>. 462 * @constructor 463 * @param {Blob} blob 464 * @author Louis Grignon 465 * @author Emmanuel Puybaret 466 */ 467 function BlobURLContent(blob) { 468 LocalURLContent.call(this, URL.createObjectURL(blob)); 469 this.blob = blob; 470 } 471 BlobURLContent.prototype = Object.create(LocalURLContent.prototype); 472 BlobURLContent.prototype.constructor = BlobURLContent; 473 474 BlobURLContent["__class"] = "com.eteks.sweethome3d.tools.BlobURLContent"; 475 BlobURLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 476 477 BlobURLContent.BLOB_PREFIX = "blob:"; 478 479 /** 480 * Returns an instance of <code>BlobURLContent</code> for the given <code>blob</code>. 481 * @param {Blob} blob 482 * @return {BlobURLContent} 483 */ 484 BlobURLContent.fromBlob = function(blob) { 485 // Check blob content is in cache 486 for (var i in URLContent.urlContents) { 487 if (URLContent.urlContents [i] instanceof BlobURLContent 488 && URLContent.urlContents [i].blob === blob) { 489 return URLContent.urlContents [i]; 490 } 491 } 492 var content = new BlobURLContent(blob); 493 URLContent.urlContents [content.getURL()] = content; 494 return content; 495 } 496 497 /** 498 * Generates a BlobURLContent instance from an image. 499 * @param {HTMLImageElement} image the image to be used as content source 500 * @param {string} imageType resulting image blob mime type 501 * @param {function(BlobURLContent)} observer callback called when content is ready, with content instance as only parameter 502 */ 503 BlobURLContent.fromImage = function(image, imageType, observer) { 504 var canvas = document.createElement("canvas"); 505 var context = canvas.getContext("2d"); 506 canvas.width = image.width; 507 canvas.height = image.height; 508 context.drawImage(image, 0, 0, image.width, image.height); 509 if (canvas.msToBlob) { 510 observer(BlobURLContent.fromBlob(canvas.msToBlob())); 511 } else { 512 canvas.toBlob(function (blob) { 513 observer(BlobURLContent.fromBlob(blob)); 514 }, imageType, 0.7); 515 } 516 } 517 518 /** 519 * Retrieves asynchronously an URL of this content usable for JavaScript functions with URL paramater. 520 * @param {{urlReady: function, urlError: function}} observer optional observer 521 which <code>urlReady</code> function will be called asynchronously once URL is available. 522 */ 523 BlobURLContent.prototype.getStreamURL = function(observer) { 524 observer.urlReady(this.getURL()); 525 } 526 527 /** 528 * Returns the blob stored by this content, possibly asynchronously if <code>observer</code> parameter is given. 529 * @param {{blobReady: function, blobError: function}} [observer] optional observer 530 which blobReady function will be called asynchronously once blob is available. 531 * @return {Blob} blob content 532 */ 533 BlobURLContent.prototype.getBlob = function(observer) { 534 if (observer !== undefined) { 535 observer.blobReady(this.blob); 536 } 537 return this.blob; 538 } 539 540 541 /** 542 * Content read from local storage stored in a blob encoded in Base 64. 543 * @constructor 544 * @param {string} url an URL of the form <code>localstorage://key</code> 545 where <code>key</code> is the key of the blob to read from local storage 546 * @ignore 547 * @author Emmanuel Puybaret 548 */ 549 function LocalStorageURLContent(url) { 550 LocalURLContent.call(this, url); 551 this.blob = null; 552 this.blobUrl = null; 553 } 554 LocalStorageURLContent.prototype = Object.create(LocalURLContent.prototype); 555 LocalStorageURLContent.prototype.constructor = LocalStorageURLContent; 556 557 LocalStorageURLContent["__class"] = "com.eteks.sweethome3d.tools.LocalStorageURLContent"; 558 LocalStorageURLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 559 560 LocalStorageURLContent.LOCAL_STORAGE_PREFIX = "localstorage://"; 561 562 /** 563 * Retrieves asynchronously an URL of this content usable for JavaScript functions with URL paramater. 564 * @param {{urlReady: function, urlError: function}} observer optional observer 565 which <code>urlReady</code> function will be called asynchronously once URL is available. 566 */ 567 LocalStorageURLContent.prototype.getStreamURL = function(observer) { 568 if (this.blobUrl == null) { 569 var urlContent = this; 570 this.getBlob({ 571 blobReady: function(blob) { 572 observer.urlReady(urlContent.blobUrl); 573 }, 574 blobError: function(status, error) { 575 if (observer.urlError !== undefined) { 576 observer.urlError(status, error); 577 } 578 } 579 }); 580 } else { 581 observer.urlReady(this.blobUrl); 582 } 583 } 584 585 /** 586 * Returns the blob stored by this content. 587 * @param {{blobReady: function, blobError: function}} [observer] optional observer 588 which blobReady function will be called asynchronously once blob is available. 589 * @return {Blob} blob content 590 */ 591 LocalStorageURLContent.prototype.getBlob = function(observer) { 592 if (this.blob == null) { 593 var url = this.getURL(); 594 if (url.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) === 0) { 595 var path = url.substring(url.indexOf(LocalStorageURLContent.LOCAL_STORAGE_PREFIX) + LocalStorageURLContent.LOCAL_STORAGE_PREFIX.length); 596 var key = decodeURIComponent(path.indexOf('?') > 0 ? path.substring(0, path.indexOf('?')) : path); 597 var data = localStorage.getItem(key); 598 if (data != null) { 599 var contentType = data.substring("data:".length, data.indexOf(';')); 600 var chars = atob(data.substring(data.indexOf(',') + 1)); 601 var numbers = new Array(chars.length); 602 for (var i = 0; i < numbers.length; i++) { 603 numbers[i] = chars.charCodeAt(i); 604 } 605 var byteArray = new Uint8Array(numbers); 606 this.blob = new Blob([byteArray], {type: contentType}); 607 this.blobUrl = URL.createObjectURL(this.blob); 608 } else { 609 if (observer.urlError !== undefined) { 610 observer.urlError(1, "No key '" + key + "' in localStorage"); 611 } 612 } 613 } else { 614 if (observer.urlError !== undefined) { 615 observer.urlError(1, url + " not a local storage url"); 616 } 617 } 618 } 619 if (observer !== undefined 620 && observer.blobReady !== undefined 621 && this.blob != null) { 622 observer.blobReady(this.blob); 623 } 624 return this.blob; 625 } 626 627 628 /** 629 * Content read from IndexedDB stored in a blob. 630 * @constructor 631 * @param {string} url an URL of the form <code>indexeddb://database/objectstore/field?keyPathField=key</code> 632 where <code>database</code> is the database name, <code>objectstore</code> the object store where 633 the blob is stored in the given <code>field</code> and <code>key</code> the key value 634 of <code>keyPathField</code> used to select the blob. If the database doesn't exist, it will be 635 created with a keyPath equal to <code>keyPathField</code>. 636 * @ignore 637 * @author Emmanuel Puybaret 638 */ 639 function IndexedDBURLContent(url) { 640 LocalURLContent.call(this, url); 641 this.blob = null; 642 this.blobUrl = null; 643 } 644 IndexedDBURLContent.prototype = Object.create(LocalURLContent.prototype); 645 IndexedDBURLContent.prototype.constructor = IndexedDBURLContent; 646 647 IndexedDBURLContent["__class"] = "com.eteks.sweethome3d.tools.IndexedDBURLContent"; 648 IndexedDBURLContent["__interfaces"] = ["com.eteks.sweethome3d.model.Content"]; 649 650 IndexedDBURLContent.INDEXED_DB_PREFIX = "indexeddb://"; 651 652 /** 653 * Retrieves asynchronously an URL of this content usable for JavaScript functions with URL paramater. 654 * @param {{urlReady: function, urlError: function}} observer optional observer 655 which <code>urlReady</code> function will be called asynchronously once URL is available. 656 */ 657 IndexedDBURLContent.prototype.getStreamURL = function(observer) { 658 if (this.blobUrl == null) { 659 var urlContent = this; 660 this.getBlob({ 661 blobReady: function(blob) { 662 observer.urlReady(urlContent.blobUrl); 663 }, 664 blobError: function(status, error) { 665 if (observer.urlError !== undefined) { 666 observer.urlError(status, error); 667 } 668 } 669 }); 670 } else { 671 observer.urlReady(this.blobUrl); 672 } 673 } 674 675 /** 676 * Returns the blob stored by this content, reading it asynchronously. 677 * @param {{blobReady: function, blobError: function}} [observer] optional observer 678 which blobReady function will be called asynchronously if blob is not available yet. 679 * @return {Blob} blob content or <code>null</code> if blob wasn't read yet 680 */ 681 IndexedDBURLContent.prototype.getBlob = function(observer) { 682 if (observer !== undefined) { 683 if (this.blob != null) { 684 observer.blobReady(this.blob); 685 } else { 686 var url = this.getURL(); 687 if (url.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) >= 0) { 688 // Parse URL of the form indexeddb://database/objectstore/field?keyPathField=key 689 var databaseNameIndex = url.indexOf(IndexedDBURLContent.INDEXED_DB_PREFIX) + IndexedDBURLContent.INDEXED_DB_PREFIX.length; 690 var firstPathSlashIndex = url.indexOf('/', databaseNameIndex); 691 var secondPathSlashIndex = url.indexOf('/', firstPathSlashIndex + 1); 692 var questionMarkIndex = url.indexOf('?', secondPathSlashIndex + 1); 693 var equalIndex = url.indexOf('=', questionMarkIndex + 1); 694 var ampersandIndex = url.indexOf('&', equalIndex + 1); 695 var databaseName = url.substring(databaseNameIndex, firstPathSlashIndex); 696 var objectStore = url.substring(firstPathSlashIndex + 1, secondPathSlashIndex); 697 var contentField = url.substring(secondPathSlashIndex + 1, questionMarkIndex); 698 var keyPathField = url.substring(questionMarkIndex + 1, equalIndex); 699 var key = decodeURIComponent(url.substring(equalIndex + 1, ampersandIndex > 0 ? ampersandIndex : url.length)); 700 var urlContent = this; 701 702 var databaseUpgradeNeeded = function(ev) { 703 var database = ev.target.result; 704 if (!database.objectStoreNames.contains(objectStore)) { 705 database.createObjectStore(objectStore, {keyPath: keyPathField}); 706 } 707 }; 708 var databaseError = function(ev) { 709 if (observer.blobError !== undefined) { 710 observer.blobError(ev.target.errorCode, "Can't connect to database " + databaseName); 711 } 712 }; 713 var databaseSuccess = function(ev) { 714 var database = ev.target.result; 715 try { 716 if (!database.objectStoreNames.contains(objectStore)) { 717 // Reopen the database to create missing object store 718 database.close(); 719 var requestUpgrade = indexedDB.open(databaseName, database.version + 1); 720 requestUpgrade.addEventListener("upgradeneeded", databaseUpgradeNeeded); 721 requestUpgrade.addEventListener("error", databaseError); 722 requestUpgrade.addEventListener("success", databaseSuccess); 723 } else { 724 var transaction = database.transaction(objectStore, 'readonly'); 725 var store = transaction.objectStore(objectStore); 726 var query = store.get(key); 727 query.addEventListener("error", function(ev) { 728 if (observer.blobError !== undefined) { 729 observer.blobError(ev.target.errorCode, "Can't query in " + objectStore); 730 } 731 }); 732 query.addEventListener("success", function(ev) { 733 if (ev.target.result !== undefined) { 734 urlContent.blob = ev.target.result [contentField]; 735 // Store other properties in blob properties 736 for (var i in ev.target.result) { 737 var propertyName = ev.target.result [i]; 738 if (propertyName !== keyPathField 739 && propertyName != contentField 740 && urlContent.blob [propertyName] === undefined) { 741 urlContent.blob [propertyName] = ev.target.result [propertyName]; 742 } 743 } 744 urlContent.blobUrl = URL.createObjectURL(urlContent.blob); 745 if (observer.blobReady !== undefined) { 746 observer.blobReady(urlContent.blob); 747 } 748 } else if (observer.blobError !== undefined) { 749 observer.blobError(-1, "Blob with key " + key + " not found"); 750 } 751 }); 752 transaction.addEventListener("complete", function(ev) { 753 database.close(); 754 }); 755 } 756 } catch (ex) { 757 if (observer.blobError !== undefined) { 758 observer.blobError(ex, ex.message); 759 } 760 } 761 }; 762 763 if (indexedDB != null) { 764 var request = indexedDB.open(databaseName); 765 request.addEventListener("upgradeneeded", databaseUpgradeNeeded); 766 request.addEventListener("error", databaseError); 767 request.addEventListener("success", databaseSuccess); 768 } else { 769 observer.blobError(new Error("indexedDB"), "indexedDB unavailable"); 770 } 771 } else if (observer.urlError !== undefined) { 772 observer.urlError(1, url + " not an indexedDB url"); 773 } 774 } 775 } 776 return this.blob; 777 } 778 779 /** 780 * Returns <code>true</code> if this content URL is available. 781 */ 782 IndexedDBURLContent.prototype.isStreamURLReady = function() { 783 return this.blobUrl != null; 784 } 785 786 787 /** 788 * Utilities about the system environment. 789 * @class 790 * @ignore 791 * @author Emmanuel Puybaret 792 */ 793 var OperatingSystem = {} 794 795 /** 796 * Returns <code>true</code> if the operating system is Linux. 797 */ 798 OperatingSystem.isLinux = function() { 799 if (navigator && navigator.platform) { 800 return navigator.platform.indexOf("Linux") !== -1; 801 } else { 802 return false; 803 } 804 } 805 806 /** 807 * Returns <code>true</code> if the operating system is Windows. 808 */ 809 OperatingSystem.isWindows = function() { 810 if (navigator && navigator.platform) { 811 return navigator.platform.indexOf("Windows") !== -1 || navigator.platform.indexOf("Win") !== -1; 812 } else { 813 return false; 814 } 815 } 816 817 /** 818 * Returns <code>true</code> if the operating system is Mac OS X. 819 */ 820 OperatingSystem.isMacOSX = function() { 821 if (navigator && navigator.platform) { 822 return navigator.platform.indexOf("Mac") !== -1; 823 } else { 824 return false; 825 } 826 } 827 828 /** 829 * Returns the operating system name used to filter some information. 830 */ 831 OperatingSystem.getName = function() { 832 if (OperatingSystem.isMacOSX()) { 833 return "Mac OS X"; 834 } else if (OperatingSystem.isLinux()) { 835 return "Linux"; 836 } else if (OperatingSystem.isWindows()) { 837 return "Windows"; 838 } else { 839 return "Other"; 840 } 841 } 842 843 /** 844 * Returns <code>true</code> if the current browser is Internet Explorer or Edge (note based on Chromium). 845 */ 846 OperatingSystem.isInternetExplorerOrLegacyEdge = function() { 847 // IE and Edge test from https://stackoverflow.com/questions/31757852/how-can-i-detect-internet-explorer-ie-and-microsoft-edge-using-javascript 848 return (document.documentMode || /Edge/.test(navigator.userAgent)); 849 } 850 851 /** 852 * Returns <code>true</code> if the current browser is Internet Explorer. 853 */ 854 OperatingSystem.isInternetExplorer = function() { 855 // IE test from https://stackoverflow.com/questions/31757852/how-can-i-detect-internet-explorer-ie-and-microsoft-edge-using-javascript 856 return document.documentMode; 857 } 858 859 860 /** 861 * ZIP reading utilities. 862 * @class 863 * @author Emmanuel Puybaret 864 */ 865 var ZIPTools = {}; 866 867 ZIPTools.READING = "Reading"; 868 869 ZIPTools.openedZips = {}; 870 ZIPTools.runningRequests = []; 871 872 /** 873 * Reads the ZIP data in the given URL. 874 * @param {string} url the URL of a zip file containing an OBJ entry that will be loaded 875 * or an URL noted as jar:url!/objEntry where objEntry will be loaded. 876 * @param {boolean} [synchronous] optional parameter equal to false by default 877 * @param {{zipReady, zipError, progression}} zipObserver An observer containing zipReady(zip), 878 * zipError(error), progression(part, info, percentage) methods that 879 * will called at various phases. 880 */ 881 ZIPTools.getZIP = function(url, synchronous, zipObserver) { 882 if (zipObserver === undefined) { 883 zipObserver = synchronous; 884 synchronous = false; 885 } 886 if (url in ZIPTools.openedZips) { 887 zipObserver.zipReady(ZIPTools.openedZips [url]); 888 } else { 889 var urlContent = URLContent.fromURL(url); 890 if (synchronous 891 && !urlContent.isStreamURLReady()) { 892 throw new IllegalStateException("Can't run synchronously with unavailable URL"); 893 } 894 urlContent.getStreamURL({ 895 urlReady: function(streamUrl) { 896 try { 897 var request = new XMLHttpRequest(); 898 request.open('GET', streamUrl, !synchronous); 899 request.responseType = "arraybuffer"; 900 request.withCredentials = true; 901 request.overrideMimeType("application/octet-stream"); 902 request.addEventListener("readystatechange", 903 function(ev) { 904 if (request.readyState === XMLHttpRequest.DONE) { 905 if ((request.status === 200 || request.status === 0) 906 && request.response != null) { 907 try { 908 ZIPTools.runningRequests.splice(ZIPTools.runningRequests.indexOf(request), 1); 909 var zip = new JSZip(request.response); 910 ZIPTools.openedZips [url] = zip; 911 zipObserver.zipReady(ZIPTools.openedZips [url]); 912 } catch (ex) { 913 zipObserver.zipError(ex); 914 } 915 } else { 916 // Report error for requests that weren't aborted 917 var index = ZIPTools.runningRequests.indexOf(request); 918 if (index >= 0) { 919 ZIPTools.runningRequests.splice(index, 1); 920 zipObserver.zipError(new Error(request.status + " while requesting " + url)); 921 } 922 } 923 } 924 }); 925 request.addEventListener("progress", 926 function(ev) { 927 if (ev.lengthComputable 928 && zipObserver.progression !== undefined) { 929 zipObserver.progression(ZIPTools.READING, url, ev.loaded / ev.total); 930 } 931 }); 932 request.send(); 933 ZIPTools.runningRequests.push(request); 934 } catch (ex) { 935 zipObserver.zipError(ex); 936 } 937 }, 938 urlError: function(status, error) { 939 if (zipObserver.zipError !== undefined) { 940 zipObserver.zipError(error); 941 } 942 } 943 }); 944 } 945 } 946 947 948 /** 949 * Clears cache and aborts running requests. 950 */ 951 ZIPTools.clear = function() { 952 ZIPTools.openedZips = {}; 953 // Abort running requests 954 while (ZIPTools.runningRequests.length > 0) { 955 var request = ZIPTools.runningRequests [ZIPTools.runningRequests.length - 1]; 956 ZIPTools.runningRequests.splice(ZIPTools.runningRequests.length - 1, 1); 957 request.abort(); 958 } 959 } 960 961 /** 962 * Removes from cache the content matching the given <code>url</code>. 963 */ 964 ZIPTools.disposeZIP = function(url) { 965 delete ZIPTools.openedZips [url]; 966 } 967 968 /** 969 * Returns true if the given image data describes a GIF file. 970 * @param {string|Uint8Array} imageData 971 * @package 972 * @ignore 973 */ 974 ZIPTools.isGIFImage = function(imageData) { 975 if (imageData.length <= 6) { 976 return false; 977 } else if (typeof imageData === "string") { 978 return imageData.charCodeAt(0) === 0x47 979 && imageData.charCodeAt(1) === 0x49 980 && imageData.charCodeAt(2) === 0x46 981 && imageData.charCodeAt(3) === 0x38 982 && (imageData.charCodeAt(4) === 0x37 || imageData.charCodeAt(4) === 0x39) 983 && imageData.charCodeAt(5) === 0x61; 984 } else { 985 return imageData [0] === 0x47 986 && imageData [1] === 0x49 987 && imageData [2] === 0x46 988 && imageData [3] === 0x38 989 && (imageData [4] === 0x37 || imageData [4] === 0x39) 990 && imageData [5] === 0x61; 991 } 992 } 993 994 /** 995 * Returns true if the given image data describes a BMP file. 996 * @param {string|Uint8Array} imageData 997 * @package 998 * @ignore 999 */ 1000 ZIPTools.isBMPImage = function(imageData) { 1001 if (imageData.length <= 2) { 1002 return false; 1003 } else if (typeof imageData === "string") { 1004 return imageData.charCodeAt(0) === 0x42 1005 && imageData.charCodeAt(1) === 0x4D; 1006 } else { 1007 return imageData [0] === 0x42 1008 && imageData [1] === 0x4D; 1009 } 1010 } 1011 1012 /** 1013 * Returns true if the given image data describes a JPEG file. 1014 * @param {string|Uint8Array} imageData 1015 * @package 1016 * @ignore 1017 */ 1018 ZIPTools.isJPEGImage = function(imageData) { 1019 if (imageData.length <= 3) { 1020 return false; 1021 } else if (typeof imageData === "string") { 1022 return imageData.charCodeAt(0) === 0xFF 1023 && (imageData.charCodeAt(1) === 0xD8 || imageData.charCodeAt(1) === 0x4F) 1024 && imageData.charCodeAt(2) === 0xFF; 1025 } else { 1026 return imageData [0] === 0xFF 1027 && (imageData [1] === 0xD8 || imageData [1] === 0x4F) 1028 && imageData [2] === 0xFF; 1029 } 1030 } 1031 1032 /** 1033 * Returns true if the given image data describes a PNG file. 1034 * @param {string|Uint8Array} imageData 1035 * @package 1036 * @ignore 1037 */ 1038 ZIPTools.isPNGImage = function(imageData) { 1039 if (imageData.length <= 8) { 1040 return false; 1041 } else if (typeof imageData === "string") { 1042 return imageData.charCodeAt(0) === 0x89 1043 && imageData.charCodeAt(1) === 0x50 1044 && imageData.charCodeAt(2) === 0x4E 1045 && imageData.charCodeAt(3) === 0x47 1046 && imageData.charCodeAt(4) === 0x0D 1047 && imageData.charCodeAt(5) === 0x0A 1048 && imageData.charCodeAt(6) === 0x1A 1049 && imageData.charCodeAt(7) === 0x0A; 1050 } else { 1051 return imageData [0] === 0x89 1052 && imageData [1] === 0x50 1053 && imageData [2] === 0x4E 1054 && imageData [3] === 0x47 1055 && imageData [4] === 0x0D 1056 && imageData [5] === 0x0A 1057 && imageData [6] === 0x1A 1058 && imageData [7] === 0x0A; 1059 } 1060 } 1061 1062 /** 1063 * Returns true if the given image data describes a transparent PNG file. 1064 * @param {string|Uint8Array} imageData 1065 * @package 1066 * @ignore 1067 */ 1068 ZIPTools.isTransparentImage = function(imageData) { 1069 if (imageData.length > 26) { 1070 if (typeof imageData === "string") { 1071 return (imageData.charCodeAt(25) === 4 1072 || imageData.charCodeAt(25) === 6 1073 || (imageData.indexOf("PLTE") !== -1 && imageData.indexOf("tRNS") !== -1)); 1074 } else { 1075 if (imageData [25] === 4 1076 || imageData [25] === 6) { 1077 return true; 1078 } else { 1079 // Search if imageData contains PLTE and tRNS 1080 for (var i = 0; i < imageData.length; i++) { 1081 if (imageData [i] === 0x50 1082 && imageData [i + 1] === 0x4C 1083 && imageData [i + 2] === 0x54 1084 && imageData [i + 3] === 0x45) { 1085 for (var j = 0; j < imageData.length; j++) { 1086 if (imageData [j] === 0x74 1087 && imageData [j + 1] === 0x52 1088 && imageData [j + 2] === 0x4E 1089 && imageData [j + 3] === 0x53) { 1090 return true; 1091 } 1092 } 1093 } 1094 } 1095 } 1096 } 1097 } 1098 return false; 1099 } 1100 1101 /** 1102 * Returns the folder where a given Javascript .js file was read from. 1103 * @param {string|RegExp} [script] the URL of a script used in the program 1104 * @package 1105 * @ignore 1106 */ 1107 ZIPTools.getScriptFolder = function(script) { 1108 if (script === undefined) { 1109 // Consider this script is always here because ZIPTools itself requires it 1110 script = "jszip.min.js"; 1111 } 1112 // Search the base URL of this script 1113 if (typeof document !== "undefined") { 1114 var scripts = document.getElementsByTagName("script"); 1115 for (var i = 0; i < scripts.length; i++) { 1116 if (script instanceof RegExp && scripts[i].src.match(script) 1117 || typeof script === "string" && scripts[i].src.indexOf(script) !== -1) { 1118 return scripts[i].src.substring(0, scripts[i].src.lastIndexOf("/") + 1); 1119 } 1120 } 1121 1122 if (scripts.length > 0) { 1123 return scripts[0].src.substring(0, scripts[0].src.lastIndexOf("/") + 1); 1124 } 1125 } 1126 return "https://www.sweethome3d.com/libjs/"; 1127 } 1128