loleaflet/src/layer/tile/CalcTileLayer.js | 841 ++++++++++++++++++++++++++++++ 1 file changed, 841 insertions(+)
New commits: commit b36e069549508e927c75e8000f0cff6024884a05 Author: Dennis Francis <dennis.fran...@collabora.com> AuthorDate: Sat May 9 20:34:37 2020 +0530 Commit: Dennis Francis <dennis.fran...@collabora.com> CommitDate: Sun Jul 5 09:55:33 2020 +0200 add sheet-geometry datastructures to parse, store the .uno:SheetGeometryData JSON efficiently although it is optimized for fast querying. L.SheetGeometry is the external class that exposes all necessary sheet query interfaces. Change-Id: I24df8d85734a6cdf9c393fd2c3c5ed4de0ea29f3 Reviewed-on: https://gerrit.libreoffice.org/c/online/+/97940 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Dennis Francis <dennis.fran...@collabora.com> diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js b/loleaflet/src/layer/tile/CalcTileLayer.js index 06ffa2bef..b0773251d 100644 --- a/loleaflet/src/layer/tile/CalcTileLayer.js +++ b/loleaflet/src/layer/tile/CalcTileLayer.js @@ -567,3 +567,844 @@ L.CalcTileLayer = L.TileLayer.extend({ this._onUpdateCurrentHeader(); } }); + + +// TODO: Move these somewhere more appropriate. + +// Sheet geometry data +L.SheetGeometry = L.Class.extend({ + + // sheetGeomJSON is expected to be the parsed JSON message from core + // in response to client command '.uno:SheetGeometryData' with + // all flags (ie 'columns', 'rows', 'sizes', 'hidden', 'filtered', + // 'groups') enabled. + initialize: function (sheetGeomJSON, tileWidthTwips, tileHeightTwips, + tileSizeCSSPixels, dpiScale) { + + if (typeof sheetGeomJSON !== 'object' || + typeof tileWidthTwips !== 'number' || + typeof tileHeightTwips !== 'number' || + typeof tileSizeCSSPixels !== 'number' || + typeof dpiScale !== 'number') { + console.error('Incorrect constructor argument types or missing required arguments'); + return; + } + + this._columns = new L.SheetDimension(); + this._rows = new L.SheetDimension(); + this._unoCommand = '.uno:SheetGeometryData'; + + // Set various unit conversion info early on because on update() call below, these info are needed. + this.setTileGeometryData(tileWidthTwips, tileHeightTwips, tileSizeCSSPixels, + dpiScale, false /* update position info ?*/); + + this.update(sheetGeomJSON, /* checkCompleteness */ true); + }, + + update: function (sheetGeomJSON, checkCompleteness) { + + if (!this._testValidity(sheetGeomJSON, checkCompleteness)) { + return false; + } + + var updateOK = true; + if (sheetGeomJSON.columns) { + if (!this._columns.update(sheetGeomJSON.columns)) { + console.error(this._unoCommand + ': columns update failed.'); + updateOK = false; + } + } + + if (sheetGeomJSON.rows) { + if (!this._rows.update(sheetGeomJSON.rows)) { + console.error(this._unoCommand + ': rows update failed.'); + updateOK = false; + } + } + + this._columns.setMaxIndex(+sheetGeomJSON.maxtiledcolumn); + this._rows.setMaxIndex(+sheetGeomJSON.maxtiledrow); + + return updateOK; + }, + + setTileGeometryData: function (tileWidthTwips, tileHeightTwips, tileSizeCSSPixels, + dpiScale, updatePositions) { + + this._columns.setTileGeometryData(tileWidthTwips, tileSizeCSSPixels, dpiScale, updatePositions); + this._rows.setTileGeometryData(tileHeightTwips, tileSizeCSSPixels, dpiScale, updatePositions); + }, + + setViewArea: function (topLeftTwipsPoint, sizeTwips) { + + if (!(topLeftTwipsPoint instanceof L.Point) || !(sizeTwips instanceof L.Point)) { + console.error('invalid argument types'); + return false; + } + + var left = topLeftTwipsPoint.x; + var top = topLeftTwipsPoint.y; + var right = left + sizeTwips.x; + var bottom = top + sizeTwips.y; + + this._columns.setViewLimits(left, right); + this._rows.setViewLimits(top, bottom); + + return true; + }, + + // returns an object with keys 'start' and 'end' indicating the + // column range in the current view area. + getViewColumnRange: function () { + return this._columns.getViewElementRange(); + }, + + // returns an object with keys 'start' and 'end' indicating the + // row range in the current view area. + getViewRowRange: function () { + return this._rows.getViewElementRange(); + }, + + getViewCellRange: function () { + return { + columnrange: this.getViewColumnRange(), + rowrange: this.getViewRowRange() + }; + }, + + // Returns an object with the following fields: + // columnIndex should be zero based. + // 'startpos' (start position of the column in css pixels), 'size' (column size in css pixels). + // Note: All these fields are computed by assuming zero sizes for hidden/filtered columns. + getColumnData: function (columnIndex) { + return this._columns.getElementData(columnIndex); + }, + + // Returns an object with the following fields: + // rowIndex should be zero based. + // 'startpos' (start position of the row in css pixels), 'size' (row size in css pixels). + // Note: All these fields are computed by assuming zero sizes for hidden/filtered rows. + getRowData: function (rowIndex) { + return this._rows.getElementData(rowIndex); + }, + + // Runs the callback for every column in the inclusive range [columnStart, columnEnd]. + // callback is expected to have a signature of (column, columnData) + // where 'column' will contain the column index(zero based) and 'columnData' will be an object with + // the same fields as returned by getColumnData(). + forEachColumnInRange: function (columnStart, columnEnd, callback) { + this._columns.forEachInRange(columnStart, columnEnd, callback); + }, + + // Runs the callback for every row in the inclusive range [rowStart, rowEnd]. + // callback is expected to have a signature of (row, rowData) + // where 'row' will contain the row index(zero based) and 'rowData' will be an object with + // the same fields as returned by getRowData(). + forEachRowInRange: function (rowStart, rowEnd, callback) { + this._rows.forEachInRange(rowStart, rowEnd, callback); + }, + + getColumnGroupLevels: function () { + return this._columns.getGroupLevels(); + }, + + getRowGroupLevels: function () { + return this._rows.getGroupLevels(); + }, + + getColumnGroupsDataInView: function () { + return this._columns.getGroupsDataInView(); + }, + + getRowGroupsDataInView: function () { + return this._rows.getGroupsDataInView(); + }, + + _testValidity: function (sheetGeomJSON, checkCompleteness) { + + if (!sheetGeomJSON.hasOwnProperty('commandName')) { + console.error(this._unoCommand + ' response has no property named "commandName".'); + return false; + } + + if (sheetGeomJSON.commandName !== this._unoCommand) { + console.error('JSON response has wrong commandName: ' + + sheetGeomJSON.commandName + ' expected: ' + + this._unoCommand); + return false; + } + + if (typeof sheetGeomJSON.maxtiledcolumn !== 'string' || + !/^\d+$/.test(sheetGeomJSON.maxtiledcolumn)) { + console.error('JSON is missing/unreadable maxtiledcolumn property'); + return false; + } + + if (typeof sheetGeomJSON.maxtiledrow !== 'string' || + !/^\d+$/.test(sheetGeomJSON.maxtiledrow)) { + console.error('JSON is missing/unreadable maxtiledrow property'); + return false; + } + + if (checkCompleteness) { + + if (!sheetGeomJSON.hasOwnProperty('rows') || + !sheetGeomJSON.hasOwnProperty('columns')) { + + console.error(this._unoCommand + ' response is incomplete.'); + return false; + } + + if (typeof sheetGeomJSON.rows !== 'object' || + typeof sheetGeomJSON.columns !== 'object') { + + console.error(this._unoCommand + ' response has invalid rows/columns children.'); + return false; + } + + var expectedFields = ['sizes', 'hidden', 'filtered']; + for (var idx = 0; idx < expectedFields.length; idx++) { + + var fieldName = expectedFields[idx]; + var encodingForCols = sheetGeomJSON.columns[fieldName]; + var encodingForRows = sheetGeomJSON.rows[fieldName]; + + // Don't accept empty string or any other types. + if (typeof encodingForRows !== 'string' || !encodingForRows) { + console.error(this._unoCommand + ' response has invalid value for rows.' + + fieldName); + return false; + } + + // Don't accept empty string or any other types. + if (typeof encodingForCols !== 'string' || !encodingForCols) { + console.error(this._unoCommand + ' response has invalid value for columns.' + + fieldName); + return false; + } + } + } + + return true; + } +}); + +// Used to represent/query geometry data about either rows or columns. +L.SheetDimension = L.Class.extend({ + + initialize: function () { + + this._sizes = new L.SpanList(); + this._hidden = new L.BoolSpanList(); + this._filtered = new L.BoolSpanList(); + this._groups = new L.DimensionOutlines(); + + // This is used to store the span-list of sizes + // with hidden/filtered elements set to zero size. + // This needs to be updated whenever + // this._sizes/this._hidden/this._filtered are modified. + this._visibleSizes = undefined; + }, + + update: function (jsonObject) { + + if (typeof jsonObject !== 'object') { + return false; + } + + var regenerateVisibleSizes = false; + var loadsOK = true; + if (jsonObject.hasOwnProperty('sizes')) { + loadsOK = this._sizes.load(jsonObject.sizes); + regenerateVisibleSizes = true; + } + + if (jsonObject.hasOwnProperty('hidden')) { + var thisLoadOK = this._hidden.load(jsonObject.hidden); + loadsOK = loadsOK && thisLoadOK; + regenerateVisibleSizes = true; + } + + if (jsonObject.hasOwnProperty('filtered')) { + thisLoadOK = this._filtered.load(jsonObject.filtered); + loadsOK = loadsOK && thisLoadOK; + regenerateVisibleSizes = true; + } + + if (jsonObject.hasOwnProperty('groups')) { + thisLoadOK = this._groups.load(jsonObject.groups); + loadsOK = loadsOK && thisLoadOK; + } + + if (loadsOK && regenerateVisibleSizes) { + this._updateVisible(); + } + + return loadsOK; + }, + + setMaxIndex: function (maxIndex) { + this._maxIndex = maxIndex; + }, + + setTileGeometryData: function (tileSizeTwips, tileSizeCSSPixels, dpiScale, updatePositions) { + if (updatePositions === undefined) { + updatePositions = true; + } + + this._twipsPerCSSPixel = tileSizeTwips / tileSizeCSSPixels; + this._devPixelsPerCssPixel = dpiScale; + + if (updatePositions) { + // We need to compute positions data for every zoom change. + this._updatePositions(); + } + }, + + _updateVisible: function () { + + var invisibleSpanList = this._hidden.union(this._filtered); // this._hidden is not modified. + this._visibleSizes = this._sizes.applyZeroValues(invisibleSpanList); // this._sizes is not modified. + this._updatePositions(); + }, + + _updatePositions: function() { + + var posDevPx = 0; // position in device pixels. + var dimensionObj = this; + this._visibleSizes.addCustomDataForEachSpan(function ( + index, + size, /* size in twips of one element in the span */ + spanLength /* #elements in the span */) { + + // Important: rounding needs to be done in device pixels exactly like the core. + var sizeDevPxOne = Math.floor(size / dimensionObj._twipsPerCSSPixel * dimensionObj._devPixelsPerCssPixel); + posDevPx += (sizeDevPxOne * spanLength); + var posCssPx = posDevPx / dimensionObj._devPixelsPerCssPixel; + // position in device-pixel aligned twips. + var posTileTwips = Math.floor(posCssPx * dimensionObj._twipsPerCSSPixel); + + var customData = { + sizedev: sizeDevPxOne, + posdevpx: posDevPx, + poscsspx: posCssPx, + postiletwips: posTileTwips + }; + + return customData; + }); + }, + + getElementData: function (index) { + var span = this._visibleSizes.getSpanDataByIndex(index); + if (span === undefined) { + return undefined; + } + + return this._getElementDataFromSpanByIndex(index, span); + }, + + _getElementDataFromSpanByIndex: function (index, span) { + if (span === undefined || index < span.start || span.end < index) { + return undefined; + } + + var numSizes = span.end - index + 1; + // all in css pixels. + return { + startpos: (span.data.posdevpx - span.data.sizedev * numSizes) / this._devPixelsPerCssPixel, + size: span.data.sizedev / this._devPixelsPerCssPixel + }; + }, + + forEachInRange: function (start, end, callback) { + + var dimensionObj = this; + this._visibleSizes.forEachSpanInRange(start, end, function (span) { + var first = Math.max(span.start, start); + var last = Math.min(span.end, end); + for (var index = first; index <= last; ++index) { + callback(index, dimensionObj._getElementDataFromSpanByIndex(index, span)); + } + }); + }, + + _getIndexFromTileTwipsPos: function (pos) { + var span = this._visibleSizes.getSpanDataByCustomDataField(pos, 'postiletwips'); + var elementCount = span.end - span.start + 1; + var posStart = ((span.data.posdevpx - span.data.sizedev * elementCount) / + this._devPixelsPerCssPixel * this._twipsPerCSSPixel); + var posEnd = span.data.postiletwips; + var sizeOne = (posEnd - posStart) / elementCount; + var relativeIndex = Math.round((pos - posStart) / sizeOne); + + return span.start + relativeIndex; + }, + + setViewLimits: function (startPosTileTwips, endPosTileTwips) { + + // Extend the range a bit, to compensate for rounding errors. + this._viewStartIndex = Math.max(0, this._getIndexFromTileTwipsPos(startPosTileTwips) - 2); + this._viewEndIndex = Math.min(this._maxIndex, this._getIndexFromTileTwipsPos(endPosTileTwips) + 2); + }, + + getViewElementRange: function () { + return { + start: this._viewStartIndex, + end: this._viewEndIndex + }; + }, + + getGroupLevels: function () { + return this._outlines.getLevels(); + }, + + getGroupsDataInView: function () { + var groupsData = []; + var levels = this._outlines.getLevels(); + if (!levels) { + return groupsData; + } + + var dimensionObj = this; + this._outlines.forEachGroupInRange(this._viewStartIndex, this._viewEndIndex, + function (levelIdx, groupIdx, start, end, hidden) { + + var startElementData = dimensionObj.getElementData(start, true /* device pixels */); + var endElementData = dimensionObj.getElementData(end, true /* device pixels */); + groupsData.push({ + level: (levelIdx + 1).toString(), + index: groupIdx.toString(), + startPos: startElementData.startpos.toString(), + endPos: (endElementData.startpos + endElementData.size).toString(), + hidden: hidden ? '1' : '0' + }); + }); + + return groupsData; + }, + + getMaxIndex: function () { + return this._maxIndex; + } +}); + +L.SpanList = L.Class.extend({ + + initialize: function (encoding) { + + // spans are objects with keys: 'index' and 'value'. + // 'index' holds the last element of the span. + // Optionally custom data of a span can be added + // under the key 'data' via addCustomDataForEachSpan. + this._spanlist = []; + if (typeof encoding !== 'string') { + return; + } + + this.load(encoding); + }, + + load: function (encoding) { + + if (typeof encoding !== 'string') { + return false; + } + + var result = parseSpanListEncoding(encoding, false /* boolean value ? */); + if (result === undefined) { + return false; + } + + this._spanlist = result.spanlist; + return true; + }, + + // Runs in O(#spans in 'this' + #spans in 'other') + applyZeroValues: function (other) { + + if (!(other instanceof L.BoolSpanList)) { + return undefined; + } + + // Ensure both spanlists have the same total range. + if (this._spanlist[this._spanlist.length - 1].index !== other._spanlist[other._spanlist.length - 1]) { + return undefined; + } + + var maxElement = this._spanlist[this._spanlist.length - 1].index; + var result = new L.SpanList(); + + var thisIdx = 0; + var otherIdx = 0; + var zeroBit = other._startBit; + var resultValue = zeroBit ? 0 : this._spanlist[thisIdx].value; + + while (thisIdx < this._spanlist.length && otherIdx < other._spanlist.length) { + + // end elements of the current spans of 'this' and 'other'. + var thisElement = this._spanlist[thisIdx].index; + var otherElement = other._spanlist[otherIdx]; + + var lastElement = otherElement; + if (thisElement < otherElement) { + lastElement = thisElement; + ++thisIdx; + } + else if (otherElement < thisElement) { + zeroBit = !zeroBit; + ++otherIdx; + } + else { // both elements are equal. + zeroBit = !zeroBit; + ++thisIdx; + ++otherIdx; + } + + var nextResultValue = resultValue; + if (thisIdx < this._spanlist.length) { + nextResultValue = zeroBit ? 0 : this._spanlist[thisIdx].value; + } + + if (resultValue != nextResultValue || lastElement >= maxElement) { + // In the result spanlist a new span start from lastElement+1 + // or reached the maximum possible element. + result._spanlist.push({index: lastElement, value: resultValue}); + resultValue = nextResultValue; + } + } + + return result; + }, + + addCustomDataForEachSpan: function (getCustomDataCallback) { + + var prevIndex = -1; + this._spanlist.forEach(function (span) { + span.data = getCustomDataCallback( + span.index, span.value, + span.index - prevIndex); + prevIndex = span.index; + }); + }, + + getSpanDataByIndex: function (index) { + var spanid = this._searchByIndex(index); + if (spanid == -1) { + return undefined; + } + + return this._getSpanData(spanid); + }, + + getSpanDataByCustomDataField: function (value, fieldName) { + var spanid = this._searchByCustomDataField(value, fieldName); + if (spanid == -1) { + return undefined; + } + + return this._getSpanData(spanid); + }, + + forEachSpanInRange: function (start, end, callback) { + + if (start > end) { + return; + } + + var startId = this._searchByIndex(start); + var endId = this._searchByIndex(end); + + if (startId == -1 || endId == -1) { + return; + } + + for (var id = startId; id <= endId; ++id) { + callback(this._getSpanData(id)); + } + }, + + _getSpanData: function (spanid) { + + var span = this._spanlist[spanid]; + var dataClone = undefined; + if (span.data) { + dataClone = {}; + Object.keys(span.data).forEach(function (key) { + dataClone[key] = span.data[key]; + }); + } + + return { + start: spanid ? this._spanlist[spanid - 1].index + 1 : 0, + end: span.index, + size: span.value, + data: dataClone + }; + }, + + _searchByIndex: function (index) { + + if (index < 0 || index > this._spanlist[this._spanlist.length - 1].index) { + return -1; + } + + var start = 0; + var end = this._spanlist.length - 1; + var mid = -1; + while (start <= end) { + mid = Math.round((start + end) / 2); + var spanstart = mid ? this._spanlist[mid - 1].index + 1 : 0; + var spanend = this._spanlist[mid].index; + if (spanstart <= index && index <= spanend) { + break; + } + + if (index < spanstart) { + end = mid - 1; + } + else { // spanend < index + start = mid + 1; + } + } + + return mid; + }, + + _searchByCustomDataField: function (value, fieldName) { + + // All custom searchable data values are assumed to start from 0 at the start of first span. + var maxValue = this._spanlist[this._spanlist.length - 1].data[fieldName]; + if (value < 0 || value > maxValue) { + return -1; + } + + var start = 0; + var end = this._spanlist.length - 1; + var mid = -1; + while (start <= end) { + mid = Math.round((start + end) / 2); + var valuestart = mid ? this._spanlist[mid - 1].data[fieldName] + 1 : 0; + var valueend = this._spanlist[mid].data[fieldName]; + if (valuestart <= value && value <= valueend) { + break; + } + + if (value < valuestart) { + end = mid - 1; + } + else { // valueend < value + start = mid + 1; + } + } + + // may fail for custom data ? + return (start <= end) ? mid : -1; + } + +}); + +L.BoolSpanList = L.SpanList.extend({ + + load: function (encoding) { + + if (typeof encoding !== 'string') { + return false; + } + + var result = parseSpanListEncoding(encoding, true /* boolean value ? */); + if (result === undefined) { + return false; + } + + this._spanlist = result.spanlist; + this._startBit = result.startBit; + return true; + }, + + // Runs in O(#spans in 'this' + #spans in 'other') + union: function (other) { + + if (!(other instanceof L.BoolSpanList)) { + return undefined; + } + + // Ensure both spanlists have the same total range. + if (this._spanlist[this._spanlist.length - 1] !== other._spanlist[other._spanlist.length - 1]) { + return undefined; + } + + var maxElement = this._spanlist[this._spanlist.length - 1]; + + var result = new L.BoolSpanList(); + var thisBit = this._startBit; + var otherBit = other._startBit; + var resultBit = thisBit || otherBit; + result._startBit = resultBit; + + var thisIdx = 0; + var otherIdx = 0; + + while (thisIdx < this._spanlist.length && otherIdx < other._spanlist.length) { + + // end elements of the current spans of 'this' and 'other'. + var thisElement = this._spanlist[thisIdx]; + var otherElement = other._spanlist[otherIdx]; + + var lastElement = otherElement; + if (thisElement < otherElement) { + lastElement = thisElement; + thisBit = !thisBit; + ++thisIdx; + } + else if (otherElement < thisElement) { + otherBit = !otherBit; + ++otherIdx; + } + else { // both elements are equal. + thisBit = !thisBit; + otherBit = !otherBit; + ++thisIdx; + ++otherIdx; + } + + var nextResultBit = (thisBit || otherBit); + if (resultBit != nextResultBit || lastElement >= maxElement) { + // In the result spanlist a new span start from lastElement+1 + // or reached the maximum possible element. + result._spanlist.push(lastElement); + resultBit = nextResultBit; + } + } + + return result; + } +}); + +function parseSpanListEncoding(encoding, booleanValue) { + + var spanlist = []; + var splits = encoding.split(' '); + if (splits.length < 2) { + return undefined; + } + + var startBit = false; + if (booleanValue) { + var parts = splits[0].split(':'); + if (parts.length != 2) { + return undefined; + } + startBit = parseInt(parts[0]); + var first = parseInt(parts[1]); + if (isNaN(startBit) || isNaN(first)) { + return undefined; + } + spanlist.push(first); + } + + startBit = Boolean(startBit); + + for (var idx = 0; idx < splits.length - 1; ++idx) { + + if (booleanValue) { + if (!idx) { + continue; + } + + var entry = parseInt(splits[idx]); + if (isNaN(entry)) { + return undefined; + } + + spanlist.push(entry); + continue; + } + + var spanParts = splits[idx].split(':'); + if (spanParts.length != 2) { + return undefined; + } + + var span = { + index: parseInt(spanParts[1]), + value: parseInt(spanParts[0]) + }; + + if (isNaN(span.index) || isNaN(span.value)) { + return undefined; + } + + spanlist.push(span); + } + + var result = {spanlist: spanlist}; + + if (booleanValue) { + result.startBit = startBit; + } + + return result; +} + +L.DimensionOutlines = L.Class.extend({ + + initialize: function (encoding) { + + this._outlines = []; + if (typeof encoding !== 'string') { + return; + } + + this.load(encoding); + }, + + load: function (encoding) { + + if (typeof encoding !== 'string') { + return false; + } + + var levels = encoding.split(' '); + if (levels.length < 2) { + // No outline. + return true; + } + + var outlines = []; + + for (var levelIdx = 0; levelIdx < levels.length - 1; ++levelIdx) { + var collectionSplits = levels[levelIdx].split(','); + var collections = []; + if (collectionSplits.length < 2) { + return false; + } + + for (var collIdx = 0; collIdx < collectionSplits.length - 1; ++collIdx) { + var entrySplits = collectionSplits[collIdx].split(':'); + if (entrySplits.length < 4) { + return false; + } + + var olineEntry = { + start: parseInt(entrySplits[0]), + size: parseInt(entrySplits[1]), + hidden: parseInt(entrySplits[2]), + visible: parseInt(entrySplits[3]) + }; + + if (isNaN(olineEntry.start) || isNaN(olineEntry.size) || + isNaN(olineEntry.hidden) || isNaN(olineEntry.visible)) { + return false; + } + + collections.push(olineEntry); + } + + outlines.push(collections); + } + + this._outlines = outlines; + return true; + } +}); _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits