UNPKG

@opitzconsulting/pie-chart

Version:
641 lines (634 loc) 74 kB
import { Injectable, Component, Input, ElementRef, Output, EventEmitter, NgModule, defineInjectable } from '@angular/core'; import { select, interpolate, arc } from 'd3'; import { BrowserModule } from '@angular/platform-browser'; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class PieChartService { constructor() { } } PieChartService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ PieChartService.ctorParameters = () => []; /** @nocollapse */ PieChartService.ngInjectableDef = defineInjectable({ factory: function PieChartService_Factory() { return new PieChartService(); }, token: PieChartService, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class PieChartComponent { /** * constructor * @param {?} element */ constructor(element) { this.element = element; /** * chart data, which should be displayed */ this.data = []; /** * chart width in pixel */ this.width = 250; /** * chart height in pixel */ this.height = 250; /** * duration of animation transition */ this.duration = 1000; /** * inner spacing in pixel, if greater than 0 it defines the radius of the empty circle in the middle */ this.innerSpacing = 0; /** * outer spacing in pixel */ this.outerSpacing = 1; /** * fired when user clicks on a chart entry */ this.chartClick = new EventEmitter(); /** * fired when user hovers a chart entry */ this.chartHover = new EventEmitter(); /** * current chart data with angle and path definitions, it will be consistent to the representation */ this.curData = []; /** * end chart data with angle and path definitions, it will representate the end state and used only for interpolation */ this.endData = []; /** * copy of last processed data, used to identify changes in ngDoCheck that Angular overlooked */ this.lastData = []; /** * Function for interrupt a running chart animation. Necessary because if transition is still active * when a new transition is started, tween factory function from previos transition will still be fired * until end of transition is reached. For entries which have a started transition the tween factory * function will be fired multiple times with different tween interpolation range! */ this.interrupt = undefined; } /** * Creates a deep copy of an variable. Do not use this function with recursive objects or * browser objects like window or document. * ToDo: should be outsourced. * @template T * @param {?} v * @return {?} */ deepCopy(v) { return JSON.parse(JSON.stringify(v)); } ; ; /** * @return {?} */ ngOnInit() { this.tooltip = /** @type {?} */ (this.element.nativeElement.querySelector('div.pie-chart-tooltip')); } /** * Fired when Angular (re-)sets data-bound properties. This function does not fire when changed data in bound objects or arrays. * Angular only checks references. * @param {?} changes * @return {?} */ ngOnChanges(changes) { // check if entries in bound data property has changed this.detectDataChange(); } ; /** * Fired during every change detection run to detect and act upon changes that Angular can't or won't detect on its own. * @return {?} */ ngDoCheck() { // check if entries in bound data property has changed this.detectDataChange(); } ; /** * Checks whether the data property has changed. This function also check whether only an item property has * changed. In case of change the chart will be rendered. * @return {?} */ detectDataChange() { // fast check: if items were added or removed let /** @type {?} */ dataChanged = (this.data.length !== this.lastData.length); // detail check: if (dataChanged === false) { // loop all items for (let /** @type {?} */ idx = 0; idx < this.data.length; ++idx) { const /** @type {?} */ a = this.data[idx]; const /** @type {?} */ b = this.lastData[idx]; // check internal item properties dataChanged = dataChanged || (a.caption !== b.caption || a.color !== b.color || a.value !== b.value); // for optimization, stop if change detected if (dataChanged) break; } } // if change detected if (dataChanged) { // render chart this.render(); // copy current data to identify changes this.lastData = this.deepCopy(this.data); } } ; /** * Generates a random color for a chart item. * @param {?} value * @return {?} */ generateRandomColor(value) { const /** @type {?} */ hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; // make sure, generated color does not exists yet in data array let /** @type {?} */ color; let /** @type {?} */ uniqueColorGenerated = false; while (uniqueColorGenerated === false) { const /** @type {?} */ h = (Math.random() + 0.618033988749895) % 1; const /** @type {?} */ s = .5; const /** @type {?} */ l = .6; let /** @type {?} */ q = l + s - l * s; let /** @type {?} */ p = 2 * l - q; const /** @type {?} */ r = hue2rgb(p, q, h + 1 / 3); const /** @type {?} */ g = hue2rgb(p, q, h); const /** @type {?} */ b = hue2rgb(p, q, h - 1 / 3); color = '#' + Math.round(r * 255).toString(16) + Math.round(g * 255).toString(16) + Math.round(b * 255).toString(16); uniqueColorGenerated = (this.data.map((d) => d.color).filter((d) => d === color).length === 0); } return color; } ; /** * generates a pie chart item definition * @param {?} item * @param {?} index * @param {?} value * @param {?} startAngle * @param {?} endAngle * @return {?} */ generatePieArcData(item, index, value, startAngle, endAngle) { // generate definition const /** @type {?} */ result = { data: item, index: index, value: value, startAngle: startAngle, endAngle: endAngle, padAngle: 0, innerRadius: this.radius - 40, outerRadius: this.radius }; // generate svg path d-attribute from definition (/** @type {?} */ (result.data)).path = this.pathGenerator(result); // return definition return result; } ; /** * Checks whether items were deleted and initiate delete transition for these items. * @return {?} */ detectDeletedEntries() { // loop current state entries this.curData.forEach((curItem, idx) => { // only check if current entry is not marked as deleted if (curItem.data.deleted !== true) { // check if entry not exists anymore const /** @type {?} */ isDeleted = (this.data.filter((item) => item.caption === curItem.data.caption).length === 0); // if entry is deleted if (isDeleted) { // mark entry in current state as deleted this.curData[idx].data.deleted = true; // mark entry in end state as deleted and set value to 0 for transtion this.endData[idx].data.deleted = true; this.endData[idx].value = 0; } } }); } ; /** * Checks whether items were inserted and initiate insert transition for these items. * @return {?} */ detectInsertedEntries() { // loop given data array this.data.forEach((item, idx) => { // check if entry is new const /** @type {?} */ isInserted = (this.curData.filter((curItem) => curItem.data.deleted !== true && curItem.data.caption === item.caption).length === 0); // if entry is new if (isInserted) { { const /** @type {?} */ d = this.generatePieArcData(this.deepCopy(item), idx, 0, -1, -1); this.curData.splice(idx, 0, d); } { const /** @type {?} */ d = this.generatePieArcData(this.deepCopy(item), idx, item.value, -1, -1); this.endData.splice(idx, 0, d); } } }); } ; /** * Checks whether items were moved and initiate transition for these items. * @return {?} */ detectMovedEntries() { // separate index in current state array let /** @type {?} */ curIndex = 0; // loop data array for (let /** @type {?} */ index = 0; index < this.data.length; ++index) { // find next index in current state array, skip items marked as deleted while (this.curData[curIndex].data.deleted) ++curIndex; // check if item is moved by comparing captions if (this.data[index].caption !== this.curData[curIndex].data.caption) { { // mark item in current state array as deleted this.curData[curIndex].data.deleted = true; // mark item in end state array as deleted and set value to 0 for transition this.endData[curIndex].data.deleted = true; this.endData[curIndex].value = 0; } { const /** @type {?} */ item = this.deepCopy(this.data[index]); const /** @type {?} */ d = this.generatePieArcData(item, -1, 0, -1, -1); this.curData.splice(curIndex, 0, d); } { const /** @type {?} */ item = this.deepCopy(this.data[index]); const /** @type {?} */ d = this.generatePieArcData(item, -1, item.value, -1, -1); this.endData.splice(curIndex, 0, d); } // because of inserting item to the array's, increment index twice ++curIndex; } ++curIndex; } } ; /** * Synchronize state arrays (curData / endData) with given items (data). * @return {?} */ syncItems() { // sync values and colors this.data.forEach((item, index) => { // find item index in state array's let /** @type {?} */ curIndex = 0; for (let /** @type {?} */ i = 0; i < this.curData.length; ++i) { if (!this.curData[i].data.deleted && this.curData[i].data.caption === item.caption) { curIndex = i; break; } } // update value in state entries this.curData[curIndex].data.value = item.value; this.endData[curIndex].data.value = item.value; // update value in end state entry for transition this.endData[curIndex].value = item.value; // update color in end state entry for transition this.endData[curIndex].data.color = item.color; }); } ; /** * will be triggerd to animate chart changes. * important! this method musst be called within a setTimeout function because of angulars * rendering cycle. * @return {?} */ animateChanges() { // get svg element reference const /** @type {?} */ svg = (/** @type {?} */ (this.element.nativeElement.querySelector('svg'))); // reference all path elements in svg element const /** @type {?} */ paths = select(svg).selectAll('path'); // define interruption function to stop running animations this.interrupt = () => { // call paths interrupt method paths.interrupt(); // delete interupt definition delete this.interrupt; }; // start path animation paths .transition() .duration(this.duration) .attrTween('pie-tween-dummy', (arg0, idx, nodeList) => { // create interpolation functions to calculate step values const /** @type {?} */ iValue = interpolate(this.curData[idx].value, this.endData[idx].value); const /** @type {?} */ iStartAngle = interpolate(this.curData[idx].startAngle, this.endData[idx].startAngle); const /** @type {?} */ iEndAngle = interpolate(this.curData[idx].endAngle, this.endData[idx].endAngle); const /** @type {?} */ iColor = interpolate(this.curData[idx].data.color, this.endData[idx].data.color); // return factory function for animation steps return (t) => { // interpolate values by given transition value this.curData[idx].value = iValue(t); this.curData[idx].startAngle = iStartAngle(t); this.curData[idx].endAngle = iEndAngle(t); this.curData[idx].data.color = iColor(t); // generate new path this.curData[idx].data.path = this.pathGenerator(this.curData[idx]); // return empty string. This is only necessary for typescript compiler. Nothing should be changed here. return ''; }; }) .on('end', (arg0, idx, nodeList) => { // when transition is complete for the last item if (idx === nodeList.length - 1) { // remove as deleted marked entries this.cleanStateItems(); // Delete interupt definition, because everything has finished and nothing can be interrupted. delete this.interrupt; } }); } ; /** * Must be called after transition ends to remove entries in curData and endData which are marked * as deleted. * @return {?} */ cleanStateItems() { // clean current state array for (let /** @type {?} */ i = this.curData.length - 1; i >= 0; --i) { if (this.curData[i].data.deleted === true) { this.curData.splice(i, 1); } } // clean end state array for (let /** @type {?} */ i = this.endData.length - 1; i >= 0; --i) { if (this.endData[i].data.deleted === true) { this.endData.splice(i, 1); } } } ; /** * Checks whether all items have assigned color values and if necessary completes colors in given data array. * @return {?} */ initColors() { // loop all entries this.data.forEach((item) => { // if no color is assigned if (!item.color) { // generate random color for item item.color = this.generateRandomColor(item.value); } }); } ; /** * Returns maximal angle of current state items. * @return {?} */ getMaxAngle() { let /** @type {?} */ maxAngle = 0; this.curData.forEach((curItem) => { if (curItem.endAngle > maxAngle) { maxAngle = curItem.endAngle; } }); return maxAngle; } ; /** * Calculates angles for current and end state items. * @param {?} maxAngle last maximal angle in current state to avoid "jumping" transitions * @return {?} */ calculateAngles(maxAngle) { { // calculate sum of values const /** @type {?} */ total = this.curData.reduce((p, c) => p + c.value, 0); // loop items and calculate start and end angles, initialize rendering let /** @type {?} */ lastAngle = 0; this.curData.forEach((item, idx) => { // calculate angles by last used maximal angle. without data (total=0) simulate 0 values, so draw items in clockwise direction. const /** @type {?} */ nextAngle = lastAngle + ((maxAngle) / ((total === 0) ? 1 : total)) * item.value; item.startAngle = lastAngle; item.endAngle = nextAngle; item.index = idx; item.data.path = this.pathGenerator(item); lastAngle = nextAngle; }); } { // calculate sum of values const /** @type {?} */ total = this.endData.reduce((p, c) => p + c.value, 0); // loop items and calculate start and end angles, initialize rendering let /** @type {?} */ lastAngle = 0; this.endData.forEach((item, idx) => { // calculate angles with circumference. without data (total=0) simulate 0 values, so draw items in anti-clockwise direction. const /** @type {?} */ nextAngle = lastAngle + ((2 * Math.PI) / ((total === 0) ? 1 : total)) * item.value; item.startAngle = lastAngle; item.endAngle = nextAngle; item.index = idx; item.data.path = this.pathGenerator(item); lastAngle = nextAngle; }); } } ; /** * fired when mouse enters a pie chart path element and shows tooltip * @param {?} event * @return {?} */ overPath(event) { // get tooltip-text of path element const /** @type {?} */ txt = (/** @type {?} */ (event.target)).getAttribute('tooltip'); // show tooltip and assign text select(this.tooltip) .html(txt) .style('display', 'block') .transition() .duration(250) .style('opacity', 1); // get index const /** @type {?} */ idx = parseInt((/** @type {?} */ (event.target)).getAttribute('idx'), 10); // get caption of element const /** @type {?} */ caption = this.curData[idx].data.caption; // get original data by caption const /** @type {?} */ item = this.data.filter((d) => d.caption === caption)[0]; // if data found then emit chart click event if (item) { this.chartHover.emit(item); } } ; /** * fired when mouse moves over a pie chart path element and adjusts tooltip * @param {?} event * @return {?} */ movePath(event) { // aggregate scroll positions, because event.page* properties are relative to top left corner of document let /** @type {?} */ offsetX = 0; let /** @type {?} */ offsetY = 0; let /** @type {?} */ element = (/** @type {?} */ (this.tooltip.parentElement)); while (element) { offsetX += element.scrollLeft; offsetY += element.scrollTop; element = element.parentElement; } // adjust tooltip select(this.tooltip) .style('top', (event.pageY - offsetY + 10) + 'px') .style('left', (event.pageX - offsetX + 10) + 'px'); } ; /** * fired when mouse leaves a pie chart path element and hides tooltip * @param {?} event * @return {?} */ outPath(event) { // hide tooltip select(this.tooltip) .transition() .duration(250) .style('opacity', 0) .on('end', () => { select(this.tooltip).style('display', 'none'); }); } ; /** * fired when user clicks on a pie chart path element * @param {?} event * @return {?} */ clickPath(event) { // get index const /** @type {?} */ idx = parseInt((/** @type {?} */ (event.target)).getAttribute('idx'), 10); // get caption of element const /** @type {?} */ caption = this.curData[idx].data.caption; // get original data by caption const /** @type {?} */ item = this.data.filter((d) => d.caption === caption)[0]; // if data found then emit chart click event if (item) { this.chartClick.emit(item); } } ; /** * main rendering function * @return {?} */ render() { // interrupt possible running animations if (this.interrupt) this.interrupt(); // initialize chart colors this.initColors(); // calculate radius this.radius = Math.min(this.width, this.height) / 2; // calculate middle of chart this.center = `translate(${this.width / 2}, ${this.height / 2})`; // create path generator this.pathGenerator = arc().outerRadius(this.radius - this.outerSpacing).innerRadius(this.innerSpacing); // get current maximal angle, necessary to avoid "jumping" transitions const /** @type {?} */ maxAngle = this.getMaxAngle(); // check data array for deleted entries and assign transition configuration this.detectDeletedEntries(); // check data array for inserted entries and assign transition configuration this.detectInsertedEntries(); // check data array for moved entries and assign transition configuration this.detectMovedEntries(); // synchronize data entries with current and end state entries this.syncItems(); // calculate angles for current and end state entries this.calculateAngles(maxAngle); // important! use setTimeout because angular first must exec change detection setTimeout(() => { // start change animations this.animateChanges(); }, 0); } ; } PieChartComponent.decorators = [ { type: Component, args: [{ selector: 'oc-pie-chart', template: `<div class="pie-chart-tooltip"></div> <svg [attr.width]="width" [attr.height]="height"> <g [attr.transform]="center"> <path *ngFor="let d of curData; let idx = index;" [attr.idx]="idx" [attr.fill]="d.data.color" [attr.d]="d.data.path" [attr.tooltip]="d.data.caption" (mouseover)="overPath($event)" (mousemove)="movePath($event);" (mouseout)="outPath($event)" (click)="clickPath($event)" /> </g> </svg>`, styles: [`div.pie-chart-tooltip{position:fixed;display:none;opacity:0;font:12px sans-serif;color:#fff;background-color:rgba(35,47,52,.8);padding:5px}path{opacity:.7;stroke:#fff;stroke-width:2px}path:hover{opacity:1;stroke:#e3e3e3}`] },] }, ]; /** @nocollapse */ PieChartComponent.ctorParameters = () => [ { type: ElementRef } ]; PieChartComponent.propDecorators = { data: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], duration: [{ type: Input }], innerSpacing: [{ type: Input }], outerSpacing: [{ type: Input }], chartClick: [{ type: Output }], chartHover: [{ type: Output }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class PieChartModule { } PieChartModule.decorators = [ { type: NgModule, args: [{ imports: [BrowserModule], declarations: [PieChartComponent], exports: [PieChartComponent] },] }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ export { PieChartService, PieChartComponent, PieChartModule }; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BpdHpjb25zdWx0aW5nLXBpZS1jaGFydC5qcy5tYXAiLCJzb3VyY2VzIjpbIm5nOi8vQG9waXR6Y29uc3VsdGluZy9waWUtY2hhcnQvbGliL3BpZS1jaGFydC5zZXJ2aWNlLnRzIiwibmc6Ly9Ab3BpdHpjb25zdWx0aW5nL3BpZS1jaGFydC9saWIvcGllLWNoYXJ0LmNvbXBvbmVudC50cyIsIm5nOi8vQG9waXR6Y29uc3VsdGluZy9waWUtY2hhcnQvbGliL3BpZS1jaGFydC5tb2R1bGUudHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgSW5qZWN0YWJsZSB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xyXG5cclxuQEluamVjdGFibGUoe1xyXG4gIHByb3ZpZGVkSW46ICdyb290J1xyXG59KVxyXG5leHBvcnQgY2xhc3MgUGllQ2hhcnRTZXJ2aWNlIHtcclxuXHJcbiAgY29uc3RydWN0b3IoKSB7IH1cclxufVxyXG4iLCJpbXBvcnQgeyBDb21wb25lbnQsIElucHV0LCBPbkNoYW5nZXMsIERvQ2hlY2ssIEVsZW1lbnRSZWYsIFNpbXBsZUNoYW5nZXMsIE9uSW5pdCwgT3V0cHV0LCBFdmVudEVtaXR0ZXIgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcclxuXHJcbmltcG9ydCAqIGFzIGQzIGZyb20gJ2QzJztcclxuXHJcbi8qKiBjaGFydCBpdGVtIHByb3BlcnRpZXMgKi9cclxuZXhwb3J0IGludGVyZmFjZSBQaWVDaGFydERhdGEge1xyXG4gIC8qKiB2YWx1ZSBvZiBpdGVtICovXHJcbiAgdmFsdWU6IG51bWJlcjtcclxuICAvKiogY2FwdGlvbiBvZiBpdGVtIChtdXN0IGJlIHVuaXF1ZSkgKi9cclxuICBjYXB0aW9uOiBzdHJpbmc7XHJcbiAgLyoqIG9wdGlvbmFsIGNvbG9yIG9mIGl0ZW0gKGlmIG5vdCBzZXQsIGdlbmVyYXRlZCBhdXRvbWF0aWNhbGx5KSAqL1xyXG4gIGNvbG9yPzogc3RyaW5nO1xyXG59XHJcblxyXG4vKiogaW50ZXJuYWwgY2hhcnQgaXRlbSBwcm9wZXJ0aWVzICovXHJcbmV4cG9ydCBpbnRlcmZhY2UgSW50ZXJuYWxQaWVDaGFydERhdGEgZXh0ZW5kcyBQaWVDaGFydERhdGEge1xyXG4gIC8qKiBzdmcgcGF0aCBmb3IgaXRlbSAqL1xyXG4gIHBhdGg/OiBzdHJpbmc7XHJcbiAgLyoqIGRlbGV0ZSBmbGFnIGZvciByZW1vdmluZyBhZnRlciB0cmFuc2l0aW9uICovXHJcbiAgZGVsZXRlZD86IGJvb2xlYW47XHJcbn1cclxuXHJcbi8qKiBpbnRlcm5hbCB0eXBlIGZvciBvcHRpbWl6YXRpb24gKi9cclxuZXhwb3J0IHR5cGUgUGllQXJjRGF0YSA9IGQzLlBpZUFyY0RhdHVtPEludGVybmFsUGllQ2hhcnREYXRhPiAmIGQzLkRlZmF1bHRBcmNPYmplY3Q7XHJcblxyXG5AQ29tcG9uZW50KHtcclxuICBzZWxlY3RvcjogJ29jLXBpZS1jaGFydCcsXHJcbiAgdGVtcGxhdGU6IGA8ZGl2IGNsYXNzPVwicGllLWNoYXJ0LXRvb2x0aXBcIj48L2Rpdj5cclxuPHN2ZyBbYXR0ci53aWR0aF09XCJ3aWR0aFwiIFthdHRyLmhlaWdodF09XCJoZWlnaHRcIj5cclxuICAgIDxnIFthdHRyLnRyYW5zZm9ybV09XCJjZW50ZXJcIj5cclxuICAgICAgICA8cGF0aCAqbmdGb3I9XCJsZXQgZCBvZiBjdXJEYXRhOyBsZXQgaWR4ID0gaW5kZXg7XCIgW2F0dHIuaWR4XT1cImlkeFwiIFxyXG4gICAgICAgICAgICBbYXR0ci5maWxsXT1cImQuZGF0YS5jb2xvclwiIFthdHRyLmRdPVwiZC5kYXRhLnBhdGhcIiBbYXR0ci50b29sdGlwXT1cImQuZGF0YS5jYXB0aW9uXCJcclxuICAgICAgICAgICAgKG1vdXNlb3Zlcik9XCJvdmVyUGF0aCgkZXZlbnQpXCIgKG1vdXNlbW92ZSk9XCJtb3ZlUGF0aCgkZXZlbnQpO1wiIChtb3VzZW91dCk9XCJvdXRQYXRoKCRldmVudClcIiAoY2xpY2spPVwiY2xpY2tQYXRoKCRldmVudClcIiAvPlxyXG4gICAgPC9nPlxyXG48L3N2Zz5gLFxyXG4gIHN0eWxlczogW2BkaXYucGllLWNoYXJ0LXRvb2x0aXB7cG9zaXRpb246Zml4ZWQ7ZGlzcGxheTpub25lO29wYWNpdHk6MDtmb250OjEycHggc2Fucy1zZXJpZjtjb2xvcjojZmZmO2JhY2tncm91bmQtY29sb3I6cmdiYSgzNSw0Nyw1MiwuOCk7cGFkZGluZzo1cHh9cGF0aHtvcGFjaXR5Oi43O3N0cm9rZTojZmZmO3N0cm9rZS13aWR0aDoycHh9cGF0aDpob3ZlcntvcGFjaXR5OjE7c3Ryb2tlOiNlM2UzZTN9YF1cclxufSlcclxuZXhwb3J0IGNsYXNzIFBpZUNoYXJ0Q29tcG9uZW50IGltcGxlbWVudHMgT25Jbml0LCBPbkNoYW5nZXMsIERvQ2hlY2sge1xyXG4gIC8qKiBjaGFydCBkYXRhLCB3aGljaCBzaG91bGQgYmUgZGlzcGxheWVkICovXHJcbiAgQElucHV0KCkgZGF0YTogQXJyYXk8UGllQ2hhcnREYXRhPiA9IFtdO1xyXG4gIC8qKiBjaGFydCB3aWR0aCBpbiBwaXhlbCAqL1xyXG4gIEBJbnB1dCgpIHdpZHRoID0gMjUwO1xyXG4gIC8qKiBjaGFydCBoZWlnaHQgaW4gcGl4ZWwgKi9cclxuICBASW5wdXQoKSBoZWlnaHQgPSAyNTA7XHJcbiAgLyoqIGR1cmF0aW9uIG9mIGFuaW1hdGlvbiB0cmFuc2l0aW9uICovXHJcbiAgQElucHV0KCkgZHVyYXRpb24gPSAxMDAwO1xyXG4gIC8qKiBpbm5lciBzcGFjaW5nIGluIHBpeGVsLCBpZiBncmVhdGVyIHRoYW4gMCBpdCBkZWZpbmVzIHRoZSByYWRpdXMgb2YgdGhlIGVtcHR5IGNpcmNsZSBpbiB0aGUgbWlkZGxlICovXHJcbiAgQElucHV0KCkgaW5uZXJTcGFjaW5nID0gMDtcclxuICAvKiogb3V0ZXIgc3BhY2luZyBpbiBwaXhlbCAqL1xyXG4gIEBJbnB1dCgpIG91dGVyU3BhY2luZyA9IDE7XHJcbiAgLyoqIGZpcmVkIHdoZW4gdXNlciBjbGlja3Mgb24gYSBjaGFydCBlbnRyeSAqL1xyXG4gIEBPdXRwdXQoKSBjaGFydENsaWNrOiBFdmVudEVtaXR0ZXI8UGllQ2hhcnREYXRhPiA9IG5ldyBFdmVudEVtaXR0ZXIoKTtcclxuICAvKiogZmlyZWQgd2hlbiB1c2VyIGhvdmVycyBhIGNoYXJ0IGVudHJ5ICovXHJcbiAgQE91dHB1dCgpIGNoYXJ0SG92ZXI6IEV2ZW50RW1pdHRlcjxQaWVDaGFydERhdGE+ID0gbmV3IEV2ZW50RW1pdHRlcigpO1xyXG5cclxuICAvKiogcGllIGNoYXJ0IHJhZGl1cyBpbiBwaXhlbCAqL1xyXG4gIHB1YmxpYyByYWRpdXM6IG51bWJlcjtcclxuICAvKiogdHJhbnNmb3JtLWF0dHJpYnV0ZSB0byBjZW50ZXIgY2hhcnQgdmVydGljYWwgYW5kIGhvcml6b250YWwgKi9cclxuICBwdWJsaWMgY2VudGVyOiBzdHJpbmc7XHJcbiAgLyoqIGN1cnJlbnQgY2hhcnQgZGF0YSB3aXRoIGFuZ2xlIGFuZCBwYXRoIGRlZmluaXRpb25zLCBpdCB3aWxsIGJlIGNvbnNpc3RlbnQgdG8gdGhlIHJlcHJlc2VudGF0aW9uICovXHJcbiAgcHVibGljIGN1ckRhdGE6IFBpZUFyY0RhdGFbXSA9IFtdO1xyXG4gIC8qKiBlbmQgY2hhcnQgZGF0YSB3aXRoIGFuZ2xlIGFuZCBwYXRoIGRlZmluaXRpb25zLCBpdCB3aWxsIHJlcHJlc2VudGF0ZSB0aGUgZW5kIHN0YXRlIGFuZCB1c2VkIG9ubHkgZm9yIGludGVycG9sYXRpb24gKi9cclxuICBwcml2YXRlIGVuZERhdGE6IFBpZUFyY0RhdGFbXSA9IFtdO1xyXG4gIC8qKiBwYXRoIGdlbmVyYXRvciBmdW5jdGlvbiAoaW50ZXJuYWwgdXNlIG9ubHkpICovXHJcbiAgcHJvdGVjdGVkIHBhdGhHZW5lcmF0b3I6IGQzLkFyYzxhbnksIGQzLkRlZmF1bHRBcmNPYmplY3Q+O1xyXG4gIC8qKiBjb3B5IG9mIGxhc3QgcHJvY2Vzc2VkIGRhdGEsIHVzZWQgdG8gaWRlbnRpZnkgY2hhbmdlcyBpbiBuZ0RvQ2hlY2sgdGhhdCBBbmd1bGFyIG92ZXJsb29rZWQgKi9cclxuICBwcml2YXRlIGxhc3REYXRhOiBBcnJheTxQaWVDaGFydERhdGE+ID0gW107XHJcblxyXG4gIC8qKlxyXG4gICAqIENyZWF0ZXMgYSBkZWVwIGNvcHkgb2YgYW4gdmFyaWFibGUuIERvIG5vdCB1c2UgdGhpcyBmdW5jdGlvbiB3aXRoIHJlY3Vyc2l2ZSBvYmplY3RzIG9yXHJcbiAgICogYnJvd3NlciBvYmplY3RzIGxpa2Ugd2luZG93IG9yIGRvY3VtZW50LlxyXG4gICAqIFRvRG86IHNob3VsZCBiZSBvdXRzb3VyY2VkLlxyXG4gICAqIEBwYXJhbSB2IFxyXG4gICAqL1xyXG4gIHByb3RlY3RlZCBkZWVwQ29weTxUPih2OiBUKTogVCB7XHJcbiAgICByZXR1cm4gSlNPTi5wYXJzZShKU09OLnN0cmluZ2lmeSh2KSk7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogY29uc3RydWN0b3JcclxuICAgKiBAcGFyYW0gZWxlbWVudCBcclxuICAgKi9cclxuICBjb25zdHJ1Y3RvcihcclxuICAgIHByaXZhdGUgZWxlbWVudDogRWxlbWVudFJlZlxyXG4gICkge307XHJcblxyXG4gIG5nT25Jbml0KCkge1xyXG4gICAgdGhpcy50b29sdGlwID0gdGhpcy5lbGVtZW50Lm5hdGl2ZUVsZW1lbnQucXVlcnlTZWxlY3RvcignZGl2LnBpZS1jaGFydC10b29sdGlwJykgYXMgSFRNTERpdkVsZW1lbnQ7XHJcbiAgfVxyXG5cclxuICAvKipcclxuICAgKiBGaXJlZCB3aGVuIEFuZ3VsYXIgKHJlLSlzZXRzIGRhdGEtYm91bmQgcHJvcGVydGllcy4gVGhpcyBmdW5jdGlvbiBkb2VzIG5vdCBmaXJlIHdoZW4gY2hhbmdlZCBkYXRhIGluIGJvdW5kIG9iamVjdHMgb3IgYXJyYXlzLlxyXG4gICAqIEFuZ3VsYXIgb25seSBjaGVja3MgcmVmZXJlbmNlcy5cclxuICAgKiBAcGFyYW0gY2hhbmdlcyBcclxuICAgKi9cclxuICBuZ09uQ2hhbmdlcyhjaGFuZ2VzOiBTaW1wbGVDaGFuZ2VzKTogdm9pZCB7XHJcbiAgICAvLyBjaGVjayBpZiBlbnRyaWVzIGluIGJvdW5kIGRhdGEgcHJvcGVydHkgaGFzIGNoYW5nZWRcclxuICAgIHRoaXMuZGV0ZWN0RGF0YUNoYW5nZSgpO1xyXG4gIH07XHJcblxyXG4gIC8qKlxyXG4gICAqIEZpcmVkIGR1cmluZyBldmVyeSBjaGFuZ2UgZGV0ZWN0aW9uIHJ1biB0byBkZXRlY3QgYW5kIGFjdCB1cG9uIGNoYW5nZXMgdGhhdCBBbmd1bGFyIGNhbid0IG9yIHdvbid0IGRldGVjdCBvbiBpdHMgb3duLlxyXG4gICAqL1xyXG4gIG5nRG9DaGVjaygpIHtcclxuICAgIC8vIGNoZWNrIGlmIGVudHJpZXMgaW4gYm91bmQgZGF0YSBwcm9wZXJ0eSBoYXMgY2hhbmdlZFxyXG4gICAgdGhpcy5kZXRlY3REYXRhQ2hhbmdlKCk7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogQ2hlY2tzIHdoZXRoZXIgdGhlIGRhdGEgcHJvcGVydHkgaGFzIGNoYW5nZWQuIFRoaXMgZnVuY3Rpb24gYWxzbyBjaGVjayB3aGV0aGVyIG9ubHkgYW4gaXRlbSBwcm9wZXJ0eSBoYXNcclxuICAgKiBjaGFuZ2VkLiBJbiBjYXNlIG9mIGNoYW5nZSB0aGUgY2hhcnQgd2lsbCBiZSByZW5kZXJlZC5cclxuICAgKi9cclxuICBwcm90ZWN0ZWQgZGV0ZWN0RGF0YUNoYW5nZSgpIHtcclxuICAgIC8vIGZhc3QgY2hlY2s6IGlmIGl0ZW1zIHdlcmUgYWRkZWQgb3IgcmVtb3ZlZFxyXG4gICAgbGV0IGRhdGFDaGFuZ2VkID0gKHRoaXMuZGF0YS5sZW5ndGggIT09IHRoaXMubGFzdERhdGEubGVuZ3RoKTtcclxuICAgIC8vIGRldGFpbCBjaGVjazpcclxuICAgIGlmKGRhdGFDaGFuZ2VkID09PSBmYWxzZSl7XHJcbiAgICAgIC8vIGxvb3AgYWxsIGl0ZW1zXHJcbiAgICAgIGZvcihsZXQgaWR4PTA7IGlkeDx0aGlzLmRhdGEubGVuZ3RoOyArK2lkeCl7XHJcbiAgICAgICAgY29uc3QgYSA9IHRoaXMuZGF0YVtpZHhdO1xyXG4gICAgICAgIGNvbnN0IGIgPSB0aGlzLmxhc3REYXRhW2lkeF07XHJcbiAgICAgICAgLy8gY2hlY2sgaW50ZXJuYWwgaXRlbSBwcm9wZXJ0aWVzXHJcbiAgICAgICAgZGF0YUNoYW5nZWQgPSBkYXRhQ2hhbmdlZCB8fCAoYS5jYXB0aW9uICE9PSBiLmNhcHRpb24gfHwgYS5jb2xvciAhPT0gYi5jb2xvciB8fCBhLnZhbHVlICE9PSBiLnZhbHVlKTtcclxuICAgICAgICAvLyBmb3Igb3B0aW1pemF0aW9uLCBzdG9wIGlmIGNoYW5nZSBkZXRlY3RlZFxyXG4gICAgICAgIGlmKGRhdGFDaGFuZ2VkKSBicmVhaztcclxuICAgICAgfVxyXG4gICAgfVxyXG4gICAgLy8gaWYgY2hhbmdlIGRldGVjdGVkXHJcbiAgICBpZihkYXRhQ2hhbmdlZCl7XHJcbiAgICAgIC8vIHJlbmRlciBjaGFydFxyXG4gICAgICB0aGlzLnJlbmRlcigpO1xyXG4gICAgICAvLyBjb3B5IGN1cnJlbnQgZGF0YSB0byBpZGVudGlmeSBjaGFuZ2VzXHJcbiAgICAgIHRoaXMubGFzdERhdGEgPSB0aGlzLmRlZXBDb3B5KHRoaXMuZGF0YSk7XHJcbiAgICB9XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogR2VuZXJhdGVzIGEgcmFuZG9tIGNvbG9yIGZvciBhIGNoYXJ0IGl0ZW0uXHJcbiAgICovXHJcbiAgcHJvdGVjdGVkIGdlbmVyYXRlUmFuZG9tQ29sb3IodmFsdWU6IG51bWJlcik6IHN0cmluZyB7XHJcbiAgICBjb25zdCBodWUycmdiID0gKHA6IG51bWJlciwgcTogbnVtYmVyLCB0OiBudW1iZXIpID0+IHtcclxuICAgICAgaWYodCA8IDApIHQgKz0gMTsgXHJcbiAgICAgIGlmKHQgPiAxKSB0IC09IDE7IFxyXG4gICAgICBpZih0IDwgMS82KSByZXR1cm4gcCArIChxIC0gcCkgKiA2ICogdDtcclxuICAgICAgaWYodCA8IDEvMikgcmV0dXJuIHE7XHJcbiAgICAgIGlmKHQgPCAyLzMpIHJldHVybiBwICsgKHEgLSBwKSAqICgyLzMgLSB0KSAqIDY7XHJcbiAgICAgIHJldHVybiBwO1xyXG4gICAgfTtcclxuICAgIC8vIG1ha2Ugc3VyZSwgZ2VuZXJhdGVkIGNvbG9yIGRvZXMgbm90IGV4aXN0cyB5ZXQgaW4gZGF0YSBhcnJheVxyXG4gICAgbGV0IGNvbG9yO1xyXG4gICAgbGV0IHVuaXF1ZUNvbG9yR2VuZXJhdGVkID0gZmFsc2U7XHJcbiAgICB3aGlsZSh1bmlxdWVDb2xvckdlbmVyYXRlZCA9PT0gZmFsc2Upe1xyXG4gICAgICBjb25zdCBoID0gKE1hdGgucmFuZG9tKCkgKyAwLjYxODAzMzk4ODc0OTg5NSkgJSAxO1xyXG4gICAgICBjb25zdCBzID0gLjU7XHJcbiAgICAgIGNvbnN0IGwgPSAuNjtcclxuICAgICAgbGV0IHEgPSBsICsgcyAtIGwgKiBzO1xyXG4gICAgICBsZXQgcCA9IDIgKiBsIC0gcTtcclxuICAgICAgY29uc3QgciA9IGh1ZTJyZ2IocCwgcSwgaCArIDEvMyk7XHJcbiAgICAgIGNvbnN0IGcgPSBodWUycmdiKHAsIHEsIGgpO1xyXG4gICAgICBjb25zdCBiID0gaHVlMnJnYihwLCBxLCBoIC0gMS8zKTtcclxuICAgICAgY29sb3IgPSAnIycgXHJcbiAgICAgICAgKyBNYXRoLnJvdW5kKHIgKiAyNTUpLnRvU3RyaW5nKDE2KVxyXG4gICAgICAgICsgTWF0aC5yb3VuZChnICogMjU1KS50b1N0cmluZygxNilcclxuICAgICAgICArIE1hdGgucm91bmQoYiAqIDI1NSkudG9TdHJpbmcoMTYpO1xyXG4gICAgICB1bmlxdWVDb2xvckdlbmVyYXRlZCA9ICh0aGlzLmRhdGEubWFwKCAoZCkgPT4gZC5jb2xvcikuZmlsdGVyKCAoZCkgPT4gZCA9PT0gY29sb3IpLmxlbmd0aCA9PT0gMCk7XHJcbiAgICB9XHJcbiAgICByZXR1cm4gY29sb3I7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogZ2VuZXJhdGVzIGEgcGllIGNoYXJ0IGl0ZW0gZGVmaW5pdGlvblxyXG4gICAqIEBwYXJhbSBpdGVtIFxyXG4gICAqIEBwYXJhbSBpbmRleCBcclxuICAgKiBAcGFyYW0gdmFsdWUgXHJcbiAgICogQHBhcmFtIHN0YXJ0QW5nbGUgXHJcbiAgICogQHBhcmFtIGVuZEFuZ2xlIFxyXG4gICAqL1xyXG4gIHByb3RlY3RlZCBnZW5lcmF0ZVBpZUFyY0RhdGEoaXRlbTogUGllQ2hhcnREYXRhLCBpbmRleDogbnVtYmVyLCB2YWx1ZTogbnVtYmVyLCBzdGFydEFuZ2xlOiBudW1iZXIsIGVuZEFuZ2xlOiBudW1iZXIpOiBQaWVBcmNEYXRhIHtcclxuICAgIC8vIGdlbmVyYXRlIGRlZmluaXRpb25cclxuICAgIGNvbnN0IHJlc3VsdCA9IHtcclxuICAgICAgZGF0YTogaXRlbSxcclxuICAgICAgaW5kZXg6IGluZGV4LFxyXG4gICAgICB2YWx1ZTogdmFsdWUsXHJcbiAgICAgIHN0YXJ0QW5nbGU6IHN0YXJ0QW5nbGUsXHJcbiAgICAgIGVuZEFuZ2xlOiBlbmRBbmdsZSxcclxuICAgICAgcGFkQW5nbGU6IDAsXHJcbiAgICAgIGlubmVyUmFkaXVzOiB0aGlzLnJhZGl1cyAtIDQwLFxyXG4gICAgICBvdXRlclJhZGl1czogdGhpcy5yYWRpdXNcclxuICAgIH07XHJcbiAgICAvLyBnZW5lcmF0ZSBzdmcgcGF0aCBkLWF0dHJpYnV0ZSBmcm9tIGRlZmluaXRpb25cclxuICAgIChyZXN1bHQuZGF0YSBhcyBJbnRlcm5hbFBpZUNoYXJ0RGF0YSkucGF0aCA9IHRoaXMucGF0aEdlbmVyYXRvcihyZXN1bHQpO1xyXG4gICAgLy8gcmV0dXJuIGRlZmluaXRpb25cclxuICAgIHJldHVybiByZXN1bHQ7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogQ2hlY2tzIHdoZXRoZXIgaXRlbXMgd2VyZSBkZWxldGVkIGFuZCBpbml0aWF0ZSBkZWxldGUgdHJhbnNpdGlvbiBmb3IgdGhlc2UgaXRlbXMuXHJcbiAgICovXHJcbiAgcHJvdGVjdGVkIGRldGVjdERlbGV0ZWRFbnRyaWVzKCkge1xyXG4gICAgLy8gbG9vcCBjdXJyZW50IHN0YXRlIGVudHJpZXNcclxuICAgIHRoaXMuY3VyRGF0YS5mb3JFYWNoKCAoY3VySXRlbSwgaWR4KSA9PiB7XHJcbiAgICAgIC8vIG9ubHkgY2hlY2sgaWYgY3VycmVudCBlbnRyeSBpcyBub3QgbWFya2VkIGFzIGRlbGV0ZWRcclxuICAgICAgaWYoY3VySXRlbS5kYXRhLmRlbGV0ZWQhPT10cnVlKXtcclxuICAgICAgICAvLyBjaGVjayBpZiBlbnRyeSBub3QgZXhpc3RzIGFueW1vcmVcclxuICAgICAgICBjb25zdCBpc0RlbGV0ZWQgPSAodGhpcy5kYXRhLmZpbHRlciggKGl0ZW0pID0+IGl0ZW0uY2FwdGlvbiA9PT0gY3VySXRlbS5kYXRhLmNhcHRpb24pLmxlbmd0aCA9PT0gMCk7XHJcbiAgICAgICAgLy8gaWYgZW50cnkgaXMgZGVsZXRlZFxyXG4gICAgICAgIGlmKGlzRGVsZXRlZCl7XHJcbiAgICAgICAgICAvLyBtYXJrIGVudHJ5IGluIGN1cnJlbnQgc3RhdGUgYXMgZGVsZXRlZFxyXG4gICAgICAgICAgdGhpcy5jdXJEYXRhW2lkeF0uZGF0YS5kZWxldGVkID0gdHJ1ZTtcclxuICAgICAgICAgIC8vIG1hcmsgZW50cnkgaW4gZW5kIHN0YXRlIGFzIGRlbGV0ZWQgYW5kIHNldCB2YWx1ZSB0byAwIGZvciB0cmFuc3Rpb25cclxuICAgICAgICAgIHRoaXMuZW5kRGF0YVtpZHhdLmRhdGEuZGVsZXRlZCA9IHRydWU7XHJcbiAgICAgICAgICB0aGlzLmVuZERhdGFbaWR4XS52YWx1ZSA9IDA7XHJcbiAgICAgICAgfVxyXG4gICAgICB9XHJcbiAgICB9KTtcclxuICB9O1xyXG5cclxuICAvKipcclxuICAgKiBDaGVja3Mgd2hldGhlciBpdGVtcyB3ZXJlIGluc2VydGVkIGFuZCBpbml0aWF0ZSBpbnNlcnQgdHJhbnNpdGlvbiBmb3IgdGhlc2UgaXRlbXMuXHJcbiAgICovXHJcbiAgcHJvdGVjdGVkIGRldGVjdEluc2VydGVkRW50cmllcygpOiB2b2lkIHtcclxuICAgIC8vIGxvb3AgZ2l2ZW4gZGF0YSBhcnJheVxyXG4gICAgdGhpcy5kYXRhLmZvckVhY2goIChpdGVtLCBpZHgpID0+IHtcclxuICAgICAgLy8gY2hlY2sgaWYgZW50cnkgaXMgbmV3XHJcbiAgICAgIGNvbnN0IGlzSW5zZXJ0ZWQgPSAodGhpcy5jdXJEYXRhLmZpbHRlciggKGN1ckl0ZW0pID0+IGN1ckl0ZW0uZGF0YS5kZWxldGVkIT09dHJ1ZSAmJiBjdXJJdGVtLmRhdGEuY2FwdGlvbiA9PT0gaXRlbS5jYXB0aW9uKS5sZW5ndGg9PT0wKTtcclxuICAgICAgLy8gaWYgZW50cnkgaXMgbmV3XHJcbiAgICAgIGlmKGlzSW5zZXJ0ZWQpe1xyXG4gICAgICAgIC8vIGdlbmVyYXRlIGN1cnJlbnQgc3RhdGUgZW50cnkgd2l0aCB2YWx1ZSBvZiAwIGZvciB0cmFuc2l0aW9uXHJcbiAgICAgICAge1xyXG4gICAgICAgICAgY29uc3QgZCA9IHRoaXMuZ2VuZXJhdGVQaWVBcmNEYXRhKHRoaXMuZGVlcENvcHkoaXRlbSksIGlkeCwgMCwgLTEsIC0xKTtcclxuICAgICAgICAgIHRoaXMuY3VyRGF0YS5zcGxpY2UoaWR4LCAwLCBkKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgLy8gZ2VuZXJhdGUgZW5kIHN0YXRlIGVudHJ5IHdpdGggZ2l2ZW4gdmFsdWVcclxuICAgICAgICB7XHJcbiAgICAgICAgICBjb25zdCBkID0gdGhpcy5nZW5lcmF0ZVBpZUFyY0RhdGEodGhpcy5kZWVwQ29weShpdGVtKSwgaWR4LCBpdGVtLnZhbHVlLCAtMSwgLTEpO1xyXG4gICAgICAgICAgdGhpcy5lbmREYXRhLnNwbGljZShpZHgsIDAsIGQpO1xyXG4gICAgICAgIH1cclxuICAgICAgfVxyXG4gICAgfSk7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogQ2hlY2tzIHdoZXRoZXIgaXRlbXMgd2VyZSBtb3ZlZCBhbmQgaW5pdGlhdGUgdHJhbnNpdGlvbiBmb3IgdGhlc2UgaXRlbXMuXHJcbiAgICovXHJcbiAgcHJvdGVjdGVkIGRldGVjdE1vdmVkRW50cmllcygpOiB2b2lkIHtcclxuICAgIC8vIHNlcGFyYXRlIGluZGV4IGluIGN1cnJlbnQgc3RhdGUgYXJyYXlcclxuICAgIGxldCBjdXJJbmRleCA9IDA7XHJcbiAgICAvLyBsb29wIGRhdGEgYXJyYXlcclxuICAgIGZvcihsZXQgaW5kZXg9MDsgaW5kZXg8dGhpcy5kYXRhLmxlbmd0aDsgKytpbmRleCl7XHJcbiAgICAgIC8vIGZpbmQgbmV4dCBpbmRleCBpbiBjdXJyZW50IHN0YXRlIGFycmF5LCBza2lwIGl0ZW1zIG1hcmtlZCBhcyBkZWxldGVkXHJcbiAgICAgIHdoaWxlKHRoaXMuY3VyRGF0YVtjdXJJbmRleF0uZGF0YS5kZWxldGVkKSArK2N1ckluZGV4OyBcclxuICAgICAgLy8gY2hlY2sgaWYgaXRlbSBpcyBtb3ZlZCBieSBjb21wYXJpbmcgY2FwdGlvbnNcclxuICAgICAgaWYodGhpcy5kYXRhW2luZGV4XS5jYXB0aW9uICE9PSB0aGlzLmN1ckRhdGFbY3VySW5kZXhdLmRhdGEuY2FwdGlvbil7XHJcbiAgICAgICAgLy8gdXBkYXRpbmcgc3RhdGUgaXRlbXNcclxuICAgICAgICB7XHJcbiAgICAgICAgICAvLyBtYXJrIGl0ZW0gaW4gY3VycmVudCBzdGF0ZSBhcnJheSBhcyBkZWxldGVkXHJcbiAgICAgICAgICB0aGlzLmN1ckRhdGFbY3VySW5kZXhdLmRhdGEuZGVsZXRlZCA9IHRydWU7XHJcbiAgICAgICAgICAvLyBtYXJrIGl0ZW0gaW4gZW5kIHN0YXRlIGFycmF5IGFzIGRlbGV0ZWQgYW5kIHNldCB2YWx1ZSB0byAwIGZvciB0cmFuc2l0aW9uXHJcbiAgICAgICAgICB0aGlzLmVuZERhdGFbY3VySW5kZXhdLmRhdGEuZGVsZXRlZCA9IHRydWU7XHJcbiAgICAgICAgICB0aGlzLmVuZERhdGFbY3VySW5kZXhdLnZhbHVlID0gMDtcclxuICAgICAgICB9XHJcbiAgICAgICAgLy8gaW5zZXJ0IGVudHJ5IGluIGN1cnJlbnQgc3RhdGUgYXJyYXkgd2l0aCB2YWx1ZSAwIGZvciB0cmFuc2l0aW9uXHJcbiAgICAgICAge1xyXG4gICAgICAgICAgY29uc3QgaXRlbSA9IHRoaXMuZGVlcENvcHkodGhpcy5kYXRhW2luZGV4XSk7XHJcbiAgICAgICAgICBjb25zdCBkID0gdGhpcy5nZW5lcmF0ZVBpZUFyY0RhdGEoaXRlbSwgLTEsIDAsIC0xLCAtMSk7XHJcbiAgICAgICAgICB0aGlzLmN1ckRhdGEuc3BsaWNlKGN1ckluZGV4LCAwLCBkKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgLy8gaW5zZXJ0IGVudHJ5IGluIGVuZCBzdGF0ZSBhcnJheSB3aXRoIGdpdmVuIHZhbHVlXHJcbiAgICAgICAge1xyXG4gICAgICAgICAgY29uc3QgaXRlbSA9IHRoaXMuZGVlcENvcHkodGhpcy5kYXRhW2luZGV4XSk7XHJcbiAgICAgICAgICBjb25zdCBkID0gdGhpcy5nZW5lcmF0ZVBpZUFyY0RhdGEoaXRlbSwgLTEsIGl0ZW0udmFsdWUsIC0xLCAtMSk7XHJcbiAgICAgICAgICB0aGlzLmVuZERhdGEuc3BsaWNlKGN1ckluZGV4LCAwLCBkKTtcclxuICAgICAgICB9XHJcbiAgICAgICAgLy8gYmVjYXVzZSBvZiBpbnNlcnRpbmcgaXRlbSB0byB0aGUgYXJyYXkncywgaW5jcmVtZW50IGluZGV4IHR3aWNlXHJcbiAgICAgICAgKytjdXJJbmRleDtcclxuICAgICAgfVxyXG4gICAgICArK2N1ckluZGV4O1xyXG4gICAgfVxyXG4gIH07XHJcblxyXG4gIC8qKlxyXG4gICAqIFN5bmNocm9uaXplIHN0YXRlIGFycmF5cyAoY3VyRGF0YSAvIGVuZERhdGEpIHdpdGggZ2l2ZW4gaXRlbXMgKGRhdGEpLlxyXG4gICAqL1xyXG4gIHByb3RlY3RlZCBzeW5jSXRlbXMoKTogdm9pZCB7XHJcbiAgICAvLyBzeW5jIHZhbHVlcyBhbmQgY29sb3JzXHJcbiAgICB0aGlzLmRhdGEuZm9yRWFjaCggKGl0ZW0sIGluZGV4KSA9PiB7XHJcbiAgICAgIC8vIGZpbmQgaXRlbSBpbmRleCBpbiBzdGF0ZSBhcnJheSdzXHJcbiAgICAgIGxldCBjdXJJbmRleCA9IDA7XHJcbiAgICAgIGZvcihsZXQgaT0wOyBpPHRoaXMuY3VyRGF0YS5sZW5ndGg7ICsraSl7XHJcbiAgICAgICAgaWYoIXRoaXMuY3VyRGF0YVtpXS5kYXRhLmRlbGV0ZWQgJiYgdGhpcy5jdXJEYXRhW2ldLmRhdGEuY2FwdGlvbiA9PT0gaXRlbS5jYXB0aW9uKXtcclxuICAgICAgICAgIGN1ckluZGV4ID0gaTtcclxuICAgICAgICAgIGJyZWFrO1xyXG4gICAgICAgIH1cclxuICAgICAgfVxyXG4gICAgICAvLyB1cGRhdGUgdmFsdWUgaW4gc3RhdGUgZW50cmllc1xyXG4gICAgICB0aGlzLmN1ckRhdGFbY3VySW5kZXhdLmRhdGEudmFsdWUgPSBpdGVtLnZhbHVlO1xyXG4gICAgICB0aGlzLmVuZERhdGFbY3VySW5kZXhdLmRhdGEudmFsdWUgPSBpdGVtLnZhbHVlO1xyXG4gICAgICAvLyB1cGRhdGUgdmFsdWUgaW4gZW5kIHN0YXRlIGVudHJ5IGZvciB0cmFuc2l0aW9uXHJcbiAgICAgIHRoaXMuZW5kRGF0YVtjdXJJbmRleF0udmFsdWUgPSBpdGVtLnZhbHVlO1xyXG4gICAgICAvLyB1cGRhdGUgY29sb3IgaW4gZW5kIHN0YXRlIGVudHJ5IGZvciB0cmFuc2l0aW9uXHJcbiAgICAgIHRoaXMuZW5kRGF0YVtjdXJJbmRleF0uZGF0YS5jb2xvciA9IGl0ZW0uY29sb3I7XHJcbiAgICB9KTtcclxuICB9O1xyXG5cclxuICAvKipcclxuICAgKiBGdW5jdGlvbiBmb3IgaW50ZXJydXB0IGEgcnVubmluZyBjaGFydCBhbmltYXRpb24uIE5lY2Vzc2FyeSBiZWNhdXNlIGlmIHRyYW5zaXRpb24gaXMgc3RpbGwgYWN0aXZlXHJcbiAgICogd2hlbiBhIG5ldyB0cmFuc2l0aW9uIGlzIHN0YXJ0ZWQsIHR3ZWVuIGZhY3RvcnkgZnVuY3Rpb24gZnJvbSBwcmV2aW9zIHRyYW5zaXRpb24gd2lsbCBzdGlsbCBiZSBmaXJlZCBcclxuICAgKiB1bnRpbCBlbmQgb2YgdHJhbnNpdGlvbiBpcyByZWFjaGVkLiBGb3IgZW50cmllcyB3aGljaCBoYXZlIGEgc3RhcnRlZCB0cmFuc2l0aW9uIHRoZSB0d2VlbiBmYWN0b3J5XHJcbiAgICogZnVuY3Rpb24gd2lsbCBiZSBmaXJlZCBtdWx0aXBsZSB0aW1lcyB3aXRoIGRpZmZlcmVudCB0d2VlbiBpbnRlcnBvbGF0aW9uIHJhbmdlIVxyXG4gICAqL1xyXG4gIHByb3RlY3RlZCBpbnRlcnJ1cHQ6IEZ1bmN0aW9uID0gdW5kZWZpbmVkO1xyXG5cclxuICAvKipcclxuICAgKiB3aWxsIGJlIHRyaWdnZXJkIHRvIGFuaW1hdGUgY2hhcnQgY2hhbmdlcy5cclxuICAgKiBpbXBvcnRhbnQhIHRoaXMgbWV0aG9kIG11c3N0IGJlIGNhbGxlZCB3aXRoaW4gYSBzZXRUaW1lb3V0IGZ1bmN0aW9uIGJlY2F1c2Ugb2YgYW5ndWxhcnMgXHJcbiAgICogcmVuZGVyaW5nIGN5Y2xlLlxyXG4gICAqL1xyXG4gIHByb3RlY3RlZCBhbmltYXRlQ2hhbmdlcygpOiB2b2lkIHtcclxuICAgIC8vIGdldCBzdmcgZWxlbWVudCByZWZlcmVuY2VcclxuICAgIGNvbnN0IHN2ZyA9ICh0aGlzLmVsZW1lbnQubmF0aXZlRWxlbWVudC5xdWVyeVNlbGVjdG9yKCdzdmcnKSBhcyBTVkdFbGVtZW50KTtcclxuICAgIC8vIHJlZmVyZW5jZSBhbGwgcGF0aCBlbGVtZW50cyBpbiBzdmcgZWxlbWVudFxyXG4gICAgY29uc3QgcGF0aHMgPSBkMy5zZWxlY3Qoc3ZnKS5zZWxlY3RBbGwoJ3BhdGgnKTtcclxuICAgIC8vIGRlZmluZSBpbnRlcnJ1cHRpb24gZnVuY3Rpb24gdG8gc3RvcCBydW5uaW5nIGFuaW1hdGlvbnNcclxuICAgIHRoaXMuaW50ZXJydXB0ID0gKCkgPT4ge1xyXG4gICAgICAvLyBjYWxsIHBhdGhzIGludGVycnVwdCBtZXRob2RcclxuICAgICAgcGF0aHMuaW50ZXJydXB0KCk7XHJcbiAgICAgIC8vIGRlbGV0ZSBpbnRlcnVwdCBkZWZpbml0aW9uXHJcbiAgICAgIGRlbGV0ZSB0aGlzLmludGVycnVwdDtcclxuICAgIH07XHJcbiAgICAvLyBzdGFydCBwYXRoIGFuaW1hdGlvblxyXG4gICAgcGF0aHNcclxuICAgICAgLnRyYW5zaXRpb24oKVxyXG4gICAgICAuZHVyYXRpb24odGhpcy5kdXJhdGlvbilcclxuICAgICAgLy8gVXNlIGQzIGF0dHJUd2VlbiB0cmFuc2l0aW9uIG1ldGhvZCB3aXRoIGR1bW15IGF0dHJpYnV0ZS4gTWFrZSBzdXJlIHRoZSBkdW1teSBhdHRyaWJ1dGUgZG9lcyBub3RcclxuICAgICAgLy8gZXhpc3RzIGF0IHBhdGggZWxlbWVudHMhXHJcbiAgICAgIC5hdHRyVHdlZW4oJ3BpZS10d2Vlbi1kdW1teScsIChhcmcwLCBpZHgsIG5vZGVMaXN0KSA9PiB7XHJcbiAgICAgICAgLy8gY3JlYXRlIGludGVycG9sYXRpb24gZnVuY3Rpb25zIHRvIGNhbGN1bGF0ZSBzdGVwIHZhbHVlc1xyXG4gICAgICAgIGNvbnN0IGlWYWx1ZSA9IGQzLmludGVycG9sYXRlKHRoaXMuY3VyRGF0YVtpZHhdLnZhbHVlLCB0aGlzLmVuZERhdGFbaWR4XS52YWx1ZSk7XHJcbiAgICAgICAgY29uc3QgaVN0YXJ0QW5nbGUgPSBkMy5pbnRlcnBvbGF0ZSh0aGlzLmN1ckRhdGFbaWR4XS5zdGFydEFuZ2xlLCB0aGlzLmVuZERhdGFbaWR4XS5zdGFydEFuZ2xlKTtcclxuICAgICAgICBjb25zdCBpRW5kQW5nbGUgPSBkMy5pbnRlcnBvbGF0ZSh0aGlzLmN1ckRhdGFbaWR4XS5lbmRBbmdsZSwgdGhpcy5lbmREYXRhW2lkeF0uZW5kQW5nbGUpO1xyXG4gICAgICAgIGNvbnN0IGlDb2xvciA9IGQzLmludGVycG9sYXRlKHRoaXMuY3VyRGF0YVtpZHhdLmRhdGEuY29sb3IsIHRoaXMuZW5kRGF0YVtpZHhdLmRhdGEuY29sb3IpO1xyXG4gICAgICAgIC8vIHJldHVybiBmYWN0b3J5IGZ1bmN0aW9uIGZvciBhbmltYXRpb24gc3RlcHNcclxuICAgICAgICByZXR1cm4gKHQpID0+IHtcclxuICAgICAgICAgIC8vIGludGVycG9sYXRlIHZhbHVlcyBieSBnaXZlbiB0cmFuc2l0aW9uIHZhbHVlXHJcbiAgICAgICAgICB0aGlzLmN1ckRhdGFbaWR4XS52YWx1ZSA9IGlWYWx1ZSh0KTtcclxuICAgICAgICAgIHRoaXMuY3VyRGF0YVtpZHhdLnN0YXJ0QW5nbGUgPSBpU3RhcnRBbmdsZSh0KTtcclxuICAgICAgICAgIHRoaXMuY3VyRGF0YVtpZHhdLmVuZEFuZ2xlID0gaUVuZEFuZ2xlKHQpO1xyXG4gICAgICAgICAgdGhpcy5jdXJEYXRhW2lkeF0uZGF0YS5jb2xvciA9IGlDb2xvcih0KTtcclxuICAgICAgICAgIC8vIGdlbmVyYXRlIG5ldyBwYXRoXHJcbiAgICAgICAgICB0aGlzLmN1ckRhdGFbaWR4XS5kYXRhLnBhdGggPSB0aGlzLnBhdGhHZW5lcmF0b3IodGhpcy5jdXJEYXRhW2lkeF0pO1xyXG4gICAgICAgICAgLy8gcmV0dXJuIGVtcHR5IHN0cmluZy4gVGhpcyBpcyBvbmx5IG5lY2Vzc2FyeSBmb3IgdHlwZXNjcmlwdCBjb21waWxlci4gTm90aGluZyBzaG91bGQgYmUgY2hhbmdlZCBoZXJlLlxyXG4gICAgICAgICAgcmV0dXJuICcnO1xyXG4gICAgICAgIH07XHJcbiAgICAgIH0pXHJcbiAgICAgIC8vIHdoZW4gdHJhbnNpdGlvbiBpcyBjb21wbGV0ZVxyXG4gICAgICAub24oJ2VuZCcsIChhcmcwLCBpZHgsIG5vZGVMaXN0KSA9PiB7XHJcbiAgICAgICAgLy8gd2hlbiB0cmFuc2l0aW9uIGlzIGNvbXBsZXRlIGZvciB0aGUgbGFzdCBpdGVtXHJcbiAgICAgICAgaWYoaWR4PT09bm9kZUxpc3QubGVuZ3RoLTEpe1xyXG4gICAgICAgICAgLy8gcmVtb3ZlIGFzIGRlbGV0ZWQgbWFya2VkIGVudHJpZXNcclxuICAgICAgICAgIHRoaXMuY2xlYW5TdGF0ZUl0ZW1zKCk7XHJcbiAgICAgICAgICAvLyBEZWxldGUgaW50ZXJ1cHQgZGVmaW5pdGlvbiwgYmVjYXVzZSBldmVyeXRoaW5nIGhhcyBmaW5pc2hlZCBhbmQgbm90aGluZyBjYW4gYmUgaW50ZXJydXB0ZWQuXHJcbiAgICAgICAgICBkZWxldGUgdGhpcy5pbnRlcnJ1cHQ7XHJcbiAgICAgICAgfVxyXG4gICAgICB9KTtcclxuICB9O1xyXG5cclxuICAvKipcclxuICAgKiBNdXN0IGJlIGNhbGxlZCBhZnRlciB0cmFuc2l0aW9uIGVuZHMgdG8gcmVtb3ZlIGVudHJpZXMgaW4gY3VyRGF0YSBhbmQgZW5kRGF0YSB3aGljaCBhcmUgbWFya2VkXHJcbiAgICogYXMgZGVsZXRlZC5cclxuICAgKi9cclxuICBwcm90ZWN0ZWQgY2xlYW5TdGF0ZUl0ZW1zKCk6IHZvaWQge1xyXG4gICAgLy8gY2xlYW4gY3VycmVudCBzdGF0ZSBhcnJheVxyXG4gICAgZm9yKGxldCBpPXRoaXMuY3VyRGF0YS5sZW5ndGgtMTsgaT49MDsgLS1pKXtcclxuICAgICAgaWYodGhpcy5jdXJEYXRhW2ldLmRhdGEuZGVsZXRlZD09PXRydWUpe1xyXG4gICAgICAgIHRoaXMuY3VyRGF0YS5zcGxpY2UoaSwgMSk7XHJcbiAgICAgIH1cclxuICAgIH1cclxuICAgIC8vIGNsZWFuIGVuZCBzdGF0ZSBhcnJheVxyXG4gICAgZm9yKGxldCBpPXRoaXMuZW5kRGF0YS5sZW5ndGgtMTsgaT49MDsgLS1pKXtcclxuICAgICAgaWYodGhpcy5lbmREYXRhW2ldLmRhdGEuZGVsZXRlZD09PXRydWUpe1xyXG4gICAgICAgIHRoaXMuZW5kRGF0YS5zcGxpY2UoaSwxKTtcclxuICAgICAgfVxyXG4gICAgfVxyXG4gIH07XHJcblxyXG4gIC8qKlxyXG4gICAqIENoZWNrcyB3aGV0aGVyIGFsbCBpdGVtcyBoYXZlIGFzc2lnbmVkIGNvbG9yIHZhbHVlcyBhbmQgaWYgbmVjZXNzYXJ5IGNvbXBsZXRlcyBjb2xvcnMgaW4gZ2l2ZW4gZGF0YSBhcnJheS5cclxuICAgKi9cclxuICBwcm90ZWN0ZWQgaW5pdENvbG9ycygpOiB2b2lkIHtcclxuICAgIC8vIGxvb3AgYWxsIGVudHJpZXNcclxuICAgIHRoaXMuZGF0YS5mb3JFYWNoKCAoaXRlbSkgPT4ge1xyXG4gICAgICAvLyBpZiBubyBjb2xvciBpcyBhc3NpZ25lZFxyXG4gICAgICBpZighaXRlbS5jb2xvcil7XHJcbiAgICAgICAgLy8gZ2VuZXJhdGUgcmFuZG9tIGNvbG9yIGZvciBpdGVtXHJcbiAgICAgICAgaXRlbS5jb2xvciA9IHRoaXMuZ2VuZXJhdGVSYW5kb21Db2xvcihpdGVtLnZhbHVlKTtcclxuICAgICAgfVxyXG4gICAgfSk7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogUmV0dXJucyBtYXhpbWFsIGFuZ2xlIG9mIGN1cnJlbnQgc3RhdGUgaXRlbXMuXHJcbiAgICovXHJcbiAgcHJvdGVjdGVkIGdldE1heEFuZ2xlKCk6IG51bWJlciB7XHJcbiAgICBsZXQgbWF4QW5nbGUgPSAwO1xyXG4gICAgdGhpcy5jdXJEYXRhLmZvckVhY2goIChjdXJJdGVtKSA9PiB7IFxyXG4gICAgICBpZihjdXJJdGVtLmVuZEFuZ2xlID4gbWF4QW5nbGUpe1xyXG4gICAgICAgIG1heEFuZ2xlID0gY3VySXRlbS5lbmRBbmdsZTtcclxuICAgICAgfVxyXG4gICAgfSk7XHJcbiAgICByZXR1cm4gbWF4QW5nbGU7XHJcbiAgfTtcclxuXHJcbiAgLyoqXHJcbiAgICogQ2FsY3VsYXRlcyBhbmdsZXMgZm9yIGN1cnJlbnQgYW5kIGVuZCBzdGF0ZSBpdGVtcy5cclxuICAgKiBAcGFyYW0gbWF4QW5nbGUgbGFzdCBtYXhpbWFsIGFuZ2xlIGluIGN1cnJlbnQgc3RhdGUgdG8gYXZvaWQgXCJqdW1waW5nXCIgdHJhbnNpdGlvbnNcclxuICAgKi9cclxuICBwcm90ZWN0ZWQgY2FsY3VsYXRlQW5nbGVzKG1heEFuZ2xlOiBudW1iZXIpOiB2b2lkIHtcclxuICAgIC8vIGNhbGN1bGF0ZSBhbmdsZXMgZm9yIGN1cnJlbnQgc3RhdGUgaXRlbXNcclxuICAgIHtcclxuICAgICAgLy8gY2FsY3VsYXRlIHN1bSBvZiB2YWx1ZXNcclxuICAgICAgY29uc3QgdG90YWwgPSB0aGlzLmN1ckRhdGEucmVkdWNlKChwLCBjKSA9PiBwICsgYy52YWx1ZSwgMCk7XHJcbiAgICAgIC8vIGxvb3AgaXRlbXMgYW5kIGNhbGN1bGF0ZSBzdGFydCBhbmQgZW5kIGFuZ2xlcywgaW5pdGlhbGl6ZSByZW5kZXJpbmdcclxuICAgICAgbGV0IGxhc3RBbmdsZSA9IDA7XHJcbiAgICAgIHRoaXMuY3VyRGF0YS5mb3JFYWNoKCAoaXRlbSwgaWR4KSA9PiB7XHJcbiAgICAgICAgLy8gY2FsY3VsYXRlIGFuZ2xlcyBieSBsYXN0IHVzZWQgbWF4aW1hbCBhbmdsZS4gd2l0aG91dCBkYXRhICh0b3RhbD0wKSBzaW11bGF0ZSAwIHZhbHVlcywgc28gZHJhdyBpdGVtcyBpbiBjbG9ja3dpc2UgZGlyZWN0aW9uLlxyXG4gICAgICAgIGNvbnN0IG5leHRBbmdsZSA9IGxhc3RBbmdsZSArICgobWF4QW5nbGUpIC8gKCh0b3RhbD09PTApPzE6dG90YWwpKSAqIGl0ZW0udmFsdWU7XHJcbiAgICAgICAgaXRlbS5zdGFydEFuZ2xlID0gbGFzdEFuZ2xlO1xyXG4gICAgICAgIGl0ZW0uZW5kQW5nbGUgPSBuZXh0QW5nbGU7XHJcbiAgICAgICAgaXRlbS5pbmRleCA9IGlkeDtcclxuICAgICAgICBpdGVtLmRhdGEucGF0aCA9IHRoaXMucGF0aEdlbmVyYXRvcihpdGVtKTtcclxuICAgICAgICBsYXN0QW5nbGUgPSBuZXh0QW5nbGU7XHJcbiAgICAgIH0pO1xyXG4gICAgfVxyXG4gICAgLy8gY2FsY3VsYXRlIGFuZ2xlcyBmb3IgZW5kIHN0YXRlIGl0ZW1zXHJcbiAgICB7XHJcbiAgICAgIC8vIGNhbGN1bGF0ZSBzdW0gb2YgdmFsdWVzXHJcbiAgICAgIGNvbnN0IHRvdGFsID0gdGhpcy5lbmREYXRhLnJlZHVjZSgocCwgYykgPT4gcCArIGMudmFsdWUsIDApO1xyXG4gICAgICAvLyBsb29wIGl0ZW1zIGFuZCBjYWxjdWxhdGUgc3RhcnQgYW5kIGVuZCBhbmdsZXMsIGluaXRpYWxpemUgcmVuZGVyaW5nXHJcbiAgICAgIGxldCBsYXN0QW5nbGUgPSAwO1xyXG4gICAgICB0aGlzLmVuZERhdGEuZm9yRWFjaCggKGl0ZW0sIGlkeCkgPT4ge1xyXG4gICAgICAgIC8vIGNhbGN1bGF0ZSBhbmdsZXMgd2l0aCBjaXJjdW1mZXJlbmNlLiB3aXRob3V0IGRhdGEgKHRvdGFsPTApIHNpbXVsYXRlIDAgdmFsdWVzLCBzbyBkcmF3IGl0ZW1zIGluIGFudGktY2xvY2t3aXNlIGRpcmVjdGlvbi5cclxuICAgICAgICBjb25zdCBuZXh0QW5nbGUgPSBsYXN0QW5nbGUgKyAoKDIgKiBNYXRoLlBJKSAvICgodG90YWw9PT0wKT8xOnRvdGFsKSkgKiBpdGVtLnZhbHVlO1xyXG4gICAgICAgIGl0ZW0uc3RhcnRBbmdsZSA9IGxhc3RBbmdsZTtcclxuICAgICAgICBpdGVtLmVuZEFuZ2xlID0gbmV4dEFuZ2xlO1xyXG4gICAgICAgIGl0ZW0uaW5kZXggPSBpZHg7XHJcbiAgICAgICAgaXRlbS5kYXRhLnBhdGggPSB0aGlzLnBhdGhHZW5lcmF0b3IoaXRlbSk7XHJcbiAgICAgICAgbGFzdEFuZ2xlID0gbmV4dEFuZ2xlO1xyXG4gICAgICB9KTtcclxuICAgIH1cclxuICB9O1xyXG5cclxuICAvKiogcmVmZXJlbmNlIHRvIHRvb2x0aXAgZGl2IGVsZW1lbnQgKi9cclxuICBwcml2YXRlIHRvb2x0aXA6IEhUTUxEaXZFbGVtZW50O1xyXG5cclxuICAvKipcclxuICAgKiBmaXJlZCB3aGVuIG1vdXNlIGVudGVycyBhIHBpZSBjaGFydCBwYXRoIGVsZW1lbnQgYW5kIHNob3dzIHRvb2x0aXBcclxuICAgKiBAcGFyYW0gZXZlbnQgXHJcbiAgICovXHJcbiAgcHVibGlj