@odoo/o-spreadsheet 16.4.15 → 16.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +921 -188
- package/dist/o-spreadsheet.d.ts +19 -19
- package/dist/o-spreadsheet.esm.js +921 -188
- package/dist/o-spreadsheet.iife.js +921 -188
- package/dist/o-spreadsheet.iife.min.js +264 -264
- package/dist/o_spreadsheet.xml +3 -3
- package/package.json +4 -2
|
@@ -301,13 +301,6 @@ const NEWLINE = "\n";
|
|
|
301
301
|
const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
|
|
302
302
|
|
|
303
303
|
//------------------------------------------------------------------------------
|
|
304
|
-
/**
|
|
305
|
-
* Stringify an object, like JSON.stringify, except that the first level of keys
|
|
306
|
-
* is ordered.
|
|
307
|
-
*/
|
|
308
|
-
function stringify(obj) {
|
|
309
|
-
return JSON.stringify(obj, Object.keys(obj).sort());
|
|
310
|
-
}
|
|
311
304
|
/**
|
|
312
305
|
* Remove quotes from a quoted string
|
|
313
306
|
* ```js
|
|
@@ -527,7 +520,7 @@ function isObjectEmptyRecursive(argument) {
|
|
|
527
520
|
*/
|
|
528
521
|
function getItemId(item, itemsDic) {
|
|
529
522
|
for (let [key, value] of Object.entries(itemsDic)) {
|
|
530
|
-
if (
|
|
523
|
+
if (deepEquals(value, item)) {
|
|
531
524
|
return parseInt(key, 10);
|
|
532
525
|
}
|
|
533
526
|
}
|
|
@@ -2345,8 +2338,10 @@ function convertInternalFormatToFormat(internalFormat) {
|
|
|
2345
2338
|
const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
|
|
2346
2339
|
// Same as above, but matches the exact string (nothing before or after)
|
|
2347
2340
|
const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
|
|
2348
|
-
/** Reference of a column header (eg. A, AB) */
|
|
2349
|
-
const colHeader = new RegExp(
|
|
2341
|
+
/** Reference of a column header (eg. A, AB, $A) */
|
|
2342
|
+
const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
|
|
2343
|
+
/** Reference of a row header (eg. 1, $1) */
|
|
2344
|
+
const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
|
|
2350
2345
|
/** Reference of a column (eg. A, $CA, Sheet1!B) */
|
|
2351
2346
|
const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
|
|
2352
2347
|
/** Reference of a row (eg. 1, 59, Sheet1!9) */
|
|
@@ -2376,6 +2371,9 @@ function isRowReference(xc) {
|
|
|
2376
2371
|
function isColHeader(str) {
|
|
2377
2372
|
return colHeader.test(str);
|
|
2378
2373
|
}
|
|
2374
|
+
function isRowHeader(str) {
|
|
2375
|
+
return rowHeader.test(str);
|
|
2376
|
+
}
|
|
2379
2377
|
/**
|
|
2380
2378
|
* Return true if the given xc is the reference of a single cell,
|
|
2381
2379
|
* without any specified sheet (e.g. A1)
|
|
@@ -3048,18 +3046,10 @@ class RangeImpl {
|
|
|
3048
3046
|
if (isFullCol) {
|
|
3049
3047
|
parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
|
|
3050
3048
|
parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
|
|
3051
|
-
if (zone.left === zone.right) {
|
|
3052
|
-
parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
|
|
3053
|
-
parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
|
|
3054
|
-
}
|
|
3055
3049
|
}
|
|
3056
3050
|
if (isFullRow) {
|
|
3057
3051
|
parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
|
|
3058
3052
|
parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
|
|
3059
|
-
if (zone.top === zone.bottom) {
|
|
3060
|
-
parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
|
|
3061
|
-
parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
|
|
3062
|
-
}
|
|
3063
3053
|
}
|
|
3064
3054
|
return parts;
|
|
3065
3055
|
}
|
|
@@ -5611,6 +5601,7 @@ const FilterMenuPopoverBuilder = {
|
|
|
5611
5601
|
},
|
|
5612
5602
|
};
|
|
5613
5603
|
|
|
5604
|
+
const macRegex = /Mac/i;
|
|
5614
5605
|
/**
|
|
5615
5606
|
* Return true if the event was triggered from
|
|
5616
5607
|
* a child element.
|
|
@@ -5648,6 +5639,17 @@ function* iterateChildren(el) {
|
|
|
5648
5639
|
}
|
|
5649
5640
|
function getOpenedMenus() {
|
|
5650
5641
|
return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
|
|
5642
|
+
}
|
|
5643
|
+
function isMacOS() {
|
|
5644
|
+
return Boolean(macRegex.test(navigator.userAgent));
|
|
5645
|
+
}
|
|
5646
|
+
/**
|
|
5647
|
+
* @param {KeyboardEvent | MouseEvent} ev
|
|
5648
|
+
* @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
|
|
5649
|
+
* On Mac, this is the "meta" or "command" key.
|
|
5650
|
+
*/
|
|
5651
|
+
function isCtrlKey(ev) {
|
|
5652
|
+
return isMacOS() ? ev.metaKey : ev.ctrlKey;
|
|
5651
5653
|
}
|
|
5652
5654
|
|
|
5653
5655
|
/**
|
|
@@ -14704,7 +14706,7 @@ const machine = {
|
|
|
14704
14706
|
SPACE: goTo(State.RightRef),
|
|
14705
14707
|
NUMBER: goTo(State.Found),
|
|
14706
14708
|
REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
|
|
14707
|
-
SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
|
|
14709
|
+
SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
|
|
14708
14710
|
},
|
|
14709
14711
|
[State.RightColumnRef]: {
|
|
14710
14712
|
SPACE: goTo(State.RightColumnRef),
|
|
@@ -14715,6 +14717,7 @@ const machine = {
|
|
|
14715
14717
|
SPACE: goTo(State.RightRowRef),
|
|
14716
14718
|
NUMBER: goTo(State.Found),
|
|
14717
14719
|
REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
|
|
14720
|
+
SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
|
|
14718
14721
|
},
|
|
14719
14722
|
[State.Found]: {},
|
|
14720
14723
|
};
|
|
@@ -16872,7 +16875,7 @@ function createGaugeChartRuntime(chart, getters) {
|
|
|
16872
16875
|
});
|
|
16873
16876
|
return {
|
|
16874
16877
|
chartJsConfig: config,
|
|
16875
|
-
background: getters.
|
|
16878
|
+
background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
|
|
16876
16879
|
};
|
|
16877
16880
|
}
|
|
16878
16881
|
|
|
@@ -17596,7 +17599,7 @@ function createScorecardChartRuntime(chart, getters) {
|
|
|
17596
17599
|
};
|
|
17597
17600
|
baselineCell = getters.getEvaluatedCell(baselinePosition);
|
|
17598
17601
|
}
|
|
17599
|
-
const background = getters.
|
|
17602
|
+
const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
|
|
17600
17603
|
const locale = getters.getLocale();
|
|
17601
17604
|
return {
|
|
17602
17605
|
title: _t(chart.title),
|
|
@@ -17605,7 +17608,7 @@ function createScorecardChartRuntime(chart, getters) {
|
|
|
17605
17608
|
baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
|
|
17606
17609
|
baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
|
|
17607
17610
|
baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
|
|
17608
|
-
fontColor
|
|
17611
|
+
fontColor,
|
|
17609
17612
|
background,
|
|
17610
17613
|
baselineStyle: chart.baselineMode !== "percentage" && baseline
|
|
17611
17614
|
? getters.getCellStyle({
|
|
@@ -20500,10 +20503,10 @@ const arrowMap = {
|
|
|
20500
20503
|
function updateSelectionWithArrowKeys(ev, selection) {
|
|
20501
20504
|
const direction = arrowMap[ev.key];
|
|
20502
20505
|
if (ev.shiftKey) {
|
|
20503
|
-
selection.resizeAnchorZone(direction, ev
|
|
20506
|
+
selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
|
|
20504
20507
|
}
|
|
20505
20508
|
else {
|
|
20506
|
-
selection.moveAnchorCell(direction, ev
|
|
20509
|
+
selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
|
|
20507
20510
|
}
|
|
20508
20511
|
}
|
|
20509
20512
|
|
|
@@ -25548,7 +25551,10 @@ class GridOverlay extends owl.Component {
|
|
|
25548
25551
|
return;
|
|
25549
25552
|
}
|
|
25550
25553
|
const [col, row] = this.getCartesianCoordinates(ev);
|
|
25551
|
-
this.props.onCellClicked(col, row, {
|
|
25554
|
+
this.props.onCellClicked(col, row, {
|
|
25555
|
+
expandZone: ev.shiftKey,
|
|
25556
|
+
addZone: isCtrlKey(ev),
|
|
25557
|
+
});
|
|
25552
25558
|
}
|
|
25553
25559
|
onDoubleClick(ev) {
|
|
25554
25560
|
const [col, row] = this.getCartesianCoordinates(ev);
|
|
@@ -25621,6 +25627,7 @@ class AbstractResizer extends owl.Component {
|
|
|
25621
25627
|
draggerShadowThickness: 0,
|
|
25622
25628
|
delta: 0,
|
|
25623
25629
|
base: 0,
|
|
25630
|
+
position: "before",
|
|
25624
25631
|
});
|
|
25625
25632
|
_computeHandleDisplay(ev) {
|
|
25626
25633
|
const position = this._getEvOffset(ev);
|
|
@@ -25745,11 +25752,13 @@ class AbstractResizer extends owl.Component {
|
|
|
25745
25752
|
this.state.draggerLinePosition = dimensions.start;
|
|
25746
25753
|
this.state.draggerShadowPosition = dimensions.start;
|
|
25747
25754
|
this.state.base = elementIndex;
|
|
25755
|
+
this.state.position = "before";
|
|
25748
25756
|
}
|
|
25749
25757
|
else if (this._getSelectedZoneEnd() < elementIndex) {
|
|
25750
25758
|
this.state.draggerLinePosition = dimensions.end;
|
|
25751
25759
|
this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
|
|
25752
|
-
this.state.base = elementIndex
|
|
25760
|
+
this.state.base = elementIndex;
|
|
25761
|
+
this.state.position = "after";
|
|
25753
25762
|
}
|
|
25754
25763
|
else {
|
|
25755
25764
|
this.state.draggerLinePosition = startDimensions.start;
|
|
@@ -25773,7 +25782,7 @@ class AbstractResizer extends owl.Component {
|
|
|
25773
25782
|
this._increaseSelection(index);
|
|
25774
25783
|
}
|
|
25775
25784
|
else {
|
|
25776
|
-
this._selectElement(index, ev
|
|
25785
|
+
this._selectElement(index, isCtrlKey(ev));
|
|
25777
25786
|
}
|
|
25778
25787
|
this.lastSelectedElementIndex = index;
|
|
25779
25788
|
const mouseMoveSelect = (col, row) => {
|
|
@@ -25786,7 +25795,7 @@ class AbstractResizer extends owl.Component {
|
|
|
25786
25795
|
const mouseUpSelect = () => {
|
|
25787
25796
|
this.state.isSelecting = false;
|
|
25788
25797
|
this.lastSelectedElementIndex = null;
|
|
25789
|
-
this.env.model.dispatch(ev
|
|
25798
|
+
this.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
|
|
25790
25799
|
this._computeGrabDisplay(ev);
|
|
25791
25800
|
};
|
|
25792
25801
|
dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
|
|
@@ -25932,13 +25941,14 @@ class ColResizer extends AbstractResizer {
|
|
|
25932
25941
|
dimension: "COL",
|
|
25933
25942
|
base: this.state.base,
|
|
25934
25943
|
elements,
|
|
25944
|
+
position: this.state.position,
|
|
25935
25945
|
});
|
|
25936
25946
|
if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
|
|
25937
25947
|
this.env.raiseError(MergeErrorMessage);
|
|
25938
25948
|
}
|
|
25939
25949
|
}
|
|
25940
|
-
_selectElement(index,
|
|
25941
|
-
this.env.model.selection.selectColumn(index,
|
|
25950
|
+
_selectElement(index, addDistinctHeader) {
|
|
25951
|
+
this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
|
|
25942
25952
|
}
|
|
25943
25953
|
_increaseSelection(index) {
|
|
25944
25954
|
this.env.model.selection.selectColumn(index, "updateAnchor");
|
|
@@ -26105,13 +26115,14 @@ class RowResizer extends AbstractResizer {
|
|
|
26105
26115
|
dimension: "ROW",
|
|
26106
26116
|
base: this.state.base,
|
|
26107
26117
|
elements,
|
|
26118
|
+
position: this.state.position,
|
|
26108
26119
|
});
|
|
26109
26120
|
if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
|
|
26110
26121
|
this.env.raiseError(MergeErrorMessage);
|
|
26111
26122
|
}
|
|
26112
26123
|
}
|
|
26113
|
-
_selectElement(index,
|
|
26114
|
-
this.env.model.selection.selectRow(index,
|
|
26124
|
+
_selectElement(index, addDistinctHeader) {
|
|
26125
|
+
this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
|
|
26115
26126
|
}
|
|
26116
26127
|
_increaseSelection(index) {
|
|
26117
26128
|
this.env.model.selection.selectRow(index, "updateAnchor");
|
|
@@ -26212,8 +26223,8 @@ function useWheelHandler(handler) {
|
|
|
26212
26223
|
return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
|
|
26213
26224
|
}
|
|
26214
26225
|
const onMouseWheel = (ev) => {
|
|
26215
|
-
const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode);
|
|
26216
|
-
const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode);
|
|
26226
|
+
const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
|
|
26227
|
+
const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
|
|
26217
26228
|
handler(deltaX, deltaY);
|
|
26218
26229
|
};
|
|
26219
26230
|
return onMouseWheel;
|
|
@@ -26917,8 +26928,8 @@ class Grid extends owl.Component {
|
|
|
26917
26928
|
// ---------------------------------------------------------------------------
|
|
26918
26929
|
// Zone selection with mouse
|
|
26919
26930
|
// ---------------------------------------------------------------------------
|
|
26920
|
-
onCellClicked(col, row, {
|
|
26921
|
-
if (
|
|
26931
|
+
onCellClicked(col, row, { addZone, expandZone }) {
|
|
26932
|
+
if (addZone) {
|
|
26922
26933
|
this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
|
|
26923
26934
|
}
|
|
26924
26935
|
if (this.env.model.getters.hasOpenedPopover()) {
|
|
@@ -26927,10 +26938,10 @@ class Grid extends owl.Component {
|
|
|
26927
26938
|
if (this.env.model.getters.getEditionMode() === "editing") {
|
|
26928
26939
|
this.env.model.dispatch("STOP_EDITION");
|
|
26929
26940
|
}
|
|
26930
|
-
if (
|
|
26941
|
+
if (expandZone) {
|
|
26931
26942
|
this.env.model.selection.setAnchorCorner(col, row);
|
|
26932
26943
|
}
|
|
26933
|
-
else if (
|
|
26944
|
+
else if (addZone) {
|
|
26934
26945
|
this.env.model.selection.addCellToSelection(col, row);
|
|
26935
26946
|
}
|
|
26936
26947
|
else {
|
|
@@ -26992,9 +27003,7 @@ class Grid extends owl.Component {
|
|
|
26992
27003
|
return;
|
|
26993
27004
|
}
|
|
26994
27005
|
let keyDownString = "";
|
|
26995
|
-
if (ev
|
|
26996
|
-
keyDownString += "CTRL+";
|
|
26997
|
-
if (ev.metaKey)
|
|
27006
|
+
if (isCtrlKey(ev))
|
|
26998
27007
|
keyDownString += "CTRL+";
|
|
26999
27008
|
if (ev.altKey)
|
|
27000
27009
|
keyDownString += "ALT+";
|
|
@@ -31642,7 +31651,7 @@ class BordersPlugin extends CorePlugin {
|
|
|
31642
31651
|
*/
|
|
31643
31652
|
function getBorderId(border) {
|
|
31644
31653
|
for (let [key, value] of Object.entries(borders)) {
|
|
31645
|
-
if (
|
|
31654
|
+
if (deepEquals(value, border)) {
|
|
31646
31655
|
return parseInt(key, 10);
|
|
31647
31656
|
}
|
|
31648
31657
|
}
|
|
@@ -34551,7 +34560,7 @@ class SheetPlugin extends CorePlugin {
|
|
|
34551
34560
|
const elements = cmd.dimension === "COL"
|
|
34552
34561
|
? this.getNumberCols(cmd.sheetId)
|
|
34553
34562
|
: this.getNumberRows(cmd.sheetId);
|
|
34554
|
-
if (cmd.base < 0 || cmd.base
|
|
34563
|
+
if (cmd.base < 0 || cmd.base >= elements) {
|
|
34555
34564
|
return 88 /* CommandResult.InvalidHeaderIndex */;
|
|
34556
34565
|
}
|
|
34557
34566
|
else if (cmd.quantity <= 0) {
|
|
@@ -35492,10 +35501,13 @@ class CompilationParametersBuilder {
|
|
|
35492
35501
|
if (evaluatedCell === undefined) {
|
|
35493
35502
|
return { value: null, format: this.getters.getCell(position)?.format };
|
|
35494
35503
|
}
|
|
35504
|
+
if (evaluatedCell.type === CellValueType.error) {
|
|
35505
|
+
throw evaluatedCell.error;
|
|
35506
|
+
}
|
|
35495
35507
|
return evaluatedCell;
|
|
35496
35508
|
}
|
|
35497
35509
|
getEvaluatedCellIfNotEmpty(position) {
|
|
35498
|
-
const evaluatedCell = this.
|
|
35510
|
+
const evaluatedCell = this.computeCell(position);
|
|
35499
35511
|
if (evaluatedCell.type === CellValueType.empty) {
|
|
35500
35512
|
const cell = this.getters.getCell(position);
|
|
35501
35513
|
if (!cell || (!cell.isFormula && cell.content === "")) {
|
|
@@ -35504,13 +35516,6 @@ class CompilationParametersBuilder {
|
|
|
35504
35516
|
}
|
|
35505
35517
|
return evaluatedCell;
|
|
35506
35518
|
}
|
|
35507
|
-
getEvaluatedCell(position) {
|
|
35508
|
-
const evaluatedCell = this.computeCell(position);
|
|
35509
|
-
if (evaluatedCell.type === CellValueType.error) {
|
|
35510
|
-
throw evaluatedCell.error;
|
|
35511
|
-
}
|
|
35512
|
-
return evaluatedCell;
|
|
35513
|
-
}
|
|
35514
35519
|
/**
|
|
35515
35520
|
* Return the values of the cell(s) used in reference, but always in the format of a range even
|
|
35516
35521
|
* if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
|
|
@@ -35533,7 +35538,11 @@ class CompilationParametersBuilder {
|
|
|
35533
35538
|
const { top, left, bottom, right } = zone;
|
|
35534
35539
|
const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
|
|
35535
35540
|
if (cacheKey in this.rangeCache) {
|
|
35536
|
-
|
|
35541
|
+
const result = this.rangeCache[cacheKey];
|
|
35542
|
+
if (result instanceof EvaluationError) {
|
|
35543
|
+
throw result;
|
|
35544
|
+
}
|
|
35545
|
+
return result;
|
|
35537
35546
|
}
|
|
35538
35547
|
const height = _zone.bottom - _zone.top + 1;
|
|
35539
35548
|
const width = _zone.right - _zone.left + 1;
|
|
@@ -35546,6 +35555,10 @@ class CompilationParametersBuilder {
|
|
|
35546
35555
|
format[colIndex] = new Array(height);
|
|
35547
35556
|
for (let row = _zone.top; row <= _zone.bottom; row++) {
|
|
35548
35557
|
const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId, col, row });
|
|
35558
|
+
if (evaluatedCell?.type === CellValueType.error) {
|
|
35559
|
+
this.rangeCache[cacheKey] = evaluatedCell.error;
|
|
35560
|
+
throw evaluatedCell.error;
|
|
35561
|
+
}
|
|
35549
35562
|
const rowIndex = row - _zone.top;
|
|
35550
35563
|
value[colIndex][rowIndex] = evaluatedCell?.value;
|
|
35551
35564
|
if (evaluatedCell?.format !== undefined) {
|
|
@@ -35559,56 +35572,758 @@ class CompilationParametersBuilder {
|
|
|
35559
35572
|
}
|
|
35560
35573
|
}
|
|
35561
35574
|
|
|
35575
|
+
function quickselect(arr, k, left, right, compare) {
|
|
35576
|
+
quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
|
|
35577
|
+
}
|
|
35578
|
+
|
|
35579
|
+
function quickselectStep(arr, k, left, right, compare) {
|
|
35580
|
+
|
|
35581
|
+
while (right > left) {
|
|
35582
|
+
if (right - left > 600) {
|
|
35583
|
+
var n = right - left + 1;
|
|
35584
|
+
var m = k - left + 1;
|
|
35585
|
+
var z = Math.log(n);
|
|
35586
|
+
var s = 0.5 * Math.exp(2 * z / 3);
|
|
35587
|
+
var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
|
|
35588
|
+
var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
|
|
35589
|
+
var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
|
|
35590
|
+
quickselectStep(arr, k, newLeft, newRight, compare);
|
|
35591
|
+
}
|
|
35592
|
+
|
|
35593
|
+
var t = arr[k];
|
|
35594
|
+
var i = left;
|
|
35595
|
+
var j = right;
|
|
35596
|
+
|
|
35597
|
+
swap(arr, left, k);
|
|
35598
|
+
if (compare(arr[right], t) > 0) swap(arr, left, right);
|
|
35599
|
+
|
|
35600
|
+
while (i < j) {
|
|
35601
|
+
swap(arr, i, j);
|
|
35602
|
+
i++;
|
|
35603
|
+
j--;
|
|
35604
|
+
while (compare(arr[i], t) < 0) i++;
|
|
35605
|
+
while (compare(arr[j], t) > 0) j--;
|
|
35606
|
+
}
|
|
35607
|
+
|
|
35608
|
+
if (compare(arr[left], t) === 0) swap(arr, left, j);
|
|
35609
|
+
else {
|
|
35610
|
+
j++;
|
|
35611
|
+
swap(arr, j, right);
|
|
35612
|
+
}
|
|
35613
|
+
|
|
35614
|
+
if (j <= k) left = j + 1;
|
|
35615
|
+
if (k <= j) right = j - 1;
|
|
35616
|
+
}
|
|
35617
|
+
}
|
|
35618
|
+
|
|
35619
|
+
function swap(arr, i, j) {
|
|
35620
|
+
var tmp = arr[i];
|
|
35621
|
+
arr[i] = arr[j];
|
|
35622
|
+
arr[j] = tmp;
|
|
35623
|
+
}
|
|
35624
|
+
|
|
35625
|
+
function defaultCompare(a, b) {
|
|
35626
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
35627
|
+
}
|
|
35628
|
+
|
|
35629
|
+
class RBush {
|
|
35630
|
+
constructor(maxEntries = 9) {
|
|
35631
|
+
// max entries in a node is 9 by default; min node fill is 40% for best performance
|
|
35632
|
+
this._maxEntries = Math.max(4, maxEntries);
|
|
35633
|
+
this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
|
|
35634
|
+
this.clear();
|
|
35635
|
+
}
|
|
35636
|
+
|
|
35637
|
+
all() {
|
|
35638
|
+
return this._all(this.data, []);
|
|
35639
|
+
}
|
|
35640
|
+
|
|
35641
|
+
search(bbox) {
|
|
35642
|
+
let node = this.data;
|
|
35643
|
+
const result = [];
|
|
35644
|
+
|
|
35645
|
+
if (!intersects(bbox, node)) return result;
|
|
35646
|
+
|
|
35647
|
+
const toBBox = this.toBBox;
|
|
35648
|
+
const nodesToSearch = [];
|
|
35649
|
+
|
|
35650
|
+
while (node) {
|
|
35651
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
35652
|
+
const child = node.children[i];
|
|
35653
|
+
const childBBox = node.leaf ? toBBox(child) : child;
|
|
35654
|
+
|
|
35655
|
+
if (intersects(bbox, childBBox)) {
|
|
35656
|
+
if (node.leaf) result.push(child);
|
|
35657
|
+
else if (contains(bbox, childBBox)) this._all(child, result);
|
|
35658
|
+
else nodesToSearch.push(child);
|
|
35659
|
+
}
|
|
35660
|
+
}
|
|
35661
|
+
node = nodesToSearch.pop();
|
|
35662
|
+
}
|
|
35663
|
+
|
|
35664
|
+
return result;
|
|
35665
|
+
}
|
|
35666
|
+
|
|
35667
|
+
collides(bbox) {
|
|
35668
|
+
let node = this.data;
|
|
35669
|
+
|
|
35670
|
+
if (!intersects(bbox, node)) return false;
|
|
35671
|
+
|
|
35672
|
+
const nodesToSearch = [];
|
|
35673
|
+
while (node) {
|
|
35674
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
35675
|
+
const child = node.children[i];
|
|
35676
|
+
const childBBox = node.leaf ? this.toBBox(child) : child;
|
|
35677
|
+
|
|
35678
|
+
if (intersects(bbox, childBBox)) {
|
|
35679
|
+
if (node.leaf || contains(bbox, childBBox)) return true;
|
|
35680
|
+
nodesToSearch.push(child);
|
|
35681
|
+
}
|
|
35682
|
+
}
|
|
35683
|
+
node = nodesToSearch.pop();
|
|
35684
|
+
}
|
|
35685
|
+
|
|
35686
|
+
return false;
|
|
35687
|
+
}
|
|
35688
|
+
|
|
35689
|
+
load(data) {
|
|
35690
|
+
if (!(data && data.length)) return this;
|
|
35691
|
+
|
|
35692
|
+
if (data.length < this._minEntries) {
|
|
35693
|
+
for (let i = 0; i < data.length; i++) {
|
|
35694
|
+
this.insert(data[i]);
|
|
35695
|
+
}
|
|
35696
|
+
return this;
|
|
35697
|
+
}
|
|
35698
|
+
|
|
35699
|
+
// recursively build the tree with the given data from scratch using OMT algorithm
|
|
35700
|
+
let node = this._build(data.slice(), 0, data.length - 1, 0);
|
|
35701
|
+
|
|
35702
|
+
if (!this.data.children.length) {
|
|
35703
|
+
// save as is if tree is empty
|
|
35704
|
+
this.data = node;
|
|
35705
|
+
|
|
35706
|
+
} else if (this.data.height === node.height) {
|
|
35707
|
+
// split root if trees have the same height
|
|
35708
|
+
this._splitRoot(this.data, node);
|
|
35709
|
+
|
|
35710
|
+
} else {
|
|
35711
|
+
if (this.data.height < node.height) {
|
|
35712
|
+
// swap trees if inserted one is bigger
|
|
35713
|
+
const tmpNode = this.data;
|
|
35714
|
+
this.data = node;
|
|
35715
|
+
node = tmpNode;
|
|
35716
|
+
}
|
|
35717
|
+
|
|
35718
|
+
// insert the small tree into the large tree at appropriate level
|
|
35719
|
+
this._insert(node, this.data.height - node.height - 1, true);
|
|
35720
|
+
}
|
|
35721
|
+
|
|
35722
|
+
return this;
|
|
35723
|
+
}
|
|
35724
|
+
|
|
35725
|
+
insert(item) {
|
|
35726
|
+
if (item) this._insert(item, this.data.height - 1);
|
|
35727
|
+
return this;
|
|
35728
|
+
}
|
|
35729
|
+
|
|
35730
|
+
clear() {
|
|
35731
|
+
this.data = createNode([]);
|
|
35732
|
+
return this;
|
|
35733
|
+
}
|
|
35734
|
+
|
|
35735
|
+
remove(item, equalsFn) {
|
|
35736
|
+
if (!item) return this;
|
|
35737
|
+
|
|
35738
|
+
let node = this.data;
|
|
35739
|
+
const bbox = this.toBBox(item);
|
|
35740
|
+
const path = [];
|
|
35741
|
+
const indexes = [];
|
|
35742
|
+
let i, parent, goingUp;
|
|
35743
|
+
|
|
35744
|
+
// depth-first iterative tree traversal
|
|
35745
|
+
while (node || path.length) {
|
|
35746
|
+
|
|
35747
|
+
if (!node) { // go up
|
|
35748
|
+
node = path.pop();
|
|
35749
|
+
parent = path[path.length - 1];
|
|
35750
|
+
i = indexes.pop();
|
|
35751
|
+
goingUp = true;
|
|
35752
|
+
}
|
|
35753
|
+
|
|
35754
|
+
if (node.leaf) { // check current node
|
|
35755
|
+
const index = findItem(item, node.children, equalsFn);
|
|
35756
|
+
|
|
35757
|
+
if (index !== -1) {
|
|
35758
|
+
// item found, remove the item and condense tree upwards
|
|
35759
|
+
node.children.splice(index, 1);
|
|
35760
|
+
path.push(node);
|
|
35761
|
+
this._condense(path);
|
|
35762
|
+
return this;
|
|
35763
|
+
}
|
|
35764
|
+
}
|
|
35765
|
+
|
|
35766
|
+
if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
|
|
35767
|
+
path.push(node);
|
|
35768
|
+
indexes.push(i);
|
|
35769
|
+
i = 0;
|
|
35770
|
+
parent = node;
|
|
35771
|
+
node = node.children[0];
|
|
35772
|
+
|
|
35773
|
+
} else if (parent) { // go right
|
|
35774
|
+
i++;
|
|
35775
|
+
node = parent.children[i];
|
|
35776
|
+
goingUp = false;
|
|
35777
|
+
|
|
35778
|
+
} else node = null; // nothing found
|
|
35779
|
+
}
|
|
35780
|
+
|
|
35781
|
+
return this;
|
|
35782
|
+
}
|
|
35783
|
+
|
|
35784
|
+
toBBox(item) { return item; }
|
|
35785
|
+
|
|
35786
|
+
compareMinX(a, b) { return a.minX - b.minX; }
|
|
35787
|
+
compareMinY(a, b) { return a.minY - b.minY; }
|
|
35788
|
+
|
|
35789
|
+
toJSON() { return this.data; }
|
|
35790
|
+
|
|
35791
|
+
fromJSON(data) {
|
|
35792
|
+
this.data = data;
|
|
35793
|
+
return this;
|
|
35794
|
+
}
|
|
35795
|
+
|
|
35796
|
+
_all(node, result) {
|
|
35797
|
+
const nodesToSearch = [];
|
|
35798
|
+
while (node) {
|
|
35799
|
+
if (node.leaf) result.push(...node.children);
|
|
35800
|
+
else nodesToSearch.push(...node.children);
|
|
35801
|
+
|
|
35802
|
+
node = nodesToSearch.pop();
|
|
35803
|
+
}
|
|
35804
|
+
return result;
|
|
35805
|
+
}
|
|
35806
|
+
|
|
35807
|
+
_build(items, left, right, height) {
|
|
35808
|
+
|
|
35809
|
+
const N = right - left + 1;
|
|
35810
|
+
let M = this._maxEntries;
|
|
35811
|
+
let node;
|
|
35812
|
+
|
|
35813
|
+
if (N <= M) {
|
|
35814
|
+
// reached leaf level; return leaf
|
|
35815
|
+
node = createNode(items.slice(left, right + 1));
|
|
35816
|
+
calcBBox(node, this.toBBox);
|
|
35817
|
+
return node;
|
|
35818
|
+
}
|
|
35819
|
+
|
|
35820
|
+
if (!height) {
|
|
35821
|
+
// target height of the bulk-loaded tree
|
|
35822
|
+
height = Math.ceil(Math.log(N) / Math.log(M));
|
|
35823
|
+
|
|
35824
|
+
// target number of root entries to maximize storage utilization
|
|
35825
|
+
M = Math.ceil(N / Math.pow(M, height - 1));
|
|
35826
|
+
}
|
|
35827
|
+
|
|
35828
|
+
node = createNode([]);
|
|
35829
|
+
node.leaf = false;
|
|
35830
|
+
node.height = height;
|
|
35831
|
+
|
|
35832
|
+
// split the items into M mostly square tiles
|
|
35833
|
+
|
|
35834
|
+
const N2 = Math.ceil(N / M);
|
|
35835
|
+
const N1 = N2 * Math.ceil(Math.sqrt(M));
|
|
35836
|
+
|
|
35837
|
+
multiSelect(items, left, right, N1, this.compareMinX);
|
|
35838
|
+
|
|
35839
|
+
for (let i = left; i <= right; i += N1) {
|
|
35840
|
+
|
|
35841
|
+
const right2 = Math.min(i + N1 - 1, right);
|
|
35842
|
+
|
|
35843
|
+
multiSelect(items, i, right2, N2, this.compareMinY);
|
|
35844
|
+
|
|
35845
|
+
for (let j = i; j <= right2; j += N2) {
|
|
35846
|
+
|
|
35847
|
+
const right3 = Math.min(j + N2 - 1, right2);
|
|
35848
|
+
|
|
35849
|
+
// pack each entry recursively
|
|
35850
|
+
node.children.push(this._build(items, j, right3, height - 1));
|
|
35851
|
+
}
|
|
35852
|
+
}
|
|
35853
|
+
|
|
35854
|
+
calcBBox(node, this.toBBox);
|
|
35855
|
+
|
|
35856
|
+
return node;
|
|
35857
|
+
}
|
|
35858
|
+
|
|
35859
|
+
_chooseSubtree(bbox, node, level, path) {
|
|
35860
|
+
while (true) {
|
|
35861
|
+
path.push(node);
|
|
35862
|
+
|
|
35863
|
+
if (node.leaf || path.length - 1 === level) break;
|
|
35864
|
+
|
|
35865
|
+
let minArea = Infinity;
|
|
35866
|
+
let minEnlargement = Infinity;
|
|
35867
|
+
let targetNode;
|
|
35868
|
+
|
|
35869
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
35870
|
+
const child = node.children[i];
|
|
35871
|
+
const area = bboxArea(child);
|
|
35872
|
+
const enlargement = enlargedArea(bbox, child) - area;
|
|
35873
|
+
|
|
35874
|
+
// choose entry with the least area enlargement
|
|
35875
|
+
if (enlargement < minEnlargement) {
|
|
35876
|
+
minEnlargement = enlargement;
|
|
35877
|
+
minArea = area < minArea ? area : minArea;
|
|
35878
|
+
targetNode = child;
|
|
35879
|
+
|
|
35880
|
+
} else if (enlargement === minEnlargement) {
|
|
35881
|
+
// otherwise choose one with the smallest area
|
|
35882
|
+
if (area < minArea) {
|
|
35883
|
+
minArea = area;
|
|
35884
|
+
targetNode = child;
|
|
35885
|
+
}
|
|
35886
|
+
}
|
|
35887
|
+
}
|
|
35888
|
+
|
|
35889
|
+
node = targetNode || node.children[0];
|
|
35890
|
+
}
|
|
35891
|
+
|
|
35892
|
+
return node;
|
|
35893
|
+
}
|
|
35894
|
+
|
|
35895
|
+
_insert(item, level, isNode) {
|
|
35896
|
+
const bbox = isNode ? item : this.toBBox(item);
|
|
35897
|
+
const insertPath = [];
|
|
35898
|
+
|
|
35899
|
+
// find the best node for accommodating the item, saving all nodes along the path too
|
|
35900
|
+
const node = this._chooseSubtree(bbox, this.data, level, insertPath);
|
|
35901
|
+
|
|
35902
|
+
// put the item into the node
|
|
35903
|
+
node.children.push(item);
|
|
35904
|
+
extend(node, bbox);
|
|
35905
|
+
|
|
35906
|
+
// split on node overflow; propagate upwards if necessary
|
|
35907
|
+
while (level >= 0) {
|
|
35908
|
+
if (insertPath[level].children.length > this._maxEntries) {
|
|
35909
|
+
this._split(insertPath, level);
|
|
35910
|
+
level--;
|
|
35911
|
+
} else break;
|
|
35912
|
+
}
|
|
35913
|
+
|
|
35914
|
+
// adjust bboxes along the insertion path
|
|
35915
|
+
this._adjustParentBBoxes(bbox, insertPath, level);
|
|
35916
|
+
}
|
|
35917
|
+
|
|
35918
|
+
// split overflowed node into two
|
|
35919
|
+
_split(insertPath, level) {
|
|
35920
|
+
const node = insertPath[level];
|
|
35921
|
+
const M = node.children.length;
|
|
35922
|
+
const m = this._minEntries;
|
|
35923
|
+
|
|
35924
|
+
this._chooseSplitAxis(node, m, M);
|
|
35925
|
+
|
|
35926
|
+
const splitIndex = this._chooseSplitIndex(node, m, M);
|
|
35927
|
+
|
|
35928
|
+
const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
|
|
35929
|
+
newNode.height = node.height;
|
|
35930
|
+
newNode.leaf = node.leaf;
|
|
35931
|
+
|
|
35932
|
+
calcBBox(node, this.toBBox);
|
|
35933
|
+
calcBBox(newNode, this.toBBox);
|
|
35934
|
+
|
|
35935
|
+
if (level) insertPath[level - 1].children.push(newNode);
|
|
35936
|
+
else this._splitRoot(node, newNode);
|
|
35937
|
+
}
|
|
35938
|
+
|
|
35939
|
+
_splitRoot(node, newNode) {
|
|
35940
|
+
// split root node
|
|
35941
|
+
this.data = createNode([node, newNode]);
|
|
35942
|
+
this.data.height = node.height + 1;
|
|
35943
|
+
this.data.leaf = false;
|
|
35944
|
+
calcBBox(this.data, this.toBBox);
|
|
35945
|
+
}
|
|
35946
|
+
|
|
35947
|
+
_chooseSplitIndex(node, m, M) {
|
|
35948
|
+
let index;
|
|
35949
|
+
let minOverlap = Infinity;
|
|
35950
|
+
let minArea = Infinity;
|
|
35951
|
+
|
|
35952
|
+
for (let i = m; i <= M - m; i++) {
|
|
35953
|
+
const bbox1 = distBBox(node, 0, i, this.toBBox);
|
|
35954
|
+
const bbox2 = distBBox(node, i, M, this.toBBox);
|
|
35955
|
+
|
|
35956
|
+
const overlap = intersectionArea(bbox1, bbox2);
|
|
35957
|
+
const area = bboxArea(bbox1) + bboxArea(bbox2);
|
|
35958
|
+
|
|
35959
|
+
// choose distribution with minimum overlap
|
|
35960
|
+
if (overlap < minOverlap) {
|
|
35961
|
+
minOverlap = overlap;
|
|
35962
|
+
index = i;
|
|
35963
|
+
|
|
35964
|
+
minArea = area < minArea ? area : minArea;
|
|
35965
|
+
|
|
35966
|
+
} else if (overlap === minOverlap) {
|
|
35967
|
+
// otherwise choose distribution with minimum area
|
|
35968
|
+
if (area < minArea) {
|
|
35969
|
+
minArea = area;
|
|
35970
|
+
index = i;
|
|
35971
|
+
}
|
|
35972
|
+
}
|
|
35973
|
+
}
|
|
35974
|
+
|
|
35975
|
+
return index || M - m;
|
|
35976
|
+
}
|
|
35977
|
+
|
|
35978
|
+
// sorts node children by the best axis for split
|
|
35979
|
+
_chooseSplitAxis(node, m, M) {
|
|
35980
|
+
const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
|
|
35981
|
+
const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
|
|
35982
|
+
const xMargin = this._allDistMargin(node, m, M, compareMinX);
|
|
35983
|
+
const yMargin = this._allDistMargin(node, m, M, compareMinY);
|
|
35984
|
+
|
|
35985
|
+
// if total distributions margin value is minimal for x, sort by minX,
|
|
35986
|
+
// otherwise it's already sorted by minY
|
|
35987
|
+
if (xMargin < yMargin) node.children.sort(compareMinX);
|
|
35988
|
+
}
|
|
35989
|
+
|
|
35990
|
+
// total margin of all possible split distributions where each node is at least m full
|
|
35991
|
+
_allDistMargin(node, m, M, compare) {
|
|
35992
|
+
node.children.sort(compare);
|
|
35993
|
+
|
|
35994
|
+
const toBBox = this.toBBox;
|
|
35995
|
+
const leftBBox = distBBox(node, 0, m, toBBox);
|
|
35996
|
+
const rightBBox = distBBox(node, M - m, M, toBBox);
|
|
35997
|
+
let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
|
|
35998
|
+
|
|
35999
|
+
for (let i = m; i < M - m; i++) {
|
|
36000
|
+
const child = node.children[i];
|
|
36001
|
+
extend(leftBBox, node.leaf ? toBBox(child) : child);
|
|
36002
|
+
margin += bboxMargin(leftBBox);
|
|
36003
|
+
}
|
|
36004
|
+
|
|
36005
|
+
for (let i = M - m - 1; i >= m; i--) {
|
|
36006
|
+
const child = node.children[i];
|
|
36007
|
+
extend(rightBBox, node.leaf ? toBBox(child) : child);
|
|
36008
|
+
margin += bboxMargin(rightBBox);
|
|
36009
|
+
}
|
|
36010
|
+
|
|
36011
|
+
return margin;
|
|
36012
|
+
}
|
|
36013
|
+
|
|
36014
|
+
_adjustParentBBoxes(bbox, path, level) {
|
|
36015
|
+
// adjust bboxes along the given tree path
|
|
36016
|
+
for (let i = level; i >= 0; i--) {
|
|
36017
|
+
extend(path[i], bbox);
|
|
36018
|
+
}
|
|
36019
|
+
}
|
|
36020
|
+
|
|
36021
|
+
_condense(path) {
|
|
36022
|
+
// go through the path, removing empty nodes and updating bboxes
|
|
36023
|
+
for (let i = path.length - 1, siblings; i >= 0; i--) {
|
|
36024
|
+
if (path[i].children.length === 0) {
|
|
36025
|
+
if (i > 0) {
|
|
36026
|
+
siblings = path[i - 1].children;
|
|
36027
|
+
siblings.splice(siblings.indexOf(path[i]), 1);
|
|
36028
|
+
|
|
36029
|
+
} else this.clear();
|
|
36030
|
+
|
|
36031
|
+
} else calcBBox(path[i], this.toBBox);
|
|
36032
|
+
}
|
|
36033
|
+
}
|
|
36034
|
+
}
|
|
36035
|
+
|
|
36036
|
+
function findItem(item, items, equalsFn) {
|
|
36037
|
+
if (!equalsFn) return items.indexOf(item);
|
|
36038
|
+
|
|
36039
|
+
for (let i = 0; i < items.length; i++) {
|
|
36040
|
+
if (equalsFn(item, items[i])) return i;
|
|
36041
|
+
}
|
|
36042
|
+
return -1;
|
|
36043
|
+
}
|
|
36044
|
+
|
|
36045
|
+
// calculate node's bbox from bboxes of its children
|
|
36046
|
+
function calcBBox(node, toBBox) {
|
|
36047
|
+
distBBox(node, 0, node.children.length, toBBox, node);
|
|
36048
|
+
}
|
|
36049
|
+
|
|
36050
|
+
// min bounding rectangle of node children from k to p-1
|
|
36051
|
+
function distBBox(node, k, p, toBBox, destNode) {
|
|
36052
|
+
if (!destNode) destNode = createNode(null);
|
|
36053
|
+
destNode.minX = Infinity;
|
|
36054
|
+
destNode.minY = Infinity;
|
|
36055
|
+
destNode.maxX = -Infinity;
|
|
36056
|
+
destNode.maxY = -Infinity;
|
|
36057
|
+
|
|
36058
|
+
for (let i = k; i < p; i++) {
|
|
36059
|
+
const child = node.children[i];
|
|
36060
|
+
extend(destNode, node.leaf ? toBBox(child) : child);
|
|
36061
|
+
}
|
|
36062
|
+
|
|
36063
|
+
return destNode;
|
|
36064
|
+
}
|
|
36065
|
+
|
|
36066
|
+
function extend(a, b) {
|
|
36067
|
+
a.minX = Math.min(a.minX, b.minX);
|
|
36068
|
+
a.minY = Math.min(a.minY, b.minY);
|
|
36069
|
+
a.maxX = Math.max(a.maxX, b.maxX);
|
|
36070
|
+
a.maxY = Math.max(a.maxY, b.maxY);
|
|
36071
|
+
return a;
|
|
36072
|
+
}
|
|
36073
|
+
|
|
36074
|
+
function compareNodeMinX(a, b) { return a.minX - b.minX; }
|
|
36075
|
+
function compareNodeMinY(a, b) { return a.minY - b.minY; }
|
|
36076
|
+
|
|
36077
|
+
function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
|
|
36078
|
+
function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
|
|
36079
|
+
|
|
36080
|
+
function enlargedArea(a, b) {
|
|
36081
|
+
return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
|
|
36082
|
+
(Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
|
|
36083
|
+
}
|
|
36084
|
+
|
|
36085
|
+
function intersectionArea(a, b) {
|
|
36086
|
+
const minX = Math.max(a.minX, b.minX);
|
|
36087
|
+
const minY = Math.max(a.minY, b.minY);
|
|
36088
|
+
const maxX = Math.min(a.maxX, b.maxX);
|
|
36089
|
+
const maxY = Math.min(a.maxY, b.maxY);
|
|
36090
|
+
|
|
36091
|
+
return Math.max(0, maxX - minX) *
|
|
36092
|
+
Math.max(0, maxY - minY);
|
|
36093
|
+
}
|
|
36094
|
+
|
|
36095
|
+
function contains(a, b) {
|
|
36096
|
+
return a.minX <= b.minX &&
|
|
36097
|
+
a.minY <= b.minY &&
|
|
36098
|
+
b.maxX <= a.maxX &&
|
|
36099
|
+
b.maxY <= a.maxY;
|
|
36100
|
+
}
|
|
36101
|
+
|
|
36102
|
+
function intersects(a, b) {
|
|
36103
|
+
return b.minX <= a.maxX &&
|
|
36104
|
+
b.minY <= a.maxY &&
|
|
36105
|
+
b.maxX >= a.minX &&
|
|
36106
|
+
b.maxY >= a.minY;
|
|
36107
|
+
}
|
|
36108
|
+
|
|
36109
|
+
function createNode(children) {
|
|
36110
|
+
return {
|
|
36111
|
+
children,
|
|
36112
|
+
height: 1,
|
|
36113
|
+
leaf: true,
|
|
36114
|
+
minX: Infinity,
|
|
36115
|
+
minY: Infinity,
|
|
36116
|
+
maxX: -Infinity,
|
|
36117
|
+
maxY: -Infinity
|
|
36118
|
+
};
|
|
36119
|
+
}
|
|
36120
|
+
|
|
36121
|
+
// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
|
|
36122
|
+
// combines selection algorithm with binary divide & conquer approach
|
|
36123
|
+
|
|
36124
|
+
function multiSelect(arr, left, right, n, compare) {
|
|
36125
|
+
const stack = [left, right];
|
|
36126
|
+
|
|
36127
|
+
while (stack.length) {
|
|
36128
|
+
right = stack.pop();
|
|
36129
|
+
left = stack.pop();
|
|
36130
|
+
|
|
36131
|
+
if (right - left <= n) continue;
|
|
36132
|
+
|
|
36133
|
+
const mid = left + Math.ceil((right - left) / n / 2) * n;
|
|
36134
|
+
quickselect(arr, mid, left, right, compare);
|
|
36135
|
+
|
|
36136
|
+
stack.push(left, mid, mid, right);
|
|
36137
|
+
}
|
|
36138
|
+
}
|
|
36139
|
+
|
|
36140
|
+
/**
|
|
36141
|
+
* R-Tree Data Structure
|
|
36142
|
+
*
|
|
36143
|
+
* R-Tree is a spatial data structure used for efficient indexing and querying
|
|
36144
|
+
* of multi-dimensional objects, particularly in geometric and spatial applications.
|
|
36145
|
+
*
|
|
36146
|
+
* It organizes objects into a tree hierarchy, grouping nearby objects together
|
|
36147
|
+
* in bounding boxes. Each node in the tree represents a bounding box that
|
|
36148
|
+
* contains its child nodes or leaf objects. This hierarchical structure allows
|
|
36149
|
+
* for faster spatial queries.
|
|
36150
|
+
*
|
|
36151
|
+
* @see https://en.wikipedia.org/wiki/R-tree
|
|
36152
|
+
*
|
|
36153
|
+
* Consider a 2D Space with four zones: A, B, C, D
|
|
36154
|
+
* +--------------------------+
|
|
36155
|
+
* | |
|
|
36156
|
+
* | +---+ +-------+ |
|
|
36157
|
+
* | | A | | B | |
|
|
36158
|
+
* | +---+ +-------+ |
|
|
36159
|
+
* | |
|
|
36160
|
+
* | |
|
|
36161
|
+
* | +---+ |
|
|
36162
|
+
* | | C | |
|
|
36163
|
+
* | +---+ |
|
|
36164
|
+
* | +-----------+ |
|
|
36165
|
+
* | | D | |
|
|
36166
|
+
* | +-----------+ |
|
|
36167
|
+
* | |
|
|
36168
|
+
* +--------------------------+
|
|
36169
|
+
*
|
|
36170
|
+
* It groups together zones that are spatially close into a minimum bounding box.
|
|
36171
|
+
* For example, A and B are grouped together in rectangle R1, and C and D are grouped
|
|
36172
|
+
* in R2.
|
|
36173
|
+
*
|
|
36174
|
+
* R0
|
|
36175
|
+
* +--------------------------+
|
|
36176
|
+
* | R1 |
|
|
36177
|
+
* | +-----------------+ |
|
|
36178
|
+
* | | A | | B | |
|
|
36179
|
+
* | +-----------------+ |
|
|
36180
|
+
* | |
|
|
36181
|
+
* | R2 |
|
|
36182
|
+
* | +---+---+---+ |
|
|
36183
|
+
* | | | C | | |
|
|
36184
|
+
* | | +---+ | |
|
|
36185
|
+
* | +-----------+ |
|
|
36186
|
+
* | | D | |
|
|
36187
|
+
* | +-----------+ |
|
|
36188
|
+
* | |
|
|
36189
|
+
* +--------------------------+
|
|
36190
|
+
*
|
|
36191
|
+
* The tree would look like this:
|
|
36192
|
+
* R0
|
|
36193
|
+
* / \
|
|
36194
|
+
* / \
|
|
36195
|
+
* R1 R2
|
|
36196
|
+
* | |
|
|
36197
|
+
* A,B C,D
|
|
36198
|
+
|
|
36199
|
+
* Choosing how to group the zones is crucial for the performance of the tree.
|
|
36200
|
+
* Key considerations include avoiding excessive empty space coverage and minimizing overlap
|
|
36201
|
+
* to reduce the number of subtrees processed during searches.
|
|
36202
|
+
*
|
|
36203
|
+
* Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
|
|
36204
|
+
* which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
|
|
36205
|
+
* the choice cannot be made based on this criterion due to the same enlargement for different groupings,
|
|
36206
|
+
* we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
|
|
36207
|
+
*
|
|
36208
|
+
* This implementation is tailored for spreadsheet use, indexing objects associated
|
|
36209
|
+
* with a zone and a sheet.
|
|
36210
|
+
*
|
|
36211
|
+
* It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
|
|
36212
|
+
* @see https://github.com/mourner/rbush
|
|
36213
|
+
*/
|
|
36214
|
+
class SpreadsheetRTree {
|
|
36215
|
+
/**
|
|
36216
|
+
* One 2D R-tree per sheet
|
|
36217
|
+
*/
|
|
36218
|
+
rTrees = {};
|
|
36219
|
+
/**
|
|
36220
|
+
* Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
|
|
36221
|
+
* faster than inserting items one by one. After bulk loading (bulk insertion into
|
|
36222
|
+
* an empty tree), subsequent query performance is also ~20-30% better.
|
|
36223
|
+
*/
|
|
36224
|
+
constructor(items = []) {
|
|
36225
|
+
const rangesPerSheet = {};
|
|
36226
|
+
for (const item of items) {
|
|
36227
|
+
const sheetId = item.boundingBox.sheetId;
|
|
36228
|
+
if (!rangesPerSheet[sheetId]) {
|
|
36229
|
+
rangesPerSheet[sheetId] = [];
|
|
36230
|
+
}
|
|
36231
|
+
rangesPerSheet[sheetId].push(item);
|
|
36232
|
+
}
|
|
36233
|
+
for (const sheetId in rangesPerSheet) {
|
|
36234
|
+
this.rTrees[sheetId] = new ZoneRBush();
|
|
36235
|
+
this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
|
|
36236
|
+
}
|
|
36237
|
+
}
|
|
36238
|
+
insert(item) {
|
|
36239
|
+
const sheetId = item.boundingBox.sheetId;
|
|
36240
|
+
if (!this.rTrees[sheetId]) {
|
|
36241
|
+
this.rTrees[sheetId] = new ZoneRBush();
|
|
36242
|
+
}
|
|
36243
|
+
this.rTrees[sheetId].insert(item);
|
|
36244
|
+
}
|
|
36245
|
+
search({ zone, sheetId }) {
|
|
36246
|
+
if (!this.rTrees[sheetId]) {
|
|
36247
|
+
return [];
|
|
36248
|
+
}
|
|
36249
|
+
return this.rTrees[sheetId].search({
|
|
36250
|
+
minX: zone.left,
|
|
36251
|
+
minY: zone.top,
|
|
36252
|
+
maxX: zone.right,
|
|
36253
|
+
maxY: zone.bottom,
|
|
36254
|
+
});
|
|
36255
|
+
}
|
|
36256
|
+
remove(item) {
|
|
36257
|
+
const sheetId = item.boundingBox.sheetId;
|
|
36258
|
+
if (!this.rTrees[sheetId]) {
|
|
36259
|
+
return;
|
|
36260
|
+
}
|
|
36261
|
+
this.rTrees[sheetId].remove(item, deepEquals);
|
|
36262
|
+
}
|
|
36263
|
+
}
|
|
36264
|
+
/**
|
|
36265
|
+
* RBush extension to use zones as bounding boxes
|
|
36266
|
+
*/
|
|
36267
|
+
class ZoneRBush extends RBush {
|
|
36268
|
+
toBBox({ boundingBox }) {
|
|
36269
|
+
const zone = boundingBox.zone;
|
|
36270
|
+
return {
|
|
36271
|
+
minX: zone.left,
|
|
36272
|
+
minY: zone.top,
|
|
36273
|
+
maxX: zone.right,
|
|
36274
|
+
maxY: zone.bottom,
|
|
36275
|
+
};
|
|
36276
|
+
}
|
|
36277
|
+
compareMinX(a, b) {
|
|
36278
|
+
return a.boundingBox.zone.left - b.boundingBox.zone.left;
|
|
36279
|
+
}
|
|
36280
|
+
compareMinY(a, b) {
|
|
36281
|
+
return a.boundingBox.zone.top - b.boundingBox.zone.top;
|
|
36282
|
+
}
|
|
36283
|
+
}
|
|
36284
|
+
|
|
35562
36285
|
/**
|
|
35563
|
-
*
|
|
36286
|
+
* Implementation of a dependency Graph.
|
|
35564
36287
|
* The graph is used to evaluate the cells in the correct
|
|
35565
36288
|
* order, and should be updated each time a cell's content is modified
|
|
35566
36289
|
*
|
|
36290
|
+
* It uses an R-Tree data structure to efficiently find dependent cells.
|
|
35567
36291
|
*/
|
|
35568
36292
|
class FormulaDependencyGraph {
|
|
35569
|
-
|
|
35570
|
-
* Internal structure:
|
|
35571
|
-
* - key: a cell position (encoded as an integer)
|
|
35572
|
-
* - value: a set of cell positions that depends on the key
|
|
35573
|
-
*
|
|
35574
|
-
* Given
|
|
35575
|
-
* - A1:"= B1 + SQRT(B2)"
|
|
35576
|
-
* - C1:"= B1";
|
|
35577
|
-
* - C2:"= C1"
|
|
35578
|
-
*
|
|
35579
|
-
* we will have something like:
|
|
35580
|
-
* - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
|
|
35581
|
-
* - B2 ---> (A1) meaning A1 depends on B2
|
|
35582
|
-
* - C1 ---> (C2) meaning C2 depends on C1
|
|
35583
|
-
*/
|
|
35584
|
-
inverseDependencies = new Map();
|
|
36293
|
+
encoder;
|
|
35585
36294
|
dependencies = new Map();
|
|
36295
|
+
rTree;
|
|
36296
|
+
constructor(encoder, data = []) {
|
|
36297
|
+
this.encoder = encoder;
|
|
36298
|
+
this.rTree = new SpreadsheetRTree(data);
|
|
36299
|
+
}
|
|
35586
36300
|
removeAllDependencies(formulaPositionId) {
|
|
35587
|
-
const
|
|
35588
|
-
if (!
|
|
36301
|
+
const ranges = this.dependencies.get(formulaPositionId);
|
|
36302
|
+
if (!ranges) {
|
|
35589
36303
|
return;
|
|
35590
36304
|
}
|
|
35591
|
-
for (const
|
|
35592
|
-
this.
|
|
36305
|
+
for (const range of ranges) {
|
|
36306
|
+
this.rTree.remove(range);
|
|
35593
36307
|
}
|
|
35594
36308
|
this.dependencies.delete(formulaPositionId);
|
|
35595
36309
|
}
|
|
35596
36310
|
addDependencies(formulaPositionId, dependencies) {
|
|
35597
|
-
|
|
35598
|
-
|
|
35599
|
-
|
|
35600
|
-
|
|
35601
|
-
|
|
35602
|
-
|
|
35603
|
-
|
|
35604
|
-
|
|
36311
|
+
const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
|
|
36312
|
+
data: formulaPositionId,
|
|
36313
|
+
boundingBox: {
|
|
36314
|
+
zone,
|
|
36315
|
+
sheetId,
|
|
36316
|
+
},
|
|
36317
|
+
}));
|
|
36318
|
+
for (const item of rTreeItems) {
|
|
36319
|
+
this.rTree.insert(item);
|
|
35605
36320
|
}
|
|
35606
36321
|
const existingDependencies = this.dependencies.get(formulaPositionId);
|
|
35607
36322
|
if (existingDependencies) {
|
|
35608
|
-
existingDependencies.push(...
|
|
36323
|
+
existingDependencies.push(...rTreeItems);
|
|
35609
36324
|
}
|
|
35610
36325
|
else {
|
|
35611
|
-
this.dependencies.set(formulaPositionId,
|
|
36326
|
+
this.dependencies.set(formulaPositionId, rTreeItems);
|
|
35612
36327
|
}
|
|
35613
36328
|
}
|
|
35614
36329
|
/**
|
|
@@ -35616,20 +36331,20 @@ class FormulaDependencyGraph {
|
|
|
35616
36331
|
* in the correct order they should be evaluated.
|
|
35617
36332
|
* This is called a topological ordering (excluding cycles)
|
|
35618
36333
|
*/
|
|
35619
|
-
getCellsDependingOn(
|
|
36334
|
+
getCellsDependingOn(ranges) {
|
|
35620
36335
|
const visited = new JetSet();
|
|
35621
|
-
const queue = Array.from(
|
|
36336
|
+
const queue = Array.from(ranges).reverse();
|
|
35622
36337
|
while (queue.length > 0) {
|
|
35623
|
-
const
|
|
35624
|
-
visited.add(
|
|
35625
|
-
const
|
|
35626
|
-
for (const
|
|
35627
|
-
if (!visited.has(
|
|
35628
|
-
queue.push(
|
|
36338
|
+
const range = queue.pop();
|
|
36339
|
+
visited.add(...this.encoder.encodeBoundingBox(range));
|
|
36340
|
+
const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
|
|
36341
|
+
for (const positionId of impactedPositionIds) {
|
|
36342
|
+
if (!visited.has(positionId)) {
|
|
36343
|
+
queue.push(this.encoder.decodeToBoundingBox(positionId));
|
|
35629
36344
|
}
|
|
35630
36345
|
}
|
|
35631
36346
|
}
|
|
35632
|
-
visited.delete(...
|
|
36347
|
+
visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
|
|
35633
36348
|
return visited;
|
|
35634
36349
|
}
|
|
35635
36350
|
}
|
|
@@ -35717,9 +36432,9 @@ class Evaluator {
|
|
|
35717
36432
|
context;
|
|
35718
36433
|
getters;
|
|
35719
36434
|
compilationParams;
|
|
35720
|
-
|
|
36435
|
+
encoder = new PositionBitsEncoder();
|
|
35721
36436
|
evaluatedCells = new Map();
|
|
35722
|
-
formulaDependencies = lazy(new FormulaDependencyGraph());
|
|
36437
|
+
formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
|
|
35723
36438
|
blockedArrayFormulas = new Set();
|
|
35724
36439
|
spreadingRelations = new SpreadingRelation();
|
|
35725
36440
|
constructor(context, getters) {
|
|
@@ -35728,16 +36443,16 @@ class Evaluator {
|
|
|
35728
36443
|
this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
|
|
35729
36444
|
}
|
|
35730
36445
|
getEvaluatedCell(position) {
|
|
35731
|
-
return (this.evaluatedCells.get(this.
|
|
36446
|
+
return (this.evaluatedCells.get(this.encoder.encode(position)) ||
|
|
35732
36447
|
createEvaluatedCell("", { locale: this.getters.getLocale() }));
|
|
35733
36448
|
}
|
|
35734
36449
|
getArrayFormulaSpreadingOn(position) {
|
|
35735
|
-
const positionId = this.
|
|
36450
|
+
const positionId = this.encoder.encode(position);
|
|
35736
36451
|
const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
|
|
35737
|
-
return formulaPosition !== undefined ? this.
|
|
36452
|
+
return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
|
|
35738
36453
|
}
|
|
35739
36454
|
getEvaluatedPositions() {
|
|
35740
|
-
return [...this.evaluatedCells.keys()].map(this.
|
|
36455
|
+
return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
|
|
35741
36456
|
}
|
|
35742
36457
|
getArrayFormulaSpreadingOnId(positionId) {
|
|
35743
36458
|
if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
|
|
@@ -35747,7 +36462,7 @@ class Evaluator {
|
|
|
35747
36462
|
return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
|
|
35748
36463
|
}
|
|
35749
36464
|
updateDependencies(position) {
|
|
35750
|
-
const positionId = this.
|
|
36465
|
+
const positionId = this.encoder.encode(position);
|
|
35751
36466
|
this.formulaDependencies().removeAllDependencies(positionId);
|
|
35752
36467
|
const dependencies = this.getDirectDependencies(positionId);
|
|
35753
36468
|
this.formulaDependencies().addDependencies(positionId, dependencies);
|
|
@@ -35757,12 +36472,12 @@ class Evaluator {
|
|
|
35757
36472
|
this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
|
|
35758
36473
|
}
|
|
35759
36474
|
evaluateCells(positions) {
|
|
35760
|
-
const cells = positions.map(this.
|
|
36475
|
+
const cells = positions.map((p) => this.encoder.encode(p));
|
|
35761
36476
|
const cellsToCompute = new JetSet(cells);
|
|
35762
|
-
const
|
|
36477
|
+
const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
|
|
35763
36478
|
cellsToCompute.add(...this.getCellsDependingOn(cells));
|
|
35764
|
-
cellsToCompute.add(...
|
|
35765
|
-
cellsToCompute.add(...this.getCellsDependingOn(
|
|
36479
|
+
cellsToCompute.add(...arrayFormulasPositionIds);
|
|
36480
|
+
cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
|
|
35766
36481
|
this.evaluate(cellsToCompute);
|
|
35767
36482
|
}
|
|
35768
36483
|
getArrayFormulasImpactedByChangesOf(positionIds) {
|
|
@@ -35785,12 +36500,14 @@ class Evaluator {
|
|
|
35785
36500
|
this.blockedArrayFormulas = new Set();
|
|
35786
36501
|
this.spreadingRelations = new SpreadingRelation();
|
|
35787
36502
|
this.formulaDependencies = lazy(() => {
|
|
35788
|
-
const
|
|
35789
|
-
|
|
35790
|
-
|
|
35791
|
-
|
|
35792
|
-
|
|
35793
|
-
|
|
36503
|
+
const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
|
|
36504
|
+
data: positionId,
|
|
36505
|
+
boundingBox: {
|
|
36506
|
+
zone: range.zone,
|
|
36507
|
+
sheetId: range.sheetId,
|
|
36508
|
+
},
|
|
36509
|
+
})));
|
|
36510
|
+
return new FormulaDependencyGraph(this.encoder, dependencies);
|
|
35794
36511
|
});
|
|
35795
36512
|
}
|
|
35796
36513
|
evaluateAllCells() {
|
|
@@ -35811,7 +36528,7 @@ class Evaluator {
|
|
|
35811
36528
|
for (const sheetId of this.getters.getSheetIds()) {
|
|
35812
36529
|
const cellIds = this.getters.getCells(sheetId);
|
|
35813
36530
|
for (const cellId in cellIds) {
|
|
35814
|
-
positionIds.add(this.
|
|
36531
|
+
positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
|
|
35815
36532
|
}
|
|
35816
36533
|
}
|
|
35817
36534
|
return positionIds;
|
|
@@ -35878,7 +36595,7 @@ class Evaluator {
|
|
|
35878
36595
|
}
|
|
35879
36596
|
}
|
|
35880
36597
|
computeAndSave(position) {
|
|
35881
|
-
const positionId = this.
|
|
36598
|
+
const positionId = this.encoder.encode(position);
|
|
35882
36599
|
const evaluatedCell = this.computeCell(positionId);
|
|
35883
36600
|
if (!this.evaluatedCells.has(positionId)) {
|
|
35884
36601
|
this.setEvaluatedCell(positionId, evaluatedCell);
|
|
@@ -35939,15 +36656,15 @@ class Evaluator {
|
|
|
35939
36656
|
throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
|
|
35940
36657
|
}
|
|
35941
36658
|
updateSpreadRelation({ sheetId, col, row, }) {
|
|
35942
|
-
const arrayFormulaPositionId = this.
|
|
36659
|
+
const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
|
|
35943
36660
|
return (i, j) => {
|
|
35944
36661
|
const position = { sheetId, col: i + col, row: j + row };
|
|
35945
|
-
const resultPositionId = this.
|
|
36662
|
+
const resultPositionId = this.encoder.encode(position);
|
|
35946
36663
|
this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
|
|
35947
36664
|
};
|
|
35948
36665
|
}
|
|
35949
36666
|
checkCollision({ sheetId, col, row }) {
|
|
35950
|
-
const formulaPositionId = this.
|
|
36667
|
+
const formulaPositionId = this.encoder.encode({ sheetId, col, row });
|
|
35951
36668
|
return (i, j) => {
|
|
35952
36669
|
const position = { sheetId: sheetId, col: i + col, row: j + row };
|
|
35953
36670
|
const rawCell = this.getters.getCell(position);
|
|
@@ -35969,7 +36686,7 @@ class Evaluator {
|
|
|
35969
36686
|
format: format || formatFromPosition(i, j),
|
|
35970
36687
|
locale: this.getters.getLocale(),
|
|
35971
36688
|
});
|
|
35972
|
-
const positionId = this.
|
|
36689
|
+
const positionId = this.encoder.encode(position);
|
|
35973
36690
|
this.setEvaluatedCell(positionId, evaluatedCell);
|
|
35974
36691
|
// check if formula dependencies present in the spread zone
|
|
35975
36692
|
// if so, they need to be recomputed
|
|
@@ -36001,29 +36718,17 @@ class Evaluator {
|
|
|
36001
36718
|
if (!cell?.isFormula) {
|
|
36002
36719
|
return [];
|
|
36003
36720
|
}
|
|
36004
|
-
|
|
36005
|
-
for (const range of cell.dependencies) {
|
|
36006
|
-
if (range.invalidSheetName || range.invalidXc) {
|
|
36007
|
-
continue;
|
|
36008
|
-
}
|
|
36009
|
-
const sheetId = range.sheetId;
|
|
36010
|
-
forEachPositionsInZone(range.zone, (col, row) => {
|
|
36011
|
-
dependencies.push(this.encodePosition({ sheetId, col, row }));
|
|
36012
|
-
});
|
|
36013
|
-
}
|
|
36014
|
-
return dependencies;
|
|
36721
|
+
return cell.dependencies;
|
|
36015
36722
|
}
|
|
36016
36723
|
getCellsDependingOn(positionIds) {
|
|
36017
|
-
|
|
36724
|
+
const ranges = [];
|
|
36725
|
+
for (const positionId of positionIds) {
|
|
36726
|
+
ranges.push(this.encoder.decodeToBoundingBox(positionId));
|
|
36727
|
+
}
|
|
36728
|
+
return this.formulaDependencies().getCellsDependingOn(ranges);
|
|
36018
36729
|
}
|
|
36019
36730
|
getCell(positionId) {
|
|
36020
|
-
return this.getters.getCell(this.
|
|
36021
|
-
}
|
|
36022
|
-
encodePosition(position) {
|
|
36023
|
-
return this.positionEncoder.encode(position);
|
|
36024
|
-
}
|
|
36025
|
-
decodePosition(positionId) {
|
|
36026
|
-
return this.positionEncoder.decode(positionId);
|
|
36731
|
+
return this.getters.getCell(this.encoder.decode(positionId));
|
|
36027
36732
|
}
|
|
36028
36733
|
}
|
|
36029
36734
|
function forEachSpreadPositionInMatrix(matrix, callback) {
|
|
@@ -36098,6 +36803,13 @@ class PositionBitsEncoder {
|
|
|
36098
36803
|
encode({ sheetId, col, row }) {
|
|
36099
36804
|
return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
|
|
36100
36805
|
}
|
|
36806
|
+
encodeBoundingBox({ sheetId, zone }) {
|
|
36807
|
+
const positions = [];
|
|
36808
|
+
forEachPositionsInZone(zone, (col, row) => {
|
|
36809
|
+
positions.push(this.encode({ sheetId, col, row }));
|
|
36810
|
+
});
|
|
36811
|
+
return positions;
|
|
36812
|
+
}
|
|
36101
36813
|
decode(id) {
|
|
36102
36814
|
// keep only the last 21 bits by AND-ing the bit sequence with 21 ones
|
|
36103
36815
|
const row = Number(id & 2097151n);
|
|
@@ -36105,6 +36817,10 @@ class PositionBitsEncoder {
|
|
|
36105
36817
|
const sheetId = this.decodeSheet(id >> 42n);
|
|
36106
36818
|
return { sheetId, col, row };
|
|
36107
36819
|
}
|
|
36820
|
+
decodeToBoundingBox(id) {
|
|
36821
|
+
const { sheetId, col, row } = this.decode(id);
|
|
36822
|
+
return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
|
|
36823
|
+
}
|
|
36108
36824
|
encodeSheet(sheetId) {
|
|
36109
36825
|
const sheetKey = this.sheetMapping[sheetId];
|
|
36110
36826
|
if (sheetKey === undefined) {
|
|
@@ -36219,6 +36935,7 @@ class PositionBitsEncoder {
|
|
|
36219
36935
|
class EvaluationPlugin extends UIPlugin {
|
|
36220
36936
|
static getters = [
|
|
36221
36937
|
"evaluateFormula",
|
|
36938
|
+
"getCorrespondingFormulaCell",
|
|
36222
36939
|
"getRangeFormattedValues",
|
|
36223
36940
|
"getRangeValues",
|
|
36224
36941
|
"getRangeFormats",
|
|
@@ -36272,7 +36989,12 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
36272
36989
|
// Getters
|
|
36273
36990
|
// ---------------------------------------------------------------------------
|
|
36274
36991
|
evaluateFormula(sheetId, formulaString) {
|
|
36275
|
-
|
|
36992
|
+
try {
|
|
36993
|
+
return this.evaluator.evaluateFormula(sheetId, formulaString);
|
|
36994
|
+
}
|
|
36995
|
+
catch (error) {
|
|
36996
|
+
return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
|
|
36997
|
+
}
|
|
36276
36998
|
}
|
|
36277
36999
|
/**
|
|
36278
37000
|
* Return the value of each cell in the range as they are displayed in the grid.
|
|
@@ -36535,7 +37257,7 @@ class CustomColorsPlugin extends UIPlugin {
|
|
|
36535
37257
|
}
|
|
36536
37258
|
|
|
36537
37259
|
class EvaluationChartPlugin extends UIPlugin {
|
|
36538
|
-
static getters = ["getChartRuntime", "
|
|
37260
|
+
static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
|
|
36539
37261
|
charts = {};
|
|
36540
37262
|
createRuntimeChart = chartRuntimeFactory(this.getters);
|
|
36541
37263
|
handle(cmd) {
|
|
@@ -36573,25 +37295,26 @@ class EvaluationChartPlugin extends UIPlugin {
|
|
|
36573
37295
|
return this.charts[figureId];
|
|
36574
37296
|
}
|
|
36575
37297
|
/**
|
|
36576
|
-
* Get the background
|
|
36577
|
-
* of the chart. In order of priority, it will return :
|
|
36578
|
-
*
|
|
36579
|
-
* - the chart background color if one is defined
|
|
36580
|
-
* - the fill color of the cell if one is defined
|
|
36581
|
-
* - the fill color of the cell from conditional formats if one is defined
|
|
36582
|
-
* - the default chart color if no other color is defined
|
|
37298
|
+
* Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
|
|
36583
37299
|
*/
|
|
36584
|
-
|
|
37300
|
+
getStyleOfSingleCellChart(chartBackground, mainRange) {
|
|
36585
37301
|
if (chartBackground)
|
|
36586
|
-
return chartBackground;
|
|
37302
|
+
return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
|
|
36587
37303
|
if (!mainRange) {
|
|
36588
|
-
return
|
|
37304
|
+
return {
|
|
37305
|
+
background: BACKGROUND_CHART_COLOR,
|
|
37306
|
+
fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
|
|
37307
|
+
};
|
|
36589
37308
|
}
|
|
36590
37309
|
const col = mainRange.zone.left;
|
|
36591
37310
|
const row = mainRange.zone.top;
|
|
36592
37311
|
const sheetId = mainRange.sheetId;
|
|
36593
37312
|
const style = this.getters.getCellComputedStyle({ sheetId, col, row });
|
|
36594
|
-
|
|
37313
|
+
const background = style.fillColor || BACKGROUND_CHART_COLOR;
|
|
37314
|
+
return {
|
|
37315
|
+
background,
|
|
37316
|
+
fontColor: style.textColor || chartFontColor(background),
|
|
37317
|
+
};
|
|
36595
37318
|
}
|
|
36596
37319
|
}
|
|
36597
37320
|
|
|
@@ -36661,34 +37384,29 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
|
|
|
36661
37384
|
getComputedStyles(sheetId) {
|
|
36662
37385
|
const computedStyle = {};
|
|
36663
37386
|
for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
|
|
36664
|
-
|
|
36665
|
-
|
|
36666
|
-
|
|
36667
|
-
|
|
36668
|
-
|
|
36669
|
-
|
|
36670
|
-
|
|
36671
|
-
|
|
36672
|
-
|
|
36673
|
-
|
|
36674
|
-
for (let
|
|
36675
|
-
|
|
36676
|
-
|
|
36677
|
-
|
|
36678
|
-
if (
|
|
36679
|
-
|
|
36680
|
-
|
|
36681
|
-
|
|
36682
|
-
computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
|
|
36683
|
-
}
|
|
37387
|
+
switch (cf.rule.type) {
|
|
37388
|
+
case "ColorScaleRule":
|
|
37389
|
+
for (let range of cf.ranges) {
|
|
37390
|
+
this.applyColorScale(sheetId, range, cf.rule, computedStyle);
|
|
37391
|
+
}
|
|
37392
|
+
break;
|
|
37393
|
+
case "CellIsRule":
|
|
37394
|
+
for (let ref of cf.ranges) {
|
|
37395
|
+
const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
|
|
37396
|
+
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
37397
|
+
for (let col = zone.left; col <= zone.right; col++) {
|
|
37398
|
+
const pr = this.rulePredicate[cf.rule.type];
|
|
37399
|
+
let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
|
|
37400
|
+
if (pr && pr(cell, cf.rule)) {
|
|
37401
|
+
if (!computedStyle[col])
|
|
37402
|
+
computedStyle[col] = [];
|
|
37403
|
+
// we must combine all the properties of all the CF rules applied to the given cell
|
|
37404
|
+
computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
|
|
36684
37405
|
}
|
|
36685
37406
|
}
|
|
36686
37407
|
}
|
|
36687
|
-
|
|
36688
|
-
|
|
36689
|
-
}
|
|
36690
|
-
catch (_) {
|
|
36691
|
-
// we don't care about the errors within the evaluation of a rule
|
|
37408
|
+
}
|
|
37409
|
+
break;
|
|
36692
37410
|
}
|
|
36693
37411
|
}
|
|
36694
37412
|
return computedStyle;
|
|
@@ -36724,7 +37442,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
|
|
|
36724
37442
|
return percentile(rangeValues, Number(threshold.value) / 100, true);
|
|
36725
37443
|
case "formula":
|
|
36726
37444
|
const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);
|
|
36727
|
-
return
|
|
37445
|
+
return typeof value === "number" ? value : null;
|
|
36728
37446
|
default:
|
|
36729
37447
|
return null;
|
|
36730
37448
|
}
|
|
@@ -37290,19 +38008,21 @@ class AutofillPlugin extends UIPlugin {
|
|
|
37290
38008
|
let col = zone.left;
|
|
37291
38009
|
let row = zone.bottom;
|
|
37292
38010
|
if (col > 0) {
|
|
37293
|
-
let
|
|
37294
|
-
while (
|
|
38011
|
+
let leftPosition = { sheetId, col: col - 1, row };
|
|
38012
|
+
while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
|
|
38013
|
+
this.getters.getCell(leftPosition)?.content) {
|
|
37295
38014
|
row += 1;
|
|
37296
|
-
|
|
38015
|
+
leftPosition = { sheetId, col: col - 1, row };
|
|
37297
38016
|
}
|
|
37298
38017
|
}
|
|
37299
38018
|
if (row === zone.bottom) {
|
|
37300
38019
|
col = zone.right;
|
|
37301
38020
|
if (col <= this.getters.getNumberCols(sheetId)) {
|
|
37302
|
-
let
|
|
37303
|
-
while (
|
|
38021
|
+
let rightPosition = { sheetId, col: col + 1, row };
|
|
38022
|
+
while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
|
|
38023
|
+
this.getters.getCell(rightPosition)?.content) {
|
|
37304
38024
|
row += 1;
|
|
37305
|
-
|
|
38025
|
+
rightPosition = { sheetId, col: col + 1, row };
|
|
37306
38026
|
}
|
|
37307
38027
|
}
|
|
37308
38028
|
}
|
|
@@ -40220,6 +40940,9 @@ class SelectionInputsManagerPlugin extends UIPlugin {
|
|
|
40220
40940
|
// Other
|
|
40221
40941
|
// ---------------------------------------------------------------------------
|
|
40222
40942
|
initInput(id, initialRanges, inputHasSingleRange = false) {
|
|
40943
|
+
if (this.inputs[id]) {
|
|
40944
|
+
this.unfocus();
|
|
40945
|
+
}
|
|
40223
40946
|
this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);
|
|
40224
40947
|
if (initialRanges.length === 0) {
|
|
40225
40948
|
const input = this.inputs[id];
|
|
@@ -42335,6 +43058,10 @@ function setXcToReferenceType(xc, referenceType) {
|
|
|
42335
43058
|
return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
|
|
42336
43059
|
case "colrow":
|
|
42337
43060
|
indexOfNumber = xc.search(/[0-9]/);
|
|
43061
|
+
if (indexOfNumber === -1 || indexOfNumber === 0) {
|
|
43062
|
+
// no row number (eg. A) or no column (eg. 1)
|
|
43063
|
+
return "$" + xc;
|
|
43064
|
+
}
|
|
42338
43065
|
xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
|
|
42339
43066
|
return "$" + xc;
|
|
42340
43067
|
case "none":
|
|
@@ -43750,7 +44477,7 @@ class GridSelectionPlugin extends UIPlugin {
|
|
|
43750
44477
|
sheetId: cmd.sheetId,
|
|
43751
44478
|
base: cmd.base,
|
|
43752
44479
|
quantity: thickness,
|
|
43753
|
-
position:
|
|
44480
|
+
position: cmd.position,
|
|
43754
44481
|
});
|
|
43755
44482
|
const isCol = cmd.dimension === "COL";
|
|
43756
44483
|
const start = cmd.elements[0];
|
|
@@ -43767,12 +44494,13 @@ class GridSelectionPlugin extends UIPlugin {
|
|
|
43767
44494
|
},
|
|
43768
44495
|
];
|
|
43769
44496
|
const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
|
|
44497
|
+
const base = isBasedBefore ? cmd.base : cmd.base + 1;
|
|
43770
44498
|
const pasteTarget = [
|
|
43771
44499
|
{
|
|
43772
|
-
left: isCol ?
|
|
43773
|
-
right: isCol ?
|
|
43774
|
-
top: !isCol ?
|
|
43775
|
-
bottom: !isCol ?
|
|
44500
|
+
left: isCol ? base : 0,
|
|
44501
|
+
right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
|
|
44502
|
+
top: !isCol ? base : 0,
|
|
44503
|
+
bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
|
|
43776
44504
|
},
|
|
43777
44505
|
];
|
|
43778
44506
|
state.paste(pasteTarget, { selectTarget: true });
|
|
@@ -43809,6 +44537,11 @@ class GridSelectionPlugin extends UIPlugin {
|
|
|
43809
44537
|
doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
|
|
43810
44538
|
return 2 /* CommandResult.WillRemoveExistingMerge */;
|
|
43811
44539
|
}
|
|
44540
|
+
const headers = [cmd.base, ...cmd.elements];
|
|
44541
|
+
const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
|
|
44542
|
+
if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
|
|
44543
|
+
return 88 /* CommandResult.InvalidHeaderIndex */;
|
|
44544
|
+
}
|
|
43812
44545
|
return 0 /* CommandResult.Success */;
|
|
43813
44546
|
}
|
|
43814
44547
|
//-------------------------------------------
|
|
@@ -47034,7 +47767,7 @@ class Spreadsheet extends owl.Component {
|
|
|
47034
47767
|
}
|
|
47035
47768
|
onKeydown(ev) {
|
|
47036
47769
|
let keyDownString = "";
|
|
47037
|
-
if (ev
|
|
47770
|
+
if (isCtrlKey(ev)) {
|
|
47038
47771
|
keyDownString += "CTRL+";
|
|
47039
47772
|
}
|
|
47040
47773
|
keyDownString += ev.key.toUpperCase();
|
|
@@ -50793,6 +51526,6 @@ exports.setTranslationMethod = setTranslationMethod;
|
|
|
50793
51526
|
exports.tokenize = tokenize;
|
|
50794
51527
|
|
|
50795
51528
|
|
|
50796
|
-
__info__.version = '16.4.
|
|
50797
|
-
__info__.date = '
|
|
50798
|
-
__info__.hash = '
|
|
51529
|
+
__info__.version = '16.4.17';
|
|
51530
|
+
__info__.date = '2024-01-04T12:22:53.125Z';
|
|
51531
|
+
__info__.hash = '769f8a3';
|