window.WebGLModule = class {
static idPattern = /^(?!_)(?:(?!__)[0-9a-zA-Z_])*$/;
constructor(incomingOptions) {
this.uniqueId = "";
this.ready = function() { };
this.htmlControlsId = null;
this.webGlPreferredVersion = "2.0";
this.htmlShaderPartHeader = function(title, html, dataId, isVisible, layer, isControllable = true) {
return `<div class="configurable-border"><div class="shader-part-name">${title}</div>${html}</div>`;
};
this.resetCallback = function() { };
this.visualizationReady = function(i, visualization) { };
this.visualizationInUse = function(visualization) { };
this.visualizationChanged = function(oldVis, newVis) { };
this.onError = function(error) {
console.warn("An error has occurred:", error.error, error.desc);
};
this.onFatalError = function(error) {
console.error(error["error"], error["desc"]);
};
this.debug = false;
for (let key in incomingOptions) {
if (incomingOptions.hasOwnProperty(key)) {
this[key] = incomingOptions[key];
}
}
if (!this.constructor.idPattern.test(this.uniqueId)) {
throw "WebGLModule: invalid ID! Id can contain only letters, numbers and underscore (non-consecutive, not at the beginning). ID: " + this.uniqueId;
}
this.webGLImplementation = null;
this.gl = null;
this._programs = {};
this.reset();
try {
WebGLModule.GlContextFactory.init(this, this.webGlPreferredVersion, "2.0", "1.0");
} catch (e) {
this.onFatalError({error: "Unable to initialize the visualization.", desc: e});
console.error(e);
return;
}
console.log(`WebGL ${this.webGLImplementation.getVersion()} Rendering module (ID ${this.uniqueId || '<main>'})`);
this.gl_loaded = function(gl, program, vis) {
WebGLModule.eachValidVisibleVisualizationLayer(vis, layer => layer._renderContext.glLoaded(program, gl));
};
this.gl_drawing = function(gl, program, vis, bounds) {
WebGLModule.eachValidVisibleVisualizationLayer(vis, layer => layer._renderContext.glDrawing(program, bounds, gl));
};
}
reset() {
Object.keys(this._programs).forEach(key => this._unloadProgram(key));
this._visualizations = [];
this._dataSources = [];
this._origDataSources = [];
this._customShaders = [];
this._programs = {};
this._program = -1;
this._prepared = false;
this.running = false;
this._initialized = false;
}
get isPrepared() {
return this._prepared;
}
get isInitialized() {
return this._initialized;
}
addVisualization(...visualizations) {
if (this._prepared) {
console.error("New visualization cannot be introduced after the visualizer was prepared.");
return false;
}
for (let vis of visualizations) {
if (!vis.hasOwnProperty("shaders")) {
console.warn("Invalid visualization: no shaders defined", vis);
continue;
}
let count = 0;
for (let sid in vis.shaders) {
const shader = vis.shaders[sid];
if (!shader.hasOwnProperty("params")) {
shader.params = {};
}
count++;
}
if (count < 0) {
console.warn("Invalid visualization: no shader configuration present!", vis);
continue;
}
this._visualizations.push(vis);
}
return true;
}
addCustomShaderSources(...shaderSources) {
if (this._prepared) {
console.error("The viaGL was already prepared: shaders are no longer add-able.");
return;
}
console.warn("Shader sources are deprecated feature.");
}
foreachVisualization(call) {
this._visualizations.forEach(vis => call(vis));
}
rebuildVisualization(order=undefined) {
let vis = this._visualizations[this._program];
if (order) {
vis.order = order;
}
this._unloadProgram();
this._visualizationToProgram(vis, this._program);
this._forceSwitchShader(this._program);
}
visualization(index) {
return this._visualizations[Math.min(index, this._visualizations.length-1)];
}
currentVisualizationIndex() {
return this._program;
}
switchVisualization(i) {
if (!this._initialized) {
console.warn("WebGLModule::switchVisualization(): not initialized.");
return;
}
if (this._program === i) return;
let oldIndex = this._program;
this._forceSwitchShader(i);
this.visualizationChanged(this._visualizations[oldIndex], this._visualizations[i]);
}
setDimensions(width, height) {
if (width === this.width && height === this.height) return;
this.width = width;
this.height = height;
this.gl.canvas.width = width;
this.gl.canvas.height = height;
this.gl.viewport(0, 0, width, height);
}
getSources() {
return this._dataSources;
}
processImage(data, tileDimension, zoom, pixelSize) {
let result = this.webGLImplementation.toCanvas(this._programs[this._program], this._visualizations[this._program],
data, tileDimension, zoom, pixelSize);
if (this.debug) this._renderDebugIO(data, result);
return result;
}
supportsHtmlControls() {
return typeof this.htmlControlsId === "string" && this.htmlControlsId.length > 0;
}
static eachValidVisualizationLayer(vis, callback,
onFail = (layer, e) => {layer.error = e.message; console.error(e);}) {
let shaders = vis.shaders;
let noError = true;
for (let key in shaders) {
if (shaders.hasOwnProperty(key) && !shaders[key].hasOwnProperty("error")) {
try {
callback(shaders[key]);
} catch (e) {
if (!onFail) throw e;
onFail(shaders[key], e);
noError = false;
}
}
}
return noError;
}
static eachValidVisibleVisualizationLayer(vis, callback,
onFail = (layer, e) => {layer.error = e.message; console.error(e);}) {
let shaders = vis.shaders;
let noError = true;
for (let key in shaders) {
if (shaders.hasOwnProperty(key) && shaders[key].rendering) {
try {
callback(shaders[key]);
} catch (e) {
if (!onFail) throw e;
onFail(shaders[key], e);
noError = false;
}
}
}
return noError;
}
getCurrentProgramIndex() {
if (this._program < 0 || this._program >= this._visualizations.length) this._program = 0;
return this._program;
}
static jsonReplacer(key, value) {
return key.startsWith("_") || ["eventSource"].includes(key) ? undefined : value;
}
prepareAndInit(dataSources=[], width=1, height=1) {
let _this = this;
this.prepare(dataSources, () => {
_this.init(width, height);
});
}
prepare(dataSources, onPrepared, visIndex=0) {
if (this._prepared) {
console.error("Already prepared!");
return;
}
if (this._visualizations.length < 1) {
console.error("No visualization specified!");
this.onFatalError({error: "No visualization specified!",
desc: "::prepare() called with no visualization set."});
return;
}
this._origDataSources = dataSources || [];
this._program = visIndex;
this._prepared = true;
this.getCurrentProgramIndex();
this._visualizationToProgram(this._visualizations[this._program], this._program);
onPrepared();
}
init(width=1, height=1) {
if (!this._prepared) {
console.error("The viaGL was not yet prepared. Call prepare() before init()!");
return;
}
if (this._initialized) {
console.error("Already initialized!");
return;
}
this._initialized = true;
this.setDimensions(width, height);
this.running = true;
this._forceSwitchShader(null);
this.ready();
}
changeBlending(code) {
this.webGLImplementation.setBlendEquation(code);
this.rebuildVisualization();
}
_toBuffers(program, currentVisualization) {
this.webGLImplementation.toBuffers(program, currentVisualization);
}
_forceSwitchShader(i, _reset=true) {
if (isNaN(i) || i === null || i === undefined) i = this._program;
if (i >= this._visualizations.length) {
console.error("Invalid visualization index ", i, "trying to use index 0...");
if (i === 0) return;
i = 0;
}
let target = this._visualizations[i];
if (!this._programs.hasOwnProperty(i)) {
this._visualizationToProgram(target, i);
} else if (i !== this._program) {
this._updateRequiredDataSources(target);
}
this._program = i;
if (target.hasOwnProperty("error")) {
if (this.supportsHtmlControls()) this._loadHtml(i, this._program);
this._loadScript(i, this._program);
this.running = false;
if (this._visualizations.length < 2) {
this.onFatalError(target);
} else {
this.onError(target);
}
} else {
this.running = true;
if (this.supportsHtmlControls()) this._loadHtml(i, this._program);
this._loadDebugInfo();
if (!this._loadScript(i, this._program)) {
if (!_reset) throw "Could not build visualization";
return this._forceSwitchShader(i, false);
}
this._toBuffers(this._programs[i], target);
}
}
_unloadProgram(index = this._program) {
if (this._programs && this._programs.hasOwnProperty(index)) {
let program = this._programs[index];
this._detachShader(program, "VERTEX_SHADER");
this._detachShader(program, "FRAGMENT_SHADER");
}
}
_loadHtml(visId) {
let htmlControls = document.getElementById(this.htmlControlsId);
htmlControls.innerHTML = this._visualizations[visId]._built["html"];
}
_loadScript(visId) {
return WebGLModule.eachValidVisualizationLayer(this._visualizations[visId], layer => layer._renderContext.init());
}
_getDebugInfoPanel() {
return `<div id="test-inner-${this.uniqueId}-webgl">
<b>WebGL Processing I/O (debug mode)</b>
<div id="test-${this.uniqueId}-webgl-log"></div>
Input: <br><div style="border: 1px solid;display: inline-block; overflow: auto;" id='test-${this.uniqueId}-webgl-input'>No input.</div><br>
Output:<br><div style="border: 1px solid;display: inline-block; overflow: auto;" id="test-${this.uniqueId}-webgl-output">No output.</div>`;
}
_loadDebugInfo() {
if (!this.debug) return;
if (!this.supportsHtmlControls()) {
console.warn(`WebGL Renderer ${this.uniqueId} does not support visual rendering without enabled HTML control!`);
return;
}
let container = document.getElementById(`test-${this.uniqueId}-webgl`);
if (!container) {
if (!this.htmlControlsId) {
document.body.innerHTML += `<div id="test-${this.uniqueId}-webgl" style="position:absolute; top:0; right:0; width: 250px">${this._getDebugInfoPanel()}</div>`;
} else {
document.getElementById(this.htmlControlsId).parentElement.innerHTML += `<div id="test-${this.uniqueId}-webgl" style="width: 100%;">${this._getDebugInfoPanel()}</div>`;
}
}
}
async _renderDebugIO(inputData, outputData) {
if (!this.supportsHtmlControls()) {
return;
}
let input = document.getElementById(`test-${this.uniqueId}-webgl-input`);
let output = document.getElementById(`test-${this.uniqueId}-webgl-output`);
input.innerHTML = "";
input.append(WebGLModule.DataLoader.dataAsHtmlElement(inputData));
if (outputData) {
output.innerHTML = "";
if (!this._ocanvas) this._ocanvas = document.createElement("canvas");
this._ocanvas.width = outputData.width;
this._ocanvas.height = outputData.height;
let octx = this._ocanvas.getContext('2d');
octx.drawImage(outputData, 0, 0);
output.append(this._ocanvas);
} else {
output.innerHTML = "No output!";
}
}
_buildFailed(visualization, error) {
console.error(error);
visualization.error = "Failed to compose visualization.";
visualization.desc = error;
}
_buildVisualization(order, visualization) {
try {
let data = this.webGLImplementation.generateVisualization(order, visualization, this.supportsHtmlControls());
if (data.usableShaders < 1) {
this._buildFailed(visualization, `Empty visualization: no valid visualization has been specified.
<br><b>Visualization setup:</b></br> <code>${JSON.stringify(visualization, WebGLModule.jsonReplacer)}</code>
<br><b>Dynamic shader data:</b></br><code>${JSON.stringify(visualization.data)}</code>`);
return null;
}
data.dziExtendedUrl = data.dataUrls.join(",");
visualization._built = data;
delete visualization.error;
delete visualization.desc;
return data;
} catch (error) {
this._buildFailed(visualization, error);
}
return null;
}
_detachShader(program, type) {
let shader = program[type];
if (shader) {
this.gl.detachShader(program, shader);
this.gl.deleteShader(shader);
program[type] = null;
}
}
async _downloadAndRegisterShader(url, headers) {
await fetch(url, {
method: "GET",
body: null,
redirect: 'error',
mode: 'cors',
credentials: 'same-origin',
cache: "no-cache",
referrerPolicy: 'no-referrer',
headers: headers
}).then(response => {
if (response.status < 200 || response.status > 299) {
throw new Error("There was an error when fetching the shader source: " + url);
}
return response.text();
}).then(text => {
let script = document.createElement("script");
script.type = "text/javascript";
script.text = text;
script.onerror = e => {
console.error("Failed to interpret downloaded shader layer script: ", url, "Ignoring this script...");
};
document.body.appendChild(script);
}).catch(e => {
console.error("Failed to download and initialize shader " + url, e);
});
}
_visualizationToProgram(vis, idx) {
if (!vis.hasOwnProperty("_built")) {
vis._built = {};
}
this._updateRequiredDataSources(vis);
this._processVisualization(vis, idx);
return idx;
}
_initializeShaderFactory(visualization, ShaderFactoryClass, layer, idx) {
if (!ShaderFactoryClass) {
layer.error = "Unknown layer type.";
layer.desc = `The layer type '${layer.type}' has no associated factory.`;
console.warn("Skipping layer " + layer.name);
return;
}
const _this = this;
layer._index = idx;
layer.visible = layer.visible ?? true;
layer._renderContext = new ShaderFactoryClass(`${this.uniqueId}${idx}`, {
layer: layer,
webgl: this.webGLImplementation,
invalidate: this.resetCallback,
interactive: this.supportsHtmlControls(),
rebuild: this.rebuildVisualization.bind(this, undefined),
refetch: function() {
_this._updateRequiredDataSources(visualization);
_this.visualizationChanged(visualization, visualization);
}
});
layer._renderContext.construct(layer.params || {}, layer.dataReferences);
if (!layer._renderContext.initialized()) {
console.error(`Invalid shader ${ShaderFactoryClass.name()}! Construct must call super implementation!`);
}
}
_updateRequiredDataSources(vis) {
let usedIds = new Set();
for (let key in vis.shaders) {
if (vis.shaders.hasOwnProperty(key)) {
let layer = vis.shaders[key];
layer.dataReferences.forEach(x => usedIds.add(x));
}
}
usedIds = [...usedIds].sort();
this._dataSources = [];
while (usedIds[usedIds.length-1] >= this._origDataSources.length) {
this._origDataSources.push("__generated_do_not_use__");
}
const usedIdsMax = usedIds[usedIds.length-1] || 0;
this._dataSourceMapping = new Array(Math.max(this._origDataSources.length, usedIdsMax)).fill(-1);
for (let id of usedIds) {
this._dataSourceMapping[id] = this._dataSources.length;
this._dataSources.push(this._origDataSources[id]);
while (id > this._dataSourceMapping.length) {
this._dataSourceMapping.push(-1);
}
}
}
_processVisualization(vis, idx) {
let gl = this.gl,
err = function(message, description) {
vis.error = message;
vis.desc = description;
};
let program;
if (!this._programs.hasOwnProperty(idx)) {
program = gl.createProgram();
this._programs[idx] = program;
let index = 0;
for (let key in vis.shaders) {
if (vis.shaders.hasOwnProperty(key)) {
let layer = vis.shaders[key],
ShaderFactoryClass = WebGLModule.ShaderMediator.getClass(layer.type);
if (layer.type === "none") continue;
this._initializeShaderFactory(vis, ShaderFactoryClass, layer, index++);
}
}
} else {
program = this._programs[idx];
for (let key in vis.shaders) {
if (vis.shaders.hasOwnProperty(key)) {
let layer = vis.shaders[key];
if (!layer.hasOwnProperty("error") && !layer.error &&
layer.hasOwnProperty("_renderContext") &&
layer._renderContext.constructor.type() === layer.type) {
continue;
}
delete layer.error;
delete layer.desc;
if (layer.type === "none") continue;
let ShaderFactoryClass = WebGLModule.ShaderMediator.getClass(layer.type);
this._initializeShaderFactory(vis, ShaderFactoryClass, layer, layer._index);
}
}
}
if (!Array.isArray(vis.order) || vis.order.length < 1) {
vis.order = Object.keys(vis.shaders);
}
this._buildVisualization(vis.order, vis);
if (vis.hasOwnProperty("error") && vis.error) {
this.visualizationReady(idx, vis);
return;
}
this.constructor.compileShader(gl, program,
vis._built["vertex_shader"], vis._built["fragment_shader"], err, this.debug);
this.visualizationReady(idx, vis);
}
static compileShader(gl, program, VS, FS, onError, isDebugMode) {
function ok (kind, status, value, sh) {
if (!gl['get' + kind + 'Parameter'](value, gl[status + '_STATUS'])) {
console.error((sh || 'LINK') + ':\n' + gl['get' + kind + 'InfoLog'](value));
return false;
}
return true;
}
function useShader(gl, program, data, type) {
let shader = gl.createShader(gl[type]);
gl.shaderSource(shader, data);
gl.compileShader(shader);
gl.attachShader(program, shader);
program[type] = shader;
return ok('Shader', 'COMPILE', shader, type);
}
function numberLines(str) {
return str.split('\n').map((line, index) => `${index + 1} ${line}`).join('\n')
}
if (!useShader(gl, program, VS, 'VERTEX_SHADER') ||
!useShader(gl, program, FS, 'FRAGMENT_SHADER')) {
onError("Unable to use this visualization.",
"Compilation of shader failed. For more information, see logs in the console.");
console.warn("VERTEX SHADER\n", numberLines( VS ));
console.warn("FRAGMENT SHADER\n", numberLines( FS ));
} else {
gl.linkProgram(program);
if (!ok('Program', 'LINK', program)) {
onError("Unable to use this visualization.",
"Linking of shader failed. For more information, see logs in the console.");
} else if (isDebugMode) {
console.info("FRAGMENT SHADER\n", numberLines( FS ));
}
}
}
}
WebGLModule.Rasterizer = class {
constructor() {
this.canvas = document.createElement("canvas");
WebGLModule.GlContextFactory._makerInit(this.canvas, {
glContext: function(canvas) {
return canvas.getContext('experimental-webgl', { premultipliedAlpha: false, alpha: true })
|| canvas.getContext('webgl', { premultipliedAlpha: false, alpha: true });
},
webGLImplementation: function(wrapper, glContext) {
return new WebGLModule.RasterizerContext(wrapper, glContext);
}
}, this);
this._program = this.gl.createProgram();
WebGLModule.compileShader(this.gl, this._program,
this.webGLImplementation.vs(), this.webGLImplementation.fs(), (err, desc) => {}, true);
this.webGLImplementation.toBuffers(this._program);
this.running = true;
this._initialized = false;
}
get isInitialized() {
return this._initialized;
}
setDimensions(width, height) {
if (width === this.width && height === this.height) return;
this.width = width;
this.height = height;
this.gl.canvas.width = width;
this.gl.canvas.height = height;
this.gl.viewport(0, 0, width, height);
}
init(width=1, height=1) {
if (this._initialized) {
console.error("Already initialized!");
return;
}
this._initialized = true;
this.setDimensions(width, height);
this.running = true;
gl.useProgram(program);
}
rasterizePolygons(polygonPoints) {
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
let canvas = null;
for (let i = 0; i < polygonPoints.length; i++) {
let data = polygonPoints[i];
let indices = this.constructor.earcut(data.object);
canvas = this.webGLImplementation.toCanvas(this._program, data.object, indices, data.color || [1.0, 1.0, 1.0, 1.0]);
}
return canvas;
}
static earcut(data, holeIndices, dim) {
dim = dim || 2;
let hasHoles = holeIndices && holeIndices.length,
outerLen = hasHoles ? holeIndices[0] * dim : data.length,
outerNode = this.linkedList(data, 0, outerLen, dim, true),
triangles = [];
if (!outerNode || outerNode.next === outerNode.prev) return triangles;
let minX, minY, maxX, maxY, x, y, invSize;
if (hasHoles) outerNode = this.eliminateHoles(data, holeIndices, outerNode, dim);
if (data.length > 80 * dim) {
minX = maxX = data[0];
minY = maxY = data[1];
for (let i = dim; i < outerLen; i += dim) {
x = data[i];
y = data[i + 1];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
invSize = Math.max(maxX - minX, maxY - minY);
invSize = invSize !== 0 ? 1 / invSize : 0;
}
this.earcutLinked(outerNode, triangles, dim, minX, minY, invSize);
return triangles;
}
static linkedList(data, start, end, dim, clockwise) {
let i, last;
if (clockwise === (this.signedArea(data, start, end, dim) > 0)) {
for (i = start; i < end; i += dim) last = this.insertNode(i, data[i], data[i + 1], last);
} else {
for (i = end - dim; i >= start; i -= dim) last = this.insertNode(i, data[i], data[i + 1], last);
}
if (last && this.equals(last, last.next)) {
this.removeNode(last);
last = last.next;
}
return last;
}
static filterPoints(start, end) {
if (!start) return start;
if (!end) end = start;
let p = start,
again;
do {
again = false;
if (!p.steiner && (this.equals(p, p.next) || this.area(p.prev, p, p.next) === 0)) {
this.removeNode(p);
p = end = p.prev;
if (p === p.next) break;
again = true;
} else {
p = p.next;
}
} while (again || p !== end);
return end;
}
static earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
if (!ear) return;
if (!pass && invSize) this.indexCurve(ear, minX, minY, invSize);
let stop = ear,
prev, next;
while (ear.prev !== ear.next) {
prev = ear.prev;
next = ear.next;
if (invSize ? this.isEarHashed(ear, minX, minY, invSize) : this.isEar(ear)) {
triangles.push(prev.i / dim);
triangles.push(ear.i / dim);
triangles.push(next.i / dim);
this.removeNode(ear);
ear = next.next;
stop = next.next;
continue;
}
ear = next;
if (ear === stop) {
if (!pass) {
this.earcutLinked(this.filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
} else if (pass === 1) {
ear = this.cureLocalIntersections(this.filterPoints(ear), triangles, dim);
this.earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
} else if (pass === 2) {
this.splitEarcut(ear, triangles, dim, minX, minY, invSize);
}
break;
}
}
}
static isEar(ear) {
let a = ear.prev,
b = ear,
c = ear.next;
if (this.area(a, b, c) >= 0) return false;
let p = ear.next.next;
while (p !== ear.prev) {
if (this.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
this.area(p.prev, p, p.next) >= 0) return false;
p = p.next;
}
return true;
}
static isEarHashed(ear, minX, minY, invSize) {
let a = ear.prev,
b = ear,
c = ear.next;
if (this.area(a, b, c) >= 0) return false;
let minTX = a.x < b.x ? (a.x < c.x ? a.x : c.x) : (b.x < c.x ? b.x : c.x),
minTY = a.y < b.y ? (a.y < c.y ? a.y : c.y) : (b.y < c.y ? b.y : c.y),
maxTX = a.x > b.x ? (a.x > c.x ? a.x : c.x) : (b.x > c.x ? b.x : c.x),
maxTY = a.y > b.y ? (a.y > c.y ? a.y : c.y) : (b.y > c.y ? b.y : c.y);
let minZ = this.zOrder(minTX, minTY, minX, minY, invSize),
maxZ = this.zOrder(maxTX, maxTY, minX, minY, invSize);
let p = ear.prevZ,
n = ear.nextZ;
while (p && p.z >= minZ && n && n.z <= maxZ) {
if (p !== ear.prev && p !== ear.next &&
this.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
this.area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
if (n !== ear.prev && n !== ear.next &&
this.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
this.area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
while (p && p.z >= minZ) {
if (p !== ear.prev && p !== ear.next &&
this.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
this.area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
}
while (n && n.z <= maxZ) {
if (n !== ear.prev && n !== ear.next &&
this.pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y) &&
this.area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
return true;
}
static cureLocalIntersections(start, triangles, dim) {
let p = start;
do {
let a = p.prev,
b = p.next.next;
if (!this.equals(a, b) && this.intersects(a, p, p.next, b) && this.locallyInside(a, b) && this.locallyInside(b, a)) {
triangles.push(a.i / dim);
triangles.push(p.i / dim);
triangles.push(b.i / dim);
this.removeNode(p);
this.removeNode(p.next);
p = start = b;
}
p = p.next;
} while (p !== start);
return this.filterPoints(p);
}
static splitEarcut(start, triangles, dim, minX, minY, invSize) {
let a = start;
do {
let b = a.next.next;
while (b !== a.prev) {
if (a.i !== b.i && this.isValidDiagonal(a, b)) {
let c = this.splitPolygon(a, b);
a = this.filterPoints(a, a.next);
c = this.filterPoints(c, c.next);
this.earcutLinked(a, triangles, dim, minX, minY, invSize);
this.earcutLinked(c, triangles, dim, minX, minY, invSize);
return;
}
b = b.next;
}
a = a.next;
} while (a !== start);
}
static eliminateHoles(data, holeIndices, outerNode, dim) {
let queue = [],
i, len, start, end, list;
for (i = 0, len = holeIndices.length; i < len; i++) {
start = holeIndices[i] * dim;
end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
list = this.linkedList(data, start, end, dim, false);
if (list === list.next) list.steiner = true;
queue.push(this.getLeftmost(list));
}
queue.sort(this.compareX);
for (i = 0; i < queue.length; i++) {
outerNode = this.eliminateHole(queue[i], outerNode);
outerNode = this.filterPoints(outerNode, outerNode.next);
}
return outerNode;
}
static compareX(a, b) {
return a.x - b.x;
}
static eliminateHole(hole, outerNode) {
let bridge = this.findHoleBridge(hole, outerNode);
if (!bridge) {
return outerNode;
}
let bridgeReverse = this.splitPolygon(bridge, hole);
let filteredBridge = this.filterPoints(bridge, bridge.next);
this.filterPoints(bridgeReverse, bridgeReverse.next);
return outerNode === bridge ? filteredBridge : outerNode;
}
static findHoleBridge(hole, outerNode) {
let p = outerNode,
hx = hole.x,
hy = hole.y,
qx = -Infinity,
m;
do {
if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
let x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
if (x === hx) {
if (hy === p.y) return p;
if (hy === p.next.y) return p.next;
}
m = p.x < p.next.x ? p : p.next;
}
}
p = p.next;
} while (p !== outerNode);
if (!m) return null;
if (hx === qx) return m;
let stop = m,
mx = m.x,
my = m.y,
tanMin = Infinity,
tan;
p = m;
do {
if (hx >= p.x && p.x >= mx && hx !== p.x &&
this.pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
tan = Math.abs(hy - p.y) / (hx - p.x);
if (this.locallyInside(p, hole) &&
(tan < tanMin || (tan === tanMin && (p.x > m.x || (p.x === m.x && this.sectorContainsSector(m, p)))))) {
m = p;
tanMin = tan;
}
}
p = p.next;
} while (p !== stop);
return m;
}
static sectorContainsSector(m, p) {
return this.area(m.prev, m, p.prev) < 0 && this.area(p.next, m, m.next) < 0;
}
static indexCurve(start, minX, minY, invSize) {
let p = start;
do {
if (p.z === null) p.z = this.zOrder(p.x, p.y, minX, minY, invSize);
p.prevZ = p.prev;
p.nextZ = p.next;
p = p.next;
} while (p !== start);
p.prevZ.nextZ = null;
p.prevZ = null;
this.sortLinked(p);
}
static sortLinked(list) {
let i, p, q, e, tail, numMerges, pSize, qSize,
inSize = 1;
do {
p = list;
list = null;
tail = null;
numMerges = 0;
while (p) {
numMerges++;
q = p;
pSize = 0;
for (i = 0; i < inSize; i++) {
pSize++;
q = q.nextZ;
if (!q) break;
}
qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
e = p;
p = p.nextZ;
pSize--;
} else {
e = q;
q = q.nextZ;
qSize--;
}
if (tail) tail.nextZ = e;
else list = e;
e.prevZ = tail;
tail = e;
}
p = q;
}
tail.nextZ = null;
inSize *= 2;
} while (numMerges > 1);
return list;
}
static zOrder(x, y, minX, minY, invSize) {
x = 32767 * (x - minX) * invSize;
y = 32767 * (y - minY) * invSize;
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00FF00FF;
y = (y | (y << 4)) & 0x0F0F0F0F;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return x | (y << 1);
}
static getLeftmost(start) {
let p = start,
leftmost = start;
do {
if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y)) leftmost = p;
p = p.next;
} while (p !== start);
return leftmost;
}
static pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
(ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
(bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
}
static isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !this.intersectsPolygon(a, b) &&
(this.locallyInside(a, b) && this.locallyInside(b, a) && this.middleInside(a, b) &&
(this.area(a.prev, a, b.prev) || this.area(a, b.prev, b)) ||
this.equals(a, b) && this.area(a.prev, a, a.next) > 0 && this.area(b.prev, b, b.next) > 0);
}
static area(p, q, r) {
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
}
static equals(p1, p2) {
return p1.x === p2.x && p1.y === p2.y;
}
static intersects(p1, q1, p2, q2) {
let o1 = this.sign(this.area(p1, q1, p2));
let o2 = this.sign(this.area(p1, q1, q2));
let o3 = this.sign(this.area(p2, q2, p1));
let o4 = this.sign(this.area(p2, q2, q1));
if (o1 !== o2 && o3 !== o4) return true;
if (o1 === 0 && this.onSegment(p1, p2, q1)) return true;
if (o2 === 0 && this.onSegment(p1, q2, q1)) return true;
if (o3 === 0 && this.onSegment(p2, p1, q2)) return true;
if (o4 === 0 && this.onSegment(p2, q1, q2)) return true;
return false;
}
static onSegment(p, q, r) {
return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
}
static sign(num) {
return num > 0 ? 1 : num < 0 ? -1 : 0;
}
static intersectsPolygon(a, b) {
let p = a;
do {
if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
this.intersects(p, p.next, a, b)) return true;
p = p.next;
} while (p !== a);
return false;
}
static locallyInside(a, b) {
return this.area(a.prev, a, a.next) < 0 ?
this.area(a, b, a.next) >= 0 && this.area(a, a.prev, b) >= 0 :
this.area(a, b, a.prev) < 0 || this.area(a, a.next, b) < 0;
}
static middleInside(a, b) {
let p = a,
inside = false,
px = (a.x + b.x) / 2,
py = (a.y + b.y) / 2;
do {
if (((p.y > py) !== (p.next.y > py)) && p.next.y !== p.y &&
(px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x))
inside = !inside;
p = p.next;
} while (p !== a);
return inside;
}
static splitPolygon(a, b) {
let a2 = new this.Node(a.i, a.x, a.y),
b2 = new this.Node(b.i, b.x, b.y),
an = a.next,
bp = b.prev;
a.next = b;
b.prev = a;
a2.next = an;
an.prev = a2;
b2.next = a2;
a2.prev = b2;
bp.next = b2;
b2.prev = bp;
return b2;
}
static insertNode(i, x, y, last) {
let p = new this.Node(i, x, y);
if (!last) {
p.prev = p;
p.next = p;
} else {
p.next = last.next;
p.prev = last;
last.next.prev = p;
last.next = p;
}
return p;
}
static removeNode(p) {
p.next.prev = p.prev;
p.prev.next = p.next;
if (p.prevZ) p.prevZ.nextZ = p.nextZ;
if (p.nextZ) p.nextZ.prevZ = p.prevZ;
}
static Node(i, x, y) {
this.i = i;
this.x = x;
this.y = y;
this.prev = null;
this.next = null;
this.z = null;
this.prevZ = null;
this.nextZ = null;
this.steiner = false;
}
static deviation = function(data, holeIndices, dim, triangles) {
let hasHoles = holeIndices && holeIndices.length;
let outerLen = hasHoles ? holeIndices[0] * dim : data.length;
let polygonArea = Math.abs(this.signedArea(data, 0, outerLen, dim));
if (hasHoles) {
for (var i = 0, len = holeIndices.length; i < len; i++) {
let start = holeIndices[i] * dim;
let end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
polygonArea -= Math.abs(this.signedArea(data, start, end, dim));
}
}
let trianglesArea = 0;
for (i = 0; i < triangles.length; i += 3) {
let a = triangles[i] * dim;
let b = triangles[i + 1] * dim;
let c = triangles[i + 2] * dim;
trianglesArea += Math.abs(
(data[a] - data[c]) * (data[b + 1] - data[a + 1]) -
(data[a] - data[b]) * (data[c + 1] - data[a + 1]));
}
return polygonArea === 0 && trianglesArea === 0 ? 0 :
Math.abs((trianglesArea - polygonArea) / polygonArea);
};
static signedArea(data, start, end, dim) {
let sum = 0;
for (let i = start, j = end - dim; i < end; i += dim) {
sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
j = i;
}
return sum;
}
static flatten = function(data) {
let dim = data[0][0].length,
result = {vertices: [], holes: [], dimensions: dim},
holeIndex = 0;
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].length; j++) {
for (let d = 0; d < dim; d++) result.vertices.push(data[i][j][d]);
}
if (i > 0) {
holeIndex += data[i - 1].length;
result.holes.push(holeIndex);
}
}
return result;
};
};