You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1384 lines
48 KiB
1384 lines
48 KiB
|
3 years ago
|
|
||
|
|
var CLOUD = CLOUD || {};
|
||
|
|
|
||
|
|
|
||
|
|
(function () { 'use strict';
|
||
|
|
|
||
|
|
// This is free and unencumbered software released into the public domain.
|
||
|
|
// See LICENSE.md for more information.
|
||
|
|
|
||
|
|
//
|
||
|
|
// Utilities
|
||
|
|
//
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {number} a The number to test.
|
||
|
|
* @param {number} min The minimum value in the range, inclusive.
|
||
|
|
* @param {number} max The maximum value in the range, inclusive.
|
||
|
|
* @return {boolean} True if a >= min and a <= max.
|
||
|
|
*/
|
||
|
|
function inRange(a, min, max) {
|
||
|
|
return min <= a && a <= max;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {*} o
|
||
|
|
* @return {Object}
|
||
|
|
*/
|
||
|
|
function ToDictionary(o) {
|
||
|
|
if (o === undefined) return {};
|
||
|
|
if (o === Object(o)) return o;
|
||
|
|
throw TypeError('Could not convert argument to dictionary');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {string} string Input string of UTF-16 code units.
|
||
|
|
* @return {!Array.<number>} Code points.
|
||
|
|
*/
|
||
|
|
function stringToCodePoints(string) {
|
||
|
|
// https://heycam.github.io/webidl/#dfn-obtain-unicode
|
||
|
|
|
||
|
|
// 1. Let S be the DOMString value.
|
||
|
|
var s = String(string);
|
||
|
|
|
||
|
|
// 2. Let n be the length of S.
|
||
|
|
var n = s.length;
|
||
|
|
|
||
|
|
// 3. Initialize i to 0.
|
||
|
|
var i = 0;
|
||
|
|
|
||
|
|
// 4. Initialize U to be an empty sequence of Unicode characters.
|
||
|
|
var u = [];
|
||
|
|
|
||
|
|
// 5. While i < n:
|
||
|
|
while (i < n) {
|
||
|
|
|
||
|
|
// 1. Let c be the code unit in S at index i.
|
||
|
|
var c = s.charCodeAt(i);
|
||
|
|
|
||
|
|
// 2. Depending on the value of c:
|
||
|
|
|
||
|
|
// c < 0xD800 or c > 0xDFFF
|
||
|
|
if (c < 0xD800 || c > 0xDFFF) {
|
||
|
|
// Append to U the Unicode character with code point c.
|
||
|
|
u.push(c);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0xDC00 ≤ c ≤ 0xDFFF
|
||
|
|
else if (0xDC00 <= c && c <= 0xDFFF) {
|
||
|
|
// Append to U a U+FFFD REPLACEMENT CHARACTER.
|
||
|
|
u.push(0xFFFD);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0xD800 ≤ c ≤ 0xDBFF
|
||
|
|
else if (0xD800 <= c && c <= 0xDBFF) {
|
||
|
|
// 1. If i = n−1, then append to U a U+FFFD REPLACEMENT
|
||
|
|
// CHARACTER.
|
||
|
|
if (i === n - 1) {
|
||
|
|
u.push(0xFFFD);
|
||
|
|
}
|
||
|
|
// 2. Otherwise, i < n−1:
|
||
|
|
else {
|
||
|
|
// 1. Let d be the code unit in S at index i+1.
|
||
|
|
var d = string.charCodeAt(i + 1);
|
||
|
|
|
||
|
|
// 2. If 0xDC00 ≤ d ≤ 0xDFFF, then:
|
||
|
|
if (0xDC00 <= d && d <= 0xDFFF) {
|
||
|
|
// 1. Let a be c & 0x3FF.
|
||
|
|
var a = c & 0x3FF;
|
||
|
|
|
||
|
|
// 2. Let b be d & 0x3FF.
|
||
|
|
var b = d & 0x3FF;
|
||
|
|
|
||
|
|
// 3. Append to U the Unicode character with code point
|
||
|
|
// 2^16+2^10*a+b.
|
||
|
|
u.push(0x10000 + (a << 10) + b);
|
||
|
|
|
||
|
|
// 4. Set i to i+1.
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Otherwise, d < 0xDC00 or d > 0xDFFF. Append to U a
|
||
|
|
// U+FFFD REPLACEMENT CHARACTER.
|
||
|
|
else {
|
||
|
|
u.push(0xFFFD);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Set i to i+1.
|
||
|
|
i += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. Return U.
|
||
|
|
return u;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {!Array.<number>} code_points Array of code points.
|
||
|
|
* @return {string} string String of UTF-16 code units.
|
||
|
|
*/
|
||
|
|
function codePointsToString(code_points) {
|
||
|
|
var s = '';
|
||
|
|
for (var i = 0; i < code_points.length; ++i) {
|
||
|
|
var cp = code_points[i];
|
||
|
|
if (cp <= 0xFFFF) {
|
||
|
|
s += String.fromCharCode(cp);
|
||
|
|
} else {
|
||
|
|
cp -= 0x10000;
|
||
|
|
s += String.fromCharCode((cp >> 10) + 0xD800,
|
||
|
|
(cp & 0x3FF) + 0xDC00);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return s;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
//
|
||
|
|
// Implementation of Encoding specification
|
||
|
|
// https://encoding.spec.whatwg.org/
|
||
|
|
//
|
||
|
|
|
||
|
|
//
|
||
|
|
// 3. Terminology
|
||
|
|
//
|
||
|
|
|
||
|
|
/**
|
||
|
|
* End-of-stream is a special token that signifies no more tokens
|
||
|
|
* are in the stream.
|
||
|
|
* @const
|
||
|
|
*/ var end_of_stream = -1;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A stream represents an ordered sequence of tokens.
|
||
|
|
*
|
||
|
|
* @constructor
|
||
|
|
* @param {!(Array.<number>|Uint8Array)} tokens Array of tokens that provide the
|
||
|
|
* stream.
|
||
|
|
*/
|
||
|
|
function Stream(tokens) {
|
||
|
|
/** @type {!Array.<number>} */
|
||
|
|
this.tokens = [].slice.call(tokens);
|
||
|
|
}
|
||
|
|
|
||
|
|
Stream.prototype = {
|
||
|
|
/**
|
||
|
|
* @return {boolean} True if end-of-stream has been hit.
|
||
|
|
*/
|
||
|
|
endOfStream: function() {
|
||
|
|
return !this.tokens.length;
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* When a token is read from a stream, the first token in the
|
||
|
|
* stream must be returned and subsequently removed, and
|
||
|
|
* end-of-stream must be returned otherwise.
|
||
|
|
*
|
||
|
|
* @return {number} Get the next token from the stream, or
|
||
|
|
* end_of_stream.
|
||
|
|
*/
|
||
|
|
read: function() {
|
||
|
|
if (!this.tokens.length)
|
||
|
|
return end_of_stream;
|
||
|
|
return this.tokens.shift();
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* When one or more tokens are prepended to a stream, those tokens
|
||
|
|
* must be inserted, in given order, before the first token in the
|
||
|
|
* stream.
|
||
|
|
*
|
||
|
|
* @param {(number|!Array.<number>)} token The token(s) to prepend to the stream.
|
||
|
|
*/
|
||
|
|
prepend: function(token) {
|
||
|
|
if (Array.isArray(token)) {
|
||
|
|
var tokens = /**@type {!Array.<number>}*/(token);
|
||
|
|
while (tokens.length)
|
||
|
|
this.tokens.unshift(tokens.pop());
|
||
|
|
} else {
|
||
|
|
this.tokens.unshift(token);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
/**
|
||
|
|
* When one or more tokens are pushed to a stream, those tokens
|
||
|
|
* must be inserted, in given order, after the last token in the
|
||
|
|
* stream.
|
||
|
|
*
|
||
|
|
* @param {(number|!Array.<number>)} token The tokens(s) to prepend to the stream.
|
||
|
|
*/
|
||
|
|
push: function(token) {
|
||
|
|
if (Array.isArray(token)) {
|
||
|
|
var tokens = /**@type {!Array.<number>}*/(token);
|
||
|
|
while (tokens.length)
|
||
|
|
this.tokens.push(tokens.shift());
|
||
|
|
} else {
|
||
|
|
this.tokens.push(token);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
//
|
||
|
|
// 4. Encodings
|
||
|
|
//
|
||
|
|
|
||
|
|
// 4.1 Encoders and decoders
|
||
|
|
|
||
|
|
/** @const */
|
||
|
|
var finished = -1;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {boolean} fatal If true, decoding errors raise an exception.
|
||
|
|
* @param {number=} opt_code_point Override the standard fallback code point.
|
||
|
|
* @return {number} The code point to insert on a decoding error.
|
||
|
|
*/
|
||
|
|
function decoderError(fatal, opt_code_point) {
|
||
|
|
if (fatal)
|
||
|
|
throw TypeError('Decoder error');
|
||
|
|
return opt_code_point || 0xFFFD;
|
||
|
|
}
|
||
|
|
|
||
|
|
//
|
||
|
|
// 7. API
|
||
|
|
//
|
||
|
|
|
||
|
|
/** @const */ var DEFAULT_ENCODING = 'utf-8';
|
||
|
|
|
||
|
|
// 7.1 Interface TextDecoder
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @constructor
|
||
|
|
* @param {string=} encoding The label of the encoding;
|
||
|
|
* defaults to 'utf-8'.
|
||
|
|
* @param {Object=} options
|
||
|
|
*/
|
||
|
|
function TextDecoder$1(encoding, options) {
|
||
|
|
if (!(this instanceof TextDecoder$1)) {
|
||
|
|
return new TextDecoder$1(encoding, options);
|
||
|
|
}
|
||
|
|
encoding = encoding !== undefined ? String(encoding).toLowerCase() : DEFAULT_ENCODING;
|
||
|
|
if (encoding !== DEFAULT_ENCODING) {
|
||
|
|
throw new Error('Encoding not supported. Only utf-8 is supported');
|
||
|
|
}
|
||
|
|
options = ToDictionary(options);
|
||
|
|
|
||
|
|
/** @private @type {boolean} */
|
||
|
|
this._streaming = false;
|
||
|
|
/** @private @type {boolean} */
|
||
|
|
this._BOMseen = false;
|
||
|
|
/** @private @type {?Decoder} */
|
||
|
|
this._decoder = null;
|
||
|
|
/** @private @type {boolean} */
|
||
|
|
this._fatal = Boolean(options['fatal']);
|
||
|
|
/** @private @type {boolean} */
|
||
|
|
this._ignoreBOM = Boolean(options['ignoreBOM']);
|
||
|
|
|
||
|
|
Object.defineProperty(this, 'encoding', {value: 'utf-8'});
|
||
|
|
Object.defineProperty(this, 'fatal', {value: this._fatal});
|
||
|
|
Object.defineProperty(this, 'ignoreBOM', {value: this._ignoreBOM});
|
||
|
|
}
|
||
|
|
|
||
|
|
TextDecoder$1.prototype = {
|
||
|
|
/**
|
||
|
|
* @param {ArrayBufferView=} input The buffer of bytes to decode.
|
||
|
|
* @param {Object=} options
|
||
|
|
* @return {string} The decoded string.
|
||
|
|
*/
|
||
|
|
decode: function decode(input, options) {
|
||
|
|
var bytes;
|
||
|
|
if (typeof input === 'object' && input instanceof ArrayBuffer) {
|
||
|
|
bytes = new Uint8Array(input);
|
||
|
|
} else if (typeof input === 'object' && 'buffer' in input &&
|
||
|
|
input.buffer instanceof ArrayBuffer) {
|
||
|
|
bytes = new Uint8Array(input.buffer,
|
||
|
|
input.byteOffset,
|
||
|
|
input.byteLength);
|
||
|
|
} else {
|
||
|
|
bytes = new Uint8Array(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
options = ToDictionary(options);
|
||
|
|
|
||
|
|
if (!this._streaming) {
|
||
|
|
this._decoder = new UTF8Decoder({fatal: this._fatal});
|
||
|
|
this._BOMseen = false;
|
||
|
|
}
|
||
|
|
this._streaming = Boolean(options['stream']);
|
||
|
|
|
||
|
|
var input_stream = new Stream(bytes);
|
||
|
|
|
||
|
|
var code_points = [];
|
||
|
|
|
||
|
|
/** @type {?(number|!Array.<number>)} */
|
||
|
|
var result;
|
||
|
|
|
||
|
|
while (!input_stream.endOfStream()) {
|
||
|
|
result = this._decoder.handler(input_stream, input_stream.read());
|
||
|
|
if (result === finished)
|
||
|
|
break;
|
||
|
|
if (result === null)
|
||
|
|
continue;
|
||
|
|
if (Array.isArray(result))
|
||
|
|
code_points.push.apply(code_points, /**@type {!Array.<number>}*/(result));
|
||
|
|
else
|
||
|
|
code_points.push(result);
|
||
|
|
}
|
||
|
|
if (!this._streaming) {
|
||
|
|
do {
|
||
|
|
result = this._decoder.handler(input_stream, input_stream.read());
|
||
|
|
if (result === finished)
|
||
|
|
break;
|
||
|
|
if (result === null)
|
||
|
|
continue;
|
||
|
|
if (Array.isArray(result))
|
||
|
|
code_points.push.apply(code_points, /**@type {!Array.<number>}*/(result));
|
||
|
|
else
|
||
|
|
code_points.push(result);
|
||
|
|
} while (!input_stream.endOfStream());
|
||
|
|
this._decoder = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (code_points.length) {
|
||
|
|
// If encoding is one of utf-8, utf-16be, and utf-16le, and
|
||
|
|
// ignore BOM flag and BOM seen flag are unset, run these
|
||
|
|
// subsubsteps:
|
||
|
|
if (['utf-8'].indexOf(this.encoding) !== -1 &&
|
||
|
|
!this._ignoreBOM && !this._BOMseen) {
|
||
|
|
// If token is U+FEFF, set BOM seen flag.
|
||
|
|
if (code_points[0] === 0xFEFF) {
|
||
|
|
this._BOMseen = true;
|
||
|
|
code_points.shift();
|
||
|
|
} else {
|
||
|
|
// Otherwise, if token is not end-of-stream, set BOM seen
|
||
|
|
// flag and append token to output.
|
||
|
|
this._BOMseen = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return codePointsToString(code_points);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 7.2 Interface TextEncoder
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @constructor
|
||
|
|
* @param {string=} encoding The label of the encoding;
|
||
|
|
* defaults to 'utf-8'.
|
||
|
|
* @param {Object=} options
|
||
|
|
*/
|
||
|
|
function TextEncoder$1(encoding, options) {
|
||
|
|
if (!(this instanceof TextEncoder$1))
|
||
|
|
return new TextEncoder$1(encoding, options);
|
||
|
|
encoding = encoding !== undefined ? String(encoding).toLowerCase() : DEFAULT_ENCODING;
|
||
|
|
if (encoding !== DEFAULT_ENCODING) {
|
||
|
|
throw new Error('Encoding not supported. Only utf-8 is supported');
|
||
|
|
}
|
||
|
|
options = ToDictionary(options);
|
||
|
|
|
||
|
|
/** @private @type {boolean} */
|
||
|
|
this._streaming = false;
|
||
|
|
/** @private @type {?Encoder} */
|
||
|
|
this._encoder = null;
|
||
|
|
/** @private @type {{fatal: boolean}} */
|
||
|
|
this._options = {fatal: Boolean(options['fatal'])};
|
||
|
|
|
||
|
|
Object.defineProperty(this, 'encoding', {value: 'utf-8'});
|
||
|
|
}
|
||
|
|
|
||
|
|
TextEncoder$1.prototype = {
|
||
|
|
/**
|
||
|
|
* @param {string=} opt_string The string to encode.
|
||
|
|
* @param {Object=} options
|
||
|
|
* @return {Uint8Array} Encoded bytes, as a Uint8Array.
|
||
|
|
*/
|
||
|
|
encode: function encode(opt_string, options) {
|
||
|
|
opt_string = opt_string ? String(opt_string) : '';
|
||
|
|
options = ToDictionary(options);
|
||
|
|
|
||
|
|
// NOTE: This option is nonstandard. None of the encodings
|
||
|
|
// permitted for encoding (i.e. UTF-8, UTF-16) are stateful,
|
||
|
|
// so streaming is not necessary.
|
||
|
|
if (!this._streaming)
|
||
|
|
this._encoder = new UTF8Encoder(this._options);
|
||
|
|
this._streaming = Boolean(options['stream']);
|
||
|
|
|
||
|
|
var bytes = [];
|
||
|
|
var input_stream = new Stream(stringToCodePoints(opt_string));
|
||
|
|
/** @type {?(number|!Array.<number>)} */
|
||
|
|
var result;
|
||
|
|
while (!input_stream.endOfStream()) {
|
||
|
|
result = this._encoder.handler(input_stream, input_stream.read());
|
||
|
|
if (result === finished)
|
||
|
|
break;
|
||
|
|
if (Array.isArray(result))
|
||
|
|
bytes.push.apply(bytes, /**@type {!Array.<number>}*/(result));
|
||
|
|
else
|
||
|
|
bytes.push(result);
|
||
|
|
}
|
||
|
|
if (!this._streaming) {
|
||
|
|
while (true) {
|
||
|
|
result = this._encoder.handler(input_stream, input_stream.read());
|
||
|
|
if (result === finished)
|
||
|
|
break;
|
||
|
|
if (Array.isArray(result))
|
||
|
|
bytes.push.apply(bytes, /**@type {!Array.<number>}*/(result));
|
||
|
|
else
|
||
|
|
bytes.push(result);
|
||
|
|
}
|
||
|
|
this._encoder = null;
|
||
|
|
}
|
||
|
|
return new Uint8Array(bytes);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
//
|
||
|
|
// 8. The encoding
|
||
|
|
//
|
||
|
|
|
||
|
|
// 8.1 utf-8
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @constructor
|
||
|
|
* @implements {Decoder}
|
||
|
|
* @param {{fatal: boolean}} options
|
||
|
|
*/
|
||
|
|
function UTF8Decoder(options) {
|
||
|
|
var fatal = options.fatal;
|
||
|
|
|
||
|
|
// utf-8's decoder's has an associated utf-8 code point, utf-8
|
||
|
|
// bytes seen, and utf-8 bytes needed (all initially 0), a utf-8
|
||
|
|
// lower boundary (initially 0x80), and a utf-8 upper boundary
|
||
|
|
// (initially 0xBF).
|
||
|
|
var /** @type {number} */ utf8_code_point = 0,
|
||
|
|
/** @type {number} */ utf8_bytes_seen = 0,
|
||
|
|
/** @type {number} */ utf8_bytes_needed = 0,
|
||
|
|
/** @type {number} */ utf8_lower_boundary = 0x80,
|
||
|
|
/** @type {number} */ utf8_upper_boundary = 0xBF;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param {Stream} stream The stream of bytes being decoded.
|
||
|
|
* @param {number} bite The next byte read from the stream.
|
||
|
|
* @return {?(number|!Array.<number>)} The next code point(s)
|
||
|
|
* decoded, or null if not enough data exists in the input
|
||
|
|
* stream to decode a complete code point.
|
||
|
|
*/
|
||
|
|
this.handler = function(stream, bite) {
|
||
|
|
// 1. If byte is end-of-stream and utf-8 bytes needed is not 0,
|
||
|
|
// set utf-8 bytes needed to 0 and return error.
|
||
|
|
if (bite === end_of_stream && utf8_bytes_needed !== 0) {
|
||
|
|
utf8_bytes_needed = 0;
|
||
|
|
return decoderError(fatal);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. If byte is end-of-stream, return finished.
|
||
|
|
if (bite === end_of_stream)
|
||
|
|
return finished;
|
||
|
|
|
||
|
|
// 3. If utf-8 bytes needed is 0, based on byte:
|
||
|
|
if (utf8_bytes_needed === 0) {
|
||
|
|
|
||
|
|
// 0x00 to 0x7F
|
||
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
||
|
|
// Return a code point whose value is byte.
|
||
|
|
return bite;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0xC2 to 0xDF
|
||
|
|
if (inRange(bite, 0xC2, 0xDF)) {
|
||
|
|
// Set utf-8 bytes needed to 1 and utf-8 code point to byte
|
||
|
|
// − 0xC0.
|
||
|
|
utf8_bytes_needed = 1;
|
||
|
|
utf8_code_point = bite - 0xC0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0xE0 to 0xEF
|
||
|
|
else if (inRange(bite, 0xE0, 0xEF)) {
|
||
|
|
// 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0.
|
||
|
|
if (bite === 0xE0)
|
||
|
|
utf8_lower_boundary = 0xA0;
|
||
|
|
// 2. If byte is 0xED, set utf-8 upper boundary to 0x9F.
|
||
|
|
if (bite === 0xED)
|
||
|
|
utf8_upper_boundary = 0x9F;
|
||
|
|
// 3. Set utf-8 bytes needed to 2 and utf-8 code point to
|
||
|
|
// byte − 0xE0.
|
||
|
|
utf8_bytes_needed = 2;
|
||
|
|
utf8_code_point = bite - 0xE0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 0xF0 to 0xF4
|
||
|
|
else if (inRange(bite, 0xF0, 0xF4)) {
|
||
|
|
// 1. If byte is 0xF0, set utf-8 lower boundary to 0x90.
|
||
|
|
if (bite === 0xF0)
|
||
|
|
utf8_lower_boundary = 0x90;
|
||
|
|
// 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F.
|
||
|
|
if (bite === 0xF4)
|
||
|
|
utf8_upper_boundary = 0x8F;
|
||
|
|
// 3. Set utf-8 bytes needed to 3 and utf-8 code point to
|
||
|
|
// byte − 0xF0.
|
||
|
|
utf8_bytes_needed = 3;
|
||
|
|
utf8_code_point = bite - 0xF0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise
|
||
|
|
else {
|
||
|
|
// Return error.
|
||
|
|
return decoderError(fatal);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Then (byte is in the range 0xC2 to 0xF4) set utf-8 code
|
||
|
|
// point to utf-8 code point << (6 × utf-8 bytes needed) and
|
||
|
|
// return continue.
|
||
|
|
utf8_code_point = utf8_code_point << (6 * utf8_bytes_needed);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. If byte is not in the range utf-8 lower boundary to utf-8
|
||
|
|
// upper boundary, run these substeps:
|
||
|
|
if (!inRange(bite, utf8_lower_boundary, utf8_upper_boundary)) {
|
||
|
|
|
||
|
|
// 1. Set utf-8 code point, utf-8 bytes needed, and utf-8
|
||
|
|
// bytes seen to 0, set utf-8 lower boundary to 0x80, and set
|
||
|
|
// utf-8 upper boundary to 0xBF.
|
||
|
|
utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
|
||
|
|
utf8_lower_boundary = 0x80;
|
||
|
|
utf8_upper_boundary = 0xBF;
|
||
|
|
|
||
|
|
// 2. Prepend byte to stream.
|
||
|
|
stream.prepend(bite);
|
||
|
|
|
||
|
|
// 3. Return error.
|
||
|
|
return decoderError(fatal);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary
|
||
|
|
// to 0xBF.
|
||
|
|
utf8_lower_boundary = 0x80;
|
||
|
|
utf8_upper_boundary = 0xBF;
|
||
|
|
|
||
|
|
// 6. Increase utf-8 bytes seen by one and set utf-8 code point
|
||
|
|
// to utf-8 code point + (byte − 0x80) << (6 × (utf-8 bytes
|
||
|
|
// needed − utf-8 bytes seen)).
|
||
|
|
utf8_bytes_seen += 1;
|
||
|
|
utf8_code_point += (bite - 0x80) << (6 * (utf8_bytes_needed - utf8_bytes_seen));
|
||
|
|
|
||
|
|
// 7. If utf-8 bytes seen is not equal to utf-8 bytes needed,
|
||
|
|
// continue.
|
||
|
|
if (utf8_bytes_seen !== utf8_bytes_needed)
|
||
|
|
return null;
|
||
|
|
|
||
|
|
// 8. Let code point be utf-8 code point.
|
||
|
|
var code_point = utf8_code_point;
|
||
|
|
|
||
|
|
// 9. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes
|
||
|
|
// seen to 0.
|
||
|
|
utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
|
||
|
|
|
||
|
|
// 10. Return a code point whose value is code point.
|
||
|
|
return code_point;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @constructor
|
||
|
|
* @implements {Encoder}
|
||
|
|
* @param {{fatal: boolean}} options
|
||
|
|
*/
|
||
|
|
function UTF8Encoder(options) {
|
||
|
|
var fatal = options.fatal;
|
||
|
|
/**
|
||
|
|
* @param {Stream} stream Input stream.
|
||
|
|
* @param {number} code_point Next code point read from the stream.
|
||
|
|
* @return {(number|!Array.<number>)} Byte(s) to emit.
|
||
|
|
*/
|
||
|
|
this.handler = function(stream, code_point) {
|
||
|
|
// 1. If code point is end-of-stream, return finished.
|
||
|
|
if (code_point === end_of_stream)
|
||
|
|
return finished;
|
||
|
|
|
||
|
|
// 2. If code point is in the range U+0000 to U+007F, return a
|
||
|
|
// byte whose value is code point.
|
||
|
|
if (inRange(code_point, 0x0000, 0x007f))
|
||
|
|
return code_point;
|
||
|
|
|
||
|
|
// 3. Set count and offset based on the range code point is in:
|
||
|
|
var count, offset;
|
||
|
|
// U+0080 to U+07FF: 1 and 0xC0
|
||
|
|
if (inRange(code_point, 0x0080, 0x07FF)) {
|
||
|
|
count = 1;
|
||
|
|
offset = 0xC0;
|
||
|
|
}
|
||
|
|
// U+0800 to U+FFFF: 2 and 0xE0
|
||
|
|
else if (inRange(code_point, 0x0800, 0xFFFF)) {
|
||
|
|
count = 2;
|
||
|
|
offset = 0xE0;
|
||
|
|
}
|
||
|
|
// U+10000 to U+10FFFF: 3 and 0xF0
|
||
|
|
else if (inRange(code_point, 0x10000, 0x10FFFF)) {
|
||
|
|
count = 3;
|
||
|
|
offset = 0xF0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4.Let bytes be a byte sequence whose first byte is (code
|
||
|
|
// point >> (6 × count)) + offset.
|
||
|
|
var bytes = [(code_point >> (6 * count)) + offset];
|
||
|
|
|
||
|
|
// 5. Run these substeps while count is greater than 0:
|
||
|
|
while (count > 0) {
|
||
|
|
|
||
|
|
// 1. Set temp to code point >> (6 × (count − 1)).
|
||
|
|
var temp = code_point >> (6 * (count - 1));
|
||
|
|
|
||
|
|
// 2. Append to bytes 0x80 | (temp & 0x3F).
|
||
|
|
bytes.push(0x80 | (temp & 0x3F));
|
||
|
|
|
||
|
|
// 3. Decrease count by one.
|
||
|
|
count -= 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 6. Return bytes bytes, in order.
|
||
|
|
return bytes;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function getGlobal() {
|
||
|
|
if (typeof self !== 'undefined') return self;
|
||
|
|
if (typeof global !== 'undefined') return global;
|
||
|
|
throw new Error('No global found');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof TextDecoder !== 'function') {
|
||
|
|
getGlobal().TextDecoder = TextDecoder$1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof TextEncoder !== 'function') {
|
||
|
|
getGlobal().TextEncoder = TextEncoder$1;
|
||
|
|
}
|
||
|
|
|
||
|
|
})();
|
||
|
|
class PntsParser{
|
||
|
|
constructor(){
|
||
|
|
this.utf8Decoder = new TextDecoder('utf-8');
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
parse(buffer) {
|
||
|
|
|
||
|
|
if (!buffer) {
|
||
|
|
return Promise.reject(buffer);
|
||
|
|
}
|
||
|
|
const view = new DataView(buffer);
|
||
|
|
|
||
|
|
let byteOffset = 0;
|
||
|
|
const pntsHeader = {};
|
||
|
|
let batchTable = {};
|
||
|
|
let point = {};
|
||
|
|
|
||
|
|
// Magic type is unsigned char [4]
|
||
|
|
pntsHeader.magic = this.utf8Decoder.decode(new Uint8Array(buffer, byteOffset, 4));
|
||
|
|
byteOffset += 4;
|
||
|
|
|
||
|
|
if (pntsHeader.magic) {
|
||
|
|
// Version, byteLength, batchTableJSONByteLength, batchTableBinaryByteLength and batchTable types are uint32
|
||
|
|
pntsHeader.version = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
pntsHeader.byteLength = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
pntsHeader.FTJSONLength = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
pntsHeader.FTBinaryLength = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
pntsHeader.BTJSONLength = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
pntsHeader.BTBinaryLength = view.getUint32(byteOffset, true);
|
||
|
|
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
|
||
|
|
|
||
|
|
// binary table
|
||
|
|
if (pntsHeader.FTBinaryLength > 0) {
|
||
|
|
point = this.parseFeatureBinary(buffer, byteOffset, pntsHeader.FTJSONLength);
|
||
|
|
}
|
||
|
|
|
||
|
|
// batch table
|
||
|
|
if (pntsHeader.BTJSONLength > 0) {
|
||
|
|
const sizeBegin = 28 + pntsHeader.FTJSONLength + pntsHeader.FTBinaryLength;
|
||
|
|
batchTable = BatchTableParser.parse(
|
||
|
|
buffer.slice(sizeBegin, pntsHeader.BTJSONLength + sizeBegin));
|
||
|
|
}
|
||
|
|
|
||
|
|
const pnts = {
|
||
|
|
point,
|
||
|
|
batchTable
|
||
|
|
};
|
||
|
|
return Promise.resolve(pnts);
|
||
|
|
} else {
|
||
|
|
throw new Error('Invalid pnts file.');
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
parseFeatureBinary(array, byteOffset, FTJSONLength) {
|
||
|
|
// Init geometry
|
||
|
|
const geometry = new THREE.BufferGeometry();
|
||
|
|
// init Array feature binary
|
||
|
|
const subArrayJson = this.utf8Decoder.decode(new Uint8Array(array, byteOffset, FTJSONLength));
|
||
|
|
const parseJSON = JSON.parse(subArrayJson);
|
||
|
|
let lengthFeature;
|
||
|
|
if (parseJSON.POINTS_LENGTH) {
|
||
|
|
lengthFeature = parseJSON.POINTS_LENGTH;
|
||
|
|
}
|
||
|
|
if (parseJSON.POSITION) {
|
||
|
|
const byteOffsetPos = (parseJSON.POSITION.byteOffset + subArrayJson.length + byteOffset);
|
||
|
|
const positionArray = new Float32Array(array, byteOffsetPos, lengthFeature * 3);
|
||
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3));
|
||
|
|
}
|
||
|
|
if (parseJSON.RGB) {
|
||
|
|
const byteOffsetCol = parseJSON.RGB.byteOffset + subArrayJson.length + byteOffset;
|
||
|
|
const colorArray = new Uint8Array(array, byteOffsetCol, lengthFeature * 3);
|
||
|
|
geometry.setAttribute('color', new THREE.BufferAttribute(colorArray, 3, true));
|
||
|
|
}
|
||
|
|
if (parseJSON.POSITION_QUANTIZED) {
|
||
|
|
throw new Error('For pnts loader, POSITION_QUANTIZED: not yet managed');
|
||
|
|
}
|
||
|
|
if (parseJSON.RGBA) {
|
||
|
|
throw new Error('For pnts loader, RGBA: not yet managed');
|
||
|
|
}
|
||
|
|
if (parseJSON.RGB565) {
|
||
|
|
throw new Error('For pnts loader, RGB565: not yet managed');
|
||
|
|
}
|
||
|
|
if (parseJSON.NORMAL) {
|
||
|
|
//throw new Error('For pnts loader, NORMAL: not yet managed');
|
||
|
|
const byteOffsetNor = parseJSON.RGB.byteOffset + subArrayJson.length + byteOffset;
|
||
|
|
const normalArray = new Float32Array(array, byteOffsetNor, lengthFeature * 3);
|
||
|
|
geometry.setAttribute( 'normal', new THREE.BufferAttribute(normalArray, 3 ));
|
||
|
|
}
|
||
|
|
if (parseJSON.NORMAL_OCT16P) {
|
||
|
|
throw new Error('For pnts loader, NORMAL_OCT16P: not yet managed');
|
||
|
|
}
|
||
|
|
if (parseJSON.BATCH_ID) {
|
||
|
|
throw new Error('For pnts loader, BATCH_ID: not yet managed');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add RTC feature
|
||
|
|
const offset = parseJSON.RTC_CENTER ?
|
||
|
|
new THREE.Vector3().fromArray(parseJSON.RTC_CENTER) : undefined;
|
||
|
|
|
||
|
|
return {
|
||
|
|
geometry,
|
||
|
|
offset,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
CLOUD.PntsParser = PntsParser;
|
||
|
|
;(() => {
|
||
|
|
|
||
|
|
let _inverseSceneMatrix = new THREE.Matrix4();
|
||
|
|
class ExtendTileset{
|
||
|
|
|
||
|
|
constructor(tileset, baseURL){
|
||
|
|
this.tileset = tileset;
|
||
|
|
this.baseURL = baseURL;
|
||
|
|
this.counter = 1;
|
||
|
|
this.index = {};
|
||
|
|
this.recurse(tileset.root, baseURL);
|
||
|
|
}
|
||
|
|
|
||
|
|
recurse(node, baseURL, parent) {
|
||
|
|
// compute transform (will become Object3D.matrix when the object is downloaded)
|
||
|
|
node.transform = node.transform ? (new THREE.Matrix4()).fromArray(node.transform) :
|
||
|
|
undefined;
|
||
|
|
|
||
|
|
// The only reason to store _worldFromLocalTransform is because of extendTileset where we need the
|
||
|
|
// transform chain for one node.
|
||
|
|
node._worldFromLocalTransform = node.transform;
|
||
|
|
if (parent && parent._worldFromLocalTransform) {
|
||
|
|
if (node.transform) {
|
||
|
|
node._worldFromLocalTransform = new THREE.Matrix4().multiplyMatrices(
|
||
|
|
parent._worldFromLocalTransform, node.transform);
|
||
|
|
} else {
|
||
|
|
node._worldFromLocalTransform = parent._worldFromLocalTransform;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// getBox only use inverseTileTransform for volume.region so let's not
|
||
|
|
// compute the inverse matrix each time
|
||
|
|
// Assumes that node.boundingVolume is defined if node.viewerRequestVolume is undefined
|
||
|
|
if ((node.viewerRequestVolume && node.viewerRequestVolume.region) ||
|
||
|
|
node.boundingVolume.region) {
|
||
|
|
if (node._worldFromLocalTransform) {
|
||
|
|
_inverseSceneMatrix.copy(node._worldFromLocalTransform).invert();
|
||
|
|
} else {
|
||
|
|
_inverseSceneMatrix.identity();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
node.viewerRequestVolume = node.viewerRequestVolume ? this.getBox(node.viewerRequestVolume,
|
||
|
|
_inverseSceneMatrix) : undefined;
|
||
|
|
node.boundingVolume = this.getBox(node.boundingVolume, _inverseSceneMatrix);
|
||
|
|
|
||
|
|
this.index[this.counter] = node;
|
||
|
|
node.tileId = this.counter;
|
||
|
|
node.baseURL = baseURL;
|
||
|
|
//add
|
||
|
|
node.points = undefined;
|
||
|
|
node.b3dm = undefined;
|
||
|
|
this.counter++;
|
||
|
|
if (node.children) {
|
||
|
|
for (const child of node.children) {
|
||
|
|
this.recurse(child, baseURL, node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
getBox(volume, inverseTileTransform) {
|
||
|
|
if (volume.region) {
|
||
|
|
const region = volume.region;
|
||
|
|
extent.set(
|
||
|
|
THREE.Math.radToDeg(region[0]),
|
||
|
|
THREE.Math.radToDeg(region[2]),
|
||
|
|
THREE.Math.radToDeg(region[1]),
|
||
|
|
THREE.Math.radToDeg(region[3]));
|
||
|
|
const box = new OBB().setFromExtent(extent);
|
||
|
|
// at this point box.matrix = box.epsg4978_from_local, so
|
||
|
|
// we transform it in parent_from_local by using parent's epsg4978_from_local
|
||
|
|
// which from our point of view is epsg4978_from_parent.
|
||
|
|
// box.matrix = (epsg4978_from_parent ^ -1) * epsg4978_from_local
|
||
|
|
// = parent_from_epsg4978 * epsg4978_from_local
|
||
|
|
// = parent_from_local
|
||
|
|
box.matrix.premultiply(inverseTileTransform);
|
||
|
|
// update position, rotation and scale
|
||
|
|
box.matrix.decompose(box.position, box.quaternion, box.scale);
|
||
|
|
return {
|
||
|
|
region: box
|
||
|
|
};
|
||
|
|
} else if (volume.box) {
|
||
|
|
// TODO: only works for axis aligned boxes
|
||
|
|
const box = volume.box;
|
||
|
|
// box[0], box[1], box[2] = center of the box
|
||
|
|
// box[3], box[4], box[5] = x axis direction and half-length
|
||
|
|
// box[6], box[7], box[8] = y axis direction and half-length
|
||
|
|
// box[9], box[10], box[11] = z axis direction and half-length
|
||
|
|
const center = new THREE.Vector3(box[0], box[1], box[2]);
|
||
|
|
const w = center.x - box[3];
|
||
|
|
const e = center.x + box[3];
|
||
|
|
const s = center.y - box[7];
|
||
|
|
const n = center.y + box[7];
|
||
|
|
const b = center.z - box[11];
|
||
|
|
const t = center.z + box[11];
|
||
|
|
|
||
|
|
return {
|
||
|
|
box: new THREE.Box3(new THREE.Vector3(w, s, b), new THREE.Vector3(e, n, t))
|
||
|
|
};
|
||
|
|
} else if (volume.sphere) {
|
||
|
|
const sphere = new THREE.Sphere(new THREE.Vector3(volume.sphere[0], volume.sphere[1], volume
|
||
|
|
.sphere[2]),
|
||
|
|
volume.sphere[3]);
|
||
|
|
return {
|
||
|
|
sphere
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
CLOUD.ExtendTileset = ExtendTileset;
|
||
|
|
})
|
||
|
|
|
||
|
|
class PntLoader {
|
||
|
|
constructor(maximumScreenSpaceError, unitScale) {
|
||
|
|
|
||
|
|
this.unitScale = unitScale;
|
||
|
|
this.pntParser = new PntsParser();
|
||
|
|
this.utf8Decoder = new TextDecoder('utf-8');
|
||
|
|
this.tileIndexMap = {};
|
||
|
|
this.rootPntUrl = undefined;
|
||
|
|
this.maximumScreenSpaceError = maximumScreenSpaceError === undefined ? 5 : maximumScreenSpaceError;
|
||
|
|
this.loadPromises = [];
|
||
|
|
this.camera = undefined;
|
||
|
|
this.globalInverseMatrix = new THREE.Matrix4();
|
||
|
|
this.box = null;
|
||
|
|
|
||
|
|
this.loadJsonPromises = [];
|
||
|
|
this.pntFileList = {};
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* load tilesset.json in 3dtiles
|
||
|
|
*
|
||
|
|
* @param {String} tilesJsonUrl tilesset.json ptah
|
||
|
|
* @param {Function} callback 回调函数
|
||
|
|
*/
|
||
|
|
|
||
|
|
loadPntTotalJson(tilesJsonUrl, callback) {
|
||
|
|
let scope = this;
|
||
|
|
scope.loadPromises = [];
|
||
|
|
this.rootPntUrl = tilesJsonUrl.slice(0, tilesJsonUrl.lastIndexOf('/') + 1);
|
||
|
|
|
||
|
|
const promiseFin = new Promise(function (resolve, reject) {
|
||
|
|
// load json
|
||
|
|
var loader = new THREE.FileLoader();
|
||
|
|
loader.load(tilesJsonUrl, function (text) {
|
||
|
|
let tilesJson = JSON.parse(text);
|
||
|
|
scope.box = tilesJson.root.boundingVolume.box;
|
||
|
|
if (callback) {
|
||
|
|
callback(scope.box);
|
||
|
|
}
|
||
|
|
scope.parseTilesJson(tilesJson.root);
|
||
|
|
|
||
|
|
var handlePromise = Promise.all(scope.loadJsonPromises.map(function (promiseItem) {
|
||
|
|
return promiseItem.catch(function (err) {
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
}));
|
||
|
|
|
||
|
|
var loadPntTaskManager = new CLOUD.TaskManager();
|
||
|
|
handlePromise.then((result) => {
|
||
|
|
// load pnts
|
||
|
|
let count = 0;
|
||
|
|
for (let url in scope.tileIndexMap) {
|
||
|
|
let tileIndex = scope.tileIndexMap[url];
|
||
|
|
const newPrefix = url.slice(0, url.lastIndexOf('/') + 1);
|
||
|
|
for (let id in tileIndex.index) {
|
||
|
|
let subUrl = newPrefix + tileIndex.index[id].content.url;
|
||
|
|
loadPntTaskManager.addTask(count);
|
||
|
|
scope.pntFileList[count] = {};
|
||
|
|
scope.pntFileList[count].jsonUrl = url;
|
||
|
|
scope.pntFileList[count].pntUrl = subUrl;
|
||
|
|
scope.pntFileList[count].index = id;
|
||
|
|
count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
loadPntTaskManager.processTasks(scope.loadPnt.bind(scope), undefined, function (data) {
|
||
|
|
var urlPointGroupMap = {};
|
||
|
|
for (let id in scope.pntFileList) {
|
||
|
|
var url = scope.pntFileList[id].jsonUrl;
|
||
|
|
if (!(url in urlPointGroupMap)) {
|
||
|
|
let pointGroup = new CLOUD.ObjectGroup(CLOUD.ObjectGroupType.EXTRUDEBODYMANAGER, {
|
||
|
|
pickableType: CLOUD.PICKABLETYPE.Geometry,
|
||
|
|
globalSpace: true
|
||
|
|
});
|
||
|
|
urlPointGroupMap[url] = pointGroup;
|
||
|
|
}
|
||
|
|
|
||
|
|
var tileIndex = scope.tileIndexMap[url];
|
||
|
|
tileIndex.index[scope.pntFileList[id].index].points = scope.pntFileList[id].result;
|
||
|
|
urlPointGroupMap[url].add(scope.pntFileList[id].result);
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
var pointList = [];
|
||
|
|
for (var url in urlPointGroupMap) {
|
||
|
|
pointList.push(urlPointGroupMap[url]);
|
||
|
|
}
|
||
|
|
resolve(pointList);
|
||
|
|
})
|
||
|
|
});
|
||
|
|
})
|
||
|
|
});
|
||
|
|
|
||
|
|
return promiseFin;
|
||
|
|
}
|
||
|
|
|
||
|
|
parseTilesJson(context) {
|
||
|
|
let scope = this;
|
||
|
|
|
||
|
|
if (context.content && context.content.url) {
|
||
|
|
let url = this.rootPntUrl + context.content.url;
|
||
|
|
scope.loadTileSetJson(url);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (context.children) {
|
||
|
|
for (let child of context.children) {
|
||
|
|
this.parseTilesJson(child);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
loadTileSetJson(tilesJsonUrl) {
|
||
|
|
let scope = this;
|
||
|
|
const promiseFin = new Promise(function (resolve, reject) {
|
||
|
|
var loader = new THREE.FileLoader();
|
||
|
|
loader.setResponseType('json');
|
||
|
|
loader.load(tilesJsonUrl,
|
||
|
|
function (data) {
|
||
|
|
if (data === null || data === undefined) {
|
||
|
|
reject(data);
|
||
|
|
} else {
|
||
|
|
let tileIndex = new ExtendTileset(data, '');
|
||
|
|
scope.tileIndexMap[tilesJsonUrl] = tileIndex;
|
||
|
|
const rootNode = tileIndex.index['1'];
|
||
|
|
const newPrefix = tilesJsonUrl.slice(0, tilesJsonUrl.lastIndexOf('/') + 1);
|
||
|
|
resolve(tileIndex);
|
||
|
|
}
|
||
|
|
}, undefined,
|
||
|
|
function (err) {
|
||
|
|
reject(data);
|
||
|
|
console.error(`Load ${tilesJsonUrl} failed, ${err}`);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
scope.loadJsonPromises.push(promiseFin);
|
||
|
|
return promiseFin;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* refresh the display of 3dtiles
|
||
|
|
*
|
||
|
|
* @param {CLOUD.Camera} camera camera
|
||
|
|
* @param {Matrix4} globalInverseMatrix 全局变换矩阵
|
||
|
|
*/
|
||
|
|
|
||
|
|
ProcessTileIndexs(camera, globalInverseMatrix) {
|
||
|
|
this.camera = camera;
|
||
|
|
this.globalInverseMatrix = globalInverseMatrix;
|
||
|
|
for (let url in this.tileIndexMap) {
|
||
|
|
let tileset = this.tileIndexMap[url];
|
||
|
|
const rootNode = tileset.index['1'];
|
||
|
|
this.ProcessNode(rootNode, url);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ProcessNode(node, tilesJsonUrl, parent) {
|
||
|
|
node.visible = false;
|
||
|
|
let point = this.tileIndexMap[tilesJsonUrl].index[node.tileId].points;
|
||
|
|
if (point == undefined) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
point.visible = false;
|
||
|
|
const isVisible = true; //$3dTilesCulling(camera, node, point.matrixWorld);
|
||
|
|
if (isVisible && this.$3dTilesSubdivisionControl(this.camera, node)) {
|
||
|
|
point.visible = true;
|
||
|
|
node.visible = true;
|
||
|
|
//output tileId for lod test
|
||
|
|
//console.log(node.tileId + ' ');
|
||
|
|
}
|
||
|
|
if (parent != undefined) {
|
||
|
|
// TODO: ADD
|
||
|
|
if (parent.refine.toUpperCase() === 'ADD') {
|
||
|
|
|
||
|
|
} else {
|
||
|
|
parent.visible = false;
|
||
|
|
let pPoint = this.tileIndexMap[tilesJsonUrl].index[parent.tileId]
|
||
|
|
pPoint.visible = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (node.children) {
|
||
|
|
for (let child of node.children) {
|
||
|
|
this.ProcessNode(child, tilesJsonUrl, node);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$3dTilesSubdivisionControl(camera, node) {
|
||
|
|
const sse = this.computeNodeSSE(camera, node);
|
||
|
|
return sse > this.maximumScreenSpaceError;
|
||
|
|
}
|
||
|
|
|
||
|
|
computeNodeSSE(camera, node) {
|
||
|
|
const boundingVolumeBox = new THREE.Box3();
|
||
|
|
const boundingVolumeSphere = new THREE.Sphere();
|
||
|
|
let cameraPositionWc = camera.position.clone();
|
||
|
|
cameraPositionWc.multiplyScalar(1 / this.unitScale);
|
||
|
|
cameraPositionWc.applyMatrix4(this.globalInverseMatrix);
|
||
|
|
|
||
|
|
node.distance = 0;
|
||
|
|
if (node.boundingVolume.region) {
|
||
|
|
boundingVolumeBox.copy(node.boundingVolume.region.box3D);
|
||
|
|
boundingVolumeBox.applyMatrix4(node.boundingVolume.region.matrixWorld);
|
||
|
|
node.distance = boundingVolumeBox.distanceToPoint(cameraPositionWc);
|
||
|
|
} else if (node.boundingVolume.box) {
|
||
|
|
// boundingVolume.box is affected by matrixWorld
|
||
|
|
boundingVolumeBox.copy(node.boundingVolume.box);
|
||
|
|
if (node.matrixWorld) {
|
||
|
|
boundingVolumeBox.applyMatrix4(node.matrixWorld);
|
||
|
|
}
|
||
|
|
node.distance = boundingVolumeBox.distanceToPoint(cameraPositionWc);
|
||
|
|
} else if (node.boundingVolume.sphere) {
|
||
|
|
// boundingVolume.sphere is affected by matrixWorld
|
||
|
|
boundingVolumeSphere.copy(node.boundingVolume.sphere);
|
||
|
|
boundingVolumeSphere.applyMatrix4(node.matrixWorld);
|
||
|
|
// TODO: see https://github.com/iTowns/itowns/issues/800
|
||
|
|
node.distance = Math.max(0.0,
|
||
|
|
boundingVolumeSphere.distanceToPoint(cameraPositionWc));
|
||
|
|
} else {
|
||
|
|
return Infinity;
|
||
|
|
}
|
||
|
|
if (node.distance === 0) {
|
||
|
|
// This test is needed in case geometricError = distance = 0
|
||
|
|
return Infinity;
|
||
|
|
}
|
||
|
|
|
||
|
|
// fix zero
|
||
|
|
if (node.geometricError - 0 < 0.0001) {
|
||
|
|
node.geometricError = 1;
|
||
|
|
}
|
||
|
|
return camera._preSSE * (node.geometricError / node.distance);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* update Pre Sse
|
||
|
|
*
|
||
|
|
* @param {CLOUD.Camera} camera camera
|
||
|
|
* @param {Number} height the height of canvas
|
||
|
|
* @param {Number} fov the fov of camera
|
||
|
|
*/
|
||
|
|
|
||
|
|
|
||
|
|
updatePreSse(camera, height) {
|
||
|
|
// sse = projected geometric error on screen plane from distance
|
||
|
|
// We're using an approximation, assuming that the geometric error of all
|
||
|
|
// objects is perpendicular to the camera view vector (= we always compute
|
||
|
|
// for worst case).
|
||
|
|
//
|
||
|
|
// screen plane object
|
||
|
|
// | __
|
||
|
|
// | / \
|
||
|
|
// | geometric{|
|
||
|
|
// < fov angle . } sse error {| |
|
||
|
|
// | \__/
|
||
|
|
// |
|
||
|
|
// |<--------------------->
|
||
|
|
// | distance
|
||
|
|
//
|
||
|
|
// geometric_error * screen_width (resp. screen_height)
|
||
|
|
// = ---------------------------------------
|
||
|
|
// 2 * distance * tan (horizontal_fov / 2) (resp. vertical_fov)
|
||
|
|
//
|
||
|
|
//
|
||
|
|
// We pre-compute the preSSE (= constant part of the screen space error formula) once here
|
||
|
|
const fov = camera.fov;
|
||
|
|
const verticalFOV = THREE.Math.degToRad(fov);
|
||
|
|
const verticalPreSSE = height / (2.0 * Math.tan(verticalFOV * 0.5));
|
||
|
|
|
||
|
|
// Note: the preSSE for the horizontal FOV is the same value
|
||
|
|
// focale = (this.height * 0.5) / Math.tan(verticalFOV * 0.5);
|
||
|
|
// horizontalFOV = 2 * Math.atan(this.width * 0.5 / focale);
|
||
|
|
// horizontalPreSSE = this.width / (2.0 * Math.tan(horizontalFOV * 0.5)); (1)
|
||
|
|
// => replacing horizontalFOV in Math.tan(horizontalFOV * 0.5)
|
||
|
|
// Math.tan(horizontalFOV * 0.5) = Math.tan(2 * Math.atan(this.width * 0.5 / focale) * 0.5)
|
||
|
|
// = Math.tan(Math.atan(this.width * 0.5 / focale))
|
||
|
|
// = this.width * 0.5 / focale
|
||
|
|
// => now replacing focale
|
||
|
|
// = this.width * 0.5 / (this.height * 0.5) / Math.tan(verticalFOV * 0.5)
|
||
|
|
// = Math.tan(verticalFOV * 0.5) * this.width / this.height
|
||
|
|
// => back to (1)
|
||
|
|
// horizontalPreSSE = this.width / (2.0 * Math.tan(verticalFOV * 0.5) * this.width / this.height)
|
||
|
|
// = this.height / 2.0 * Math.tan(verticalFOV * 0.5)
|
||
|
|
// = verticalPreSSE
|
||
|
|
camera._preSSE = verticalPreSSE;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
loadPnt(id, callback) {
|
||
|
|
let scope = this;
|
||
|
|
var url = scope.pntFileList[id].pntUrl;
|
||
|
|
return new Promise(function (resolve, reject) {
|
||
|
|
var loader = new THREE.FileLoader();
|
||
|
|
loader.setResponseType('arraybuffer');
|
||
|
|
loader.load(url,
|
||
|
|
function (pnts) {
|
||
|
|
if (pnts !== undefined) {
|
||
|
|
const magic = scope.utf8Decoder.decode(new Uint8Array(pnts, 0, 4));
|
||
|
|
if (magic[0] === '{') {
|
||
|
|
pnts = JSON.parse(scope.utf8Decoder.decode(new Uint8Array(pnts)));
|
||
|
|
const newPrefix = url.slice(0, url.lastIndexOf('/') + 1);
|
||
|
|
} else if (magic == 'b3dm') {
|
||
|
|
console.log('b3dm is load');
|
||
|
|
} else if (magic == 'pnts') {
|
||
|
|
console.log('pnts is load');
|
||
|
|
} else {
|
||
|
|
return Promise.reject(`Unsupported magic code ${magic}`);
|
||
|
|
}
|
||
|
|
scope.pntParser.parse(pnts).then((result) => {
|
||
|
|
const material = new THREE.PointsMaterial({
|
||
|
|
size: 0.05,
|
||
|
|
vertexColors: THREE.VertexColors
|
||
|
|
});
|
||
|
|
|
||
|
|
// creation points with geometry and material
|
||
|
|
const points = new THREE.Points(result.point.geometry, material);
|
||
|
|
|
||
|
|
// Test Normal
|
||
|
|
// if(url.indexOf("point_1_7.pnts") != -1){
|
||
|
|
// var vertexNormalsHelper = new THREE.VertexNormalsHelper(points, 10);
|
||
|
|
// points.add(vertexNormalsHelper);
|
||
|
|
// }
|
||
|
|
|
||
|
|
|
||
|
|
if (result.point.offset) {
|
||
|
|
points.position.copy(result.point.offset);
|
||
|
|
}
|
||
|
|
//可见度设为false
|
||
|
|
points.visible = false;
|
||
|
|
points.name = url;
|
||
|
|
scope.pntFileList[id].result = points;
|
||
|
|
//resolve(points);
|
||
|
|
|
||
|
|
callback();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, undefined,
|
||
|
|
function (err) {
|
||
|
|
reject(undefined);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
loadPntByPromise(url) {
|
||
|
|
let scope = this;
|
||
|
|
return new Promise(function (resolve, reject) {
|
||
|
|
var loader = new THREE.FileLoader();
|
||
|
|
loader.setResponseType('arraybuffer');
|
||
|
|
loader.load(url,
|
||
|
|
function (pnts) {
|
||
|
|
if (pnts !== undefined) {
|
||
|
|
const magic = scope.utf8Decoder.decode(new Uint8Array(pnts, 0, 4));
|
||
|
|
if (magic[0] === '{') {
|
||
|
|
pnts = JSON.parse(scope.utf8Decoder.decode(new Uint8Array(pnts)));
|
||
|
|
const newPrefix = url.slice(0, url.lastIndexOf('/') + 1);
|
||
|
|
} else if (magic == 'b3dm') {
|
||
|
|
console.log('b3dm is load');
|
||
|
|
} else if (magic == 'pnts') {
|
||
|
|
console.log('pnts is load');
|
||
|
|
} else {
|
||
|
|
return Promise.reject(`Unsupported magic code ${magic}`);
|
||
|
|
}
|
||
|
|
scope.pntParser.parse(pnts).then((result) => {
|
||
|
|
const material = new THREE.PointsMaterial({
|
||
|
|
size: 0.05,
|
||
|
|
vertexColors: THREE.VertexColors
|
||
|
|
});
|
||
|
|
|
||
|
|
// creation points with geometry and material
|
||
|
|
const points = new THREE.Points(result.point.geometry, material);
|
||
|
|
|
||
|
|
// Test Normal
|
||
|
|
// if(url.indexOf("point_1_7.pnts") != -1){
|
||
|
|
// var vertexNormalsHelper = new THREE.VertexNormalsHelper(points, 10);
|
||
|
|
// points.add(vertexNormalsHelper);
|
||
|
|
// }
|
||
|
|
|
||
|
|
|
||
|
|
if (result.point.offset) {
|
||
|
|
points.position.copy(result.point.offset);
|
||
|
|
}
|
||
|
|
//可见度设为false
|
||
|
|
points.visible = false;
|
||
|
|
points.name = url;
|
||
|
|
resolve(points);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, undefined,
|
||
|
|
function (err) {
|
||
|
|
reject(undefined);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
loadPntTotalJsonByPromise(tilesJsonUrl, callback) {
|
||
|
|
let scope = this;
|
||
|
|
scope.loadPromises = [];
|
||
|
|
this.rootPntUrl = tilesJsonUrl.slice(0, tilesJsonUrl.lastIndexOf('/') + 1);
|
||
|
|
|
||
|
|
const promiseFin = new Promise(function (resolve, reject) {
|
||
|
|
// load json
|
||
|
|
var loader = new THREE.FileLoader();
|
||
|
|
loader.load(tilesJsonUrl, function (text) {
|
||
|
|
let tilesJson = JSON.parse(text);
|
||
|
|
scope.box = tilesJson.root.boundingVolume.box;
|
||
|
|
if (callback) {
|
||
|
|
callback(scope.box);
|
||
|
|
}
|
||
|
|
scope.parseTilesJson(tilesJson.root);
|
||
|
|
|
||
|
|
var handlePromise = Promise.all(scope.loadJsonPromises.map(function (promiseItem) {
|
||
|
|
return promiseItem.catch(function (err) {
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
}));
|
||
|
|
|
||
|
|
handlePromise.then((result) => {
|
||
|
|
// load pnts
|
||
|
|
var pntLoadPromises = [];
|
||
|
|
for (let url in scope.tileIndexMap) {
|
||
|
|
scope.urlPointGroupMap[url] = {};
|
||
|
|
let tileIndex = scope.tileIndexMap[url];
|
||
|
|
const newPrefix = url.slice(0, url.lastIndexOf('/') + 1);
|
||
|
|
for (let id in tileIndex.index) {
|
||
|
|
let subUrl = newPrefix + tileIndex.index[id].content.url;
|
||
|
|
pntLoadPromises.push(scope.loadPnt(subUrl).then(result => ({
|
||
|
|
url: url,
|
||
|
|
context: {
|
||
|
|
index: id,
|
||
|
|
result
|
||
|
|
}
|
||
|
|
})));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let loadHandlePromise = Promise.all(pntLoadPromises.map(function (promiseItem) {
|
||
|
|
return promiseItem.catch(function (err) {
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
}));
|
||
|
|
|
||
|
|
loadHandlePromise.then((result) => {
|
||
|
|
|
||
|
|
for (let url in scope.urlPointGroupMap) {
|
||
|
|
|
||
|
|
let pointGroup = new CLOUD.ObjectGroup(CLOUD.ObjectGroupType.EXTRUDEBODYMANAGER, {
|
||
|
|
pickableType: CLOUD.PICKABLETYPE.Geometry,
|
||
|
|
globalSpace: true
|
||
|
|
});
|
||
|
|
|
||
|
|
scope.urlPointGroupMap[url] = pointGroup;
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
for (const res of result) {
|
||
|
|
if (res === undefined) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
var tileIndex = scope.tileIndexMap[res.url];
|
||
|
|
tileIndex.index[res.context.index].points = res.context.result;
|
||
|
|
scope.urlPointGroupMap[res.url].add(res.context.result);
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
var pointList = [];
|
||
|
|
for (var url in scope.urlPointGroupMap) {
|
||
|
|
pointList.push(scope.urlPointGroupMap[url]);
|
||
|
|
}
|
||
|
|
resolve(pointList);
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
}).catch(function (reason) {
|
||
|
|
console.log('promise reject failed reason', reason)
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
});
|
||
|
|
|
||
|
|
|
||
|
|
return promiseFin;
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
CLOUD.PntLoader = PntLoader;
|