loleaflet/Makefile.am | 1 loleaflet/css/leaflet.css | 8 loleaflet/src/layer/tile/CanvasTileLayer.js | 1332 ++++++++++++++++++++++++++++ 3 files changed, 1341 insertions(+)
New commits: commit 4b2ff56750ff11ae421572e4bceae273c540820f Author: Dennis Francis <[email protected]> AuthorDate: Tue Jul 7 13:20:34 2020 +0530 Commit: Dennis Francis <[email protected]> CommitDate: Wed Jul 8 16:50:42 2020 +0200 introduce CanvasTileLayer with support for split-panes which is derived from TileLayer but renders tiles on a canvas instead. TODO: Generalize certain methods of TileLayer instead of a complete re-implementation in CanvasTileLayer just because a few things need to change. Change-Id: I51001f83f8f663d194bc9c4b018fa9950c40f420 Reviewed-on: https://gerrit.libreoffice.org/c/online/+/98351 Tested-by: Jenkins Tested-by: Jenkins CollaboraOffice <[email protected]> Reviewed-by: Dennis Francis <[email protected]> diff --git a/loleaflet/Makefile.am b/loleaflet/Makefile.am index 49faf56f1..2b2dd22c8 100644 --- a/loleaflet/Makefile.am +++ b/loleaflet/Makefile.am @@ -201,6 +201,7 @@ LOLEAFLET_JS =\ src/layer/Layer.js \ src/layer/tile/GridLayer.js \ src/layer/tile/TileLayer.js \ + src/layer/tile/CanvasTileLayer.js \ src/layer/SplitPanesContext.js \ src/layer/tile/TileLayer.TableOverlay.js \ src/layer/ObjectFocusDarkOverlay.js \ diff --git a/loleaflet/css/leaflet.css b/loleaflet/css/leaflet.css index ccf881de2..2ea1f74ea 100644 --- a/loleaflet/css/leaflet.css +++ b/loleaflet/css/leaflet.css @@ -7,6 +7,7 @@ .leaflet-tile-container, .leaflet-map-pane svg, .leaflet-map-pane canvas, +.leaflet-canvas-container canvas .leaflet-zoom-box, .leaflet-image-layer, .leaflet-layer { @@ -993,3 +994,10 @@ input.clipboard { top: 24px; color: white; } + +.leaflet-canvas-container { + position: absolute; + left: 0; + top: 0; + z-index: 9; + } diff --git a/loleaflet/src/layer/tile/CanvasTileLayer.js b/loleaflet/src/layer/tile/CanvasTileLayer.js new file mode 100644 index 000000000..a9df21726 --- /dev/null +++ b/loleaflet/src/layer/tile/CanvasTileLayer.js @@ -0,0 +1,1332 @@ +/* -*- js-indent-level: 8 -*- */ +/* + * L.CanvasTileLayer is a L.TileLayer with canvas based rendering. + */ + +L.TileCoordData = L.Class.extend({ + + initialize: function (left, top, zoom, part) { + this.x = left; + this.y = top; + this.z = zoom; + this.part = part; + }, + + getPos: function () { + return new L.Point(this.x, this.y); + }, + + key: function () { + return this.x + ':' + this.y + ':' + this.z + ':' + this.part; + }, + + toString: function () { + return '{ left : ' + this.x + ', top : ' + this.y + + ', z : ' + this.z + ', part : ' + this.part + ' }'; + } +}); + +L.TileCoordData.parseKey = function (keyString) { + + console.assert(typeof keyString === 'string', 'key should be a string'); + var k = keyString.split(':'); + console.assert(k.length >= 4, 'invalid key format'); + return new L.TileCoordData(+k[0], +k[1], +k[2], +k[3]); +}; + +L.CanvasTilePainter = L.Class.extend({ + + options: { + debug: false, + }, + + initialize: function (layer, dpiScale, enableImageSmoothing) { + this._layer = layer; + this._canvas = this._layer._canvas; + this._splitPanesContext = this._layer.getSplitPanesContext(); + + if (dpiScale === 1 || dpiScale === 2) { + enableImageSmoothing = (enableImageSmoothing === true); + } + else { + enableImageSmoothing = (enableImageSmoothing === undefined || enableImageSmoothing); + } + + this._dpiScale = dpiScale; + + this._map = this._layer._map; + this._setupCanvas(enableImageSmoothing); + + this._topLeft = undefined; + this._lastZoom = undefined; + this._lastPart = undefined; + this._splitPos = this._splitPanesContext ? + this._splitPanesContext.getSplitPos() : new L.Point(0, 0); + + this._tileSizeCSSPx = undefined; + this._updatesRunning = false; + }, + + isUpdatesRunning: function () { + return this._updatesRunning; + }, + + startUpdates: function () { + if (this._updatesRunning === true) { + return false; + } + + this._updatesRunning = true; + this._updateWithRAF(); + return true; + }, + + stopUpdates: function () { + if (this._updatesRunning) { + L.Util.cancelAnimFrame(this._canvasRAF); + this.update(); + return true; + } + + return false; + }, + + dispose: function () { + this.stopUpdates(); + }, + + setImageSmoothing: function (enable) { + this._canvasCtx.imageSmoothingEnabled = enable; + this._canvasCtx.msImageSmoothingEnabled = enable; + }, + + _setupCanvas: function (enableImageSmoothing) { + console.assert(this._canvas, 'no canvas element'); + this._canvasCtx = this._canvas.getContext('2d', { alpha: false }); + this.setImageSmoothing(enableImageSmoothing); + var mapSize = this._map.getPixelBounds().getSize(); + this._lastSize = mapSize; + this._setCanvasSize(mapSize.x, mapSize.y); + }, + + _setCanvasSize: function (widthCSSPx, heightCSSPx) { + this._canvas.style.width = widthCSSPx + 'px'; + this._canvas.style.height = heightCSSPx + 'px'; + this._canvas.width = Math.floor(widthCSSPx * this._dpiScale); + this._canvas.height = Math.floor(heightCSSPx * this._dpiScale); + + this._width = parseInt(this._canvas.style.width); + this._height = parseInt(this._canvas.style.height); + this.clear(); + }, + + clear: function () { + this._canvasCtx.save(); + this._canvasCtx.scale(this._dpiScale, this._dpiScale); + this._canvasCtx.fillStyle = 'white'; + this._canvasCtx.fillRect(0, 0, this._width, this._height); + this._canvasCtx.restore(); + }, + + paint: function (tile, viewBounds, paneBoundsList) { + + if (this._tileSizeCSSPx === undefined) { + this._tileSizeCSSPx = this._layer._getTileSize(); + } + + var tileTopLeft = tile.coords.getPos(); + var tileSize = new L.Point(this._tileSizeCSSPx, this._tileSizeCSSPx); + var tileBounds = new L.Bounds(tileTopLeft, tileTopLeft.add(tileSize)); + + viewBounds = viewBounds || this._map.getPixelBounds(); + paneBoundsList = paneBoundsList || ( + this._splitPanesContext ? + this._splitPanesContext.getPxBoundList(viewBounds) : + [viewBounds] + ); + + for (var i = 0; i < paneBoundsList.length; ++i) { + var paneBounds = paneBoundsList[i]; + if (!paneBounds.intersects(tileBounds)) { + continue; + } + + var topLeft = paneBounds.getTopLeft(); + if (topLeft.x) { + topLeft.x = viewBounds.min.x; + } + + if (topLeft.y) { + topLeft.y = viewBounds.min.y; + } + + this._canvasCtx.save(); + this._canvasCtx.scale(this._dpiScale, this._dpiScale); + this._canvasCtx.translate(-topLeft.x, -topLeft.y); + + // create a clip for the pane/view. + this._canvasCtx.beginPath(); + var paneSize = paneBounds.getSize(); + this._canvasCtx.rect(paneBounds.min.x, paneBounds.min.y, paneSize.x + 1, paneSize.y + 1); + this._canvasCtx.clip(); + + if (this._dpiScale !== 1) { + // FIXME: avoid this scaling when possible (dpiScale = 2). + this._canvasCtx.drawImage(tile.el, tile.coords.x, tile.coords.y, this._tileSizeCSSPx, this._tileSizeCSSPx); + } + else { + this._canvasCtx.drawImage(tile.el, tile.coords.x, tile.coords.y); + } + this._canvasCtx.restore(); + } + }, + + _drawSplits: function () { + if (!this._splitPanesContext) { + return; + } + var splitPos = this._splitPanesContext.getSplitPos(); + this._canvasCtx.save(); + this._canvasCtx.scale(this._dpiScale, this._dpiScale); + this._canvasCtx.strokeStyle = 'red'; + this._canvasCtx.strokeRect(0, 0, splitPos.x, splitPos.y); + this._canvasCtx.restore(); + }, + + _updateWithRAF: function () { + // update-loop with requestAnimationFrame + this._canvasRAF = L.Util.requestAnimFrame(this._updateWithRAF, this, false /* immediate */); + this.update(); + }, + + update: function () { + + var zoom = Math.round(this._map.getZoom()); + var pixelBounds = this._map.getPixelBounds(); + var newSize = pixelBounds.getSize(); + var newTopLeft = pixelBounds.getTopLeft(); + var part = this._layer._selectedPart; + var newSplitPos = this._splitPanesContext ? + this._splitPanesContext.getSplitPos(): this._splitPos; + + var zoomChanged = (zoom !== this._lastZoom); + var partChanged = (part !== this._lastPart); + var sizeChanged = !newSize.equals(this._lastSize); + var splitPosChanged = !newSplitPos.equals(this._splitPos); + + var skipUpdate = ( + this._topLeft !== undefined && + !zoomChanged && + !partChanged && + !sizeChanged && + !splitPosChanged && + newTopLeft.equals(this._topLeft)); + + if (skipUpdate) { + return; + } + + if (sizeChanged) { + this._setCanvasSize(newSize.x, newSize.y); + this._lastSize = newSize; + } + + if (splitPosChanged) { + this._splitPos = newSplitPos; + } + + // TODO: fix _shiftAndPaint for high DPI. + var shiftPaintDisabled = true; + var fullRepaintNeeded = zoomChanged || partChanged || sizeChanged || shiftPaintDisabled; + + this._lastZoom = zoom; + this._lastPart = part; + + if (fullRepaintNeeded) { + + this._topLeft = newTopLeft; + this._paintWholeCanvas(); + + if (this.options.debug) { + this._drawSplits(); + } + + return; + } + + this._shiftAndPaint(newTopLeft); + }, + + _shiftAndPaint: function (newTopLeft) { + + console.assert(!this._splitPanesContext, '_shiftAndPaint is broken for split-panes.'); + var offset = new L.Point(this._width - 1, this._height - 1); + + var dx = newTopLeft.x - this._topLeft.x; + var dy = newTopLeft.y - this._topLeft.y; + if (!dx && !dy) { + return; + } + + // Determine the area that needs to be painted as max. two disjoint rectangles. + var rectsToPaint = []; + this._inMove = true; + var oldTopLeft = this._topLeft; + var oldBottomRight = oldTopLeft.add(offset); + var newBottomRight = newTopLeft.add(offset); + + if (Math.abs(dx) < this._width && Math.abs(dy) < this._height) { + + this._canvasCtx.save(); + this._canvasCtx.scale(this._dpiScale, this._dpiScale); + this._canvasCtx.globalCompositeOperation = 'copy'; + this._canvasCtx.drawImage(this._canvas, -dx, -dy); + this._canvasCtx.globalCompositeOperation = 'source-over'; + this._canvasCtx.restore(); + + var xstart = newTopLeft.x, xend = newBottomRight.x; + var ystart = newTopLeft.y, yend = newBottomRight.y; + if (dx) { + xstart = dx > 0 ? oldBottomRight.x + 1 : newTopLeft.x; + xend = xstart + Math.abs(dx) - 1; + } + + if (dy) { + ystart = dy > 0 ? oldBottomRight.y + 1 : newTopLeft.y; + yend = ystart + Math.abs(dy) - 1; + } + + // rectangle including the x-range that needs painting with full y-range. + // This will take care of simultaneous non-zero dx and dy. + if (dx) { + rectsToPaint.push(new L.Bounds( + new L.Point(xstart, newTopLeft.y), + new L.Point(xend, newBottomRight.y) + )); + } + + // rectangle excluding the x-range that needs painting + needed y-range. + if (dy) { + rectsToPaint.push(new L.Bounds( + new L.Point(dx > 0 ? newTopLeft.x : (dx ? xend + 1 : newTopLeft.x), ystart), + new L.Point(dx > 0 ? xstart - 1 : newBottomRight.x, yend) + )); + } + + } + else { + rectsToPaint.push(new L.Bounds(newTopLeft, newBottomRight)); + } + + this._topLeft = newTopLeft; + + this._paintRects(rectsToPaint, newTopLeft); + }, + + _paintRects: function (rects, topLeft) { + for (var i = 0; i < rects.length; ++i) { + this._paintRect(rects[i], topLeft); + } + }, + + _paintRect: function (rect) { + var zoom = this._lastZoom || Math.round(this._map.getZoom()); + var part = this._lastPart || this._layer._selectedPart; + var tileRange = this._layer._pxBoundsToTileRange(rect); + var tileSize = this._tileSizeCSSPx || this._layer._getTileSize(); + for (var j = tileRange.min.y; j <= tileRange.max.y; ++j) { + for (var i = tileRange.min.x; i <= tileRange.max.x; ++i) { + var coords = new L.TileCoordData( + i * tileSize, + j * tileSize, + zoom, + part); + + var key = coords.key(); + var tile = this._layer._tiles[key]; + var invalid = tile && tile._invalidCount && tile._invalidCount > 0; + if (tile && tile.loaded && !invalid) { + this.paint(tile); + } + } + } + }, + + _paintWholeCanvas: function () { + var zoom = this._lastZoom || Math.round(this._map.getZoom()); + var part = this._lastPart || this._layer._selectedPart; + + var viewSize = new L.Point(this._width, this._height); + var viewBounds = new L.Bounds(this._topLeft, this._topLeft.add(viewSize)); + + // Calculate all this here intead of doing it per tile. + var paneBoundsList = this._splitPanesContext ? + this._splitPanesContext.getPxBoundList(viewBounds) : [viewBounds]; + var tileRanges = paneBoundsList.map(this._layer._pxBoundsToTileRange, this._layer); + + var tileSize = this._tileSizeCSSPx || this._layer._getTileSize(); + + for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) { + var tileRange = tileRanges[rangeIdx]; + for (var j = tileRange.min.y; j <= tileRange.max.y; ++j) { + for (var i = tileRange.min.x; i <= tileRange.max.x; ++i) { + var coords = new L.TileCoordData( + i * tileSize, + j * tileSize, + zoom, + part); + + var key = coords.key(); + var tile = this._layer._tiles[key]; + var invalid = tile && tile._invalidCount && tile._invalidCount > 0; + if (tile && tile.loaded && !invalid) { + this.paint(tile, viewBounds, paneBoundsList); + } + } + } + } + }, +}); + +L.CanvasTileLayer = L.TileLayer.extend({ + + _initContainer: function () { + if (this._canvasContainer) { + console.error('called _initContainer() when this._canvasContainer is present!'); + } + + L.TileLayer.prototype._initContainer.call(this); + + var mapContainer = this._map.getContainer(); + var canvasContainerClass = 'leaflet-canvas-container'; + this._canvasContainer = L.DomUtil.create('div', canvasContainerClass, mapContainer); + this._setup(); + }, + + _setup: function () { + + if (!this._canvasContainer) { + console.error('canvas container not found. _initContainer failed ?'); + } + + this._canvas = L.DomUtil.create('canvas', '', this._canvasContainer); + this._painter = new L.CanvasTilePainter(this, L.getDpiScaleFactor()); + + // FIXME: A hack to get the Hammer events from the pane, which does not happen with + // no tile img's in it. + this._container.style.width = this._canvas.style.width; + this._container.style.height = this._canvas.style.height; + this._container.style.position = 'absolute'; + + if (L.Browser.cypressTest) { + this._cypressHelperDiv = L.DomUtil.create('div', '', this._container); + } + + this._map.on('movestart', this._painter.startUpdates, this._painter); + this._map.on('moveend', this._painter.stopUpdates, this._painter); + this._map.on('zoomend', this._painter.update, this._painter); + this._map.on('splitposchanged', this._painter.update, this._painter); + }, + + hasSplitPanesSupport: function () { + // Only enabled for Calc for now + // It may work without this.options.sheetGeometryDataEnabled but not tested. + // The overlay-pane with split-panes is still based on svg renderer, + // and not available for VML or canvas yet. + if (this.isCalc() && + this.options.sheetGeometryDataEnabled && + L.Browser.svg) { + return true; + } + + return false; + }, + + onRemove: function (map) { + this._painter.dispose(); + L.TileLayer.prototype.onRemove.call(this, map); + this._removeSplitters(); + L.DomUtil.remove(this._canvasContainer); + }, + + getEvents: function () { + var events = { + viewreset: this._viewReset, + movestart: this._moveStart, + moveend: this._move, + splitposchanged: this._move, + }; + + if (!this.options.updateWhenIdle) { + // update tiles on move, but not more often than once per given interval + events.move = L.Util.throttle(this._move, this.options.updateInterval, this); + } + + if (this._zoomAnimated) { + events.zoomanim = this._animateZoom; + } + + return events; + }, + + _removeSplitters: function () { + var map = this._map; + if (this._xSplitter) { + map.removeLayer(this._xSplitter); + this._xSplitter = undefined; + } + + if (this._ySplitter) { + map.removeLayer(this._ySplitter); + this._ySplitter = undefined; + } + }, + + _updateOpacity: function () { + this._pruneTiles(); + }, + + _updateLevels: function () { + }, + + _initTile: function () { + }, + + _pruneTiles: function () { + var key, tile; + + for (key in this._tiles) { + tile = this._tiles[key]; + tile.retain = tile.current; + } + + for (key in this._tiles) { + tile = this._tiles[key]; + if (tile.current && !tile.active) { + var coords = tile.coords; + if (!this._retainParent(coords.x, coords.y, coords.z, coords.part, coords.z - 5)) { + this._retainChildren(coords.x, coords.y, coords.z, coords.part, coords.z + 2); + } + } + } + + for (key in this._tiles) { + if (!this._tiles[key].retain) { + this._removeTile(key); + } + } + }, + + _animateZoom: function () { + }, + + _setZoomTransforms: function () { + }, + + _setZoomTransform: function () { + }, + + _getTilePos: function (coords) { + return coords.getPos(); + }, + + _wrapCoords: function (coords) { + return new L.TileCoordData( + this._wrapX ? L.Util.wrapNum(coords.x, this._wrapX) : coords.x, + this._wrapY ? L.Util.wrapNum(coords.y, this._wrapY) : coords.y, + coords.z, + coords.part); + }, + + _pxBoundsToTileRanges: function (bounds) { + if (!this._splitPanesContext) { + return [this._pxBoundsToTileRange(bounds)]; + } + + var boundList = this._splitPanesContext.getPxBoundList(bounds); + return boundList.map(this._pxBoundsToTileRange, this); + }, + + _pxBoundsToTileRange: function (bounds) { + return new L.Bounds( + bounds.min.divideBy(this._tileSize).floor(), + bounds.max.divideBy(this._tileSize).floor()); + }, + + _twipsToCoords: function (twips) { + return new L.TileCoordData( + Math.round(twips.x / twips.tileWidth) * this._tileSize, + Math.round(twips.y / twips.tileHeight) * this._tileSize); + }, + + _coordsToTwips: function (coords) { + return new L.Point( + Math.floor(coords.x / this._tileSize) * this._tileWidthTwips, + Math.floor(coords.y / this._tileSize) * this._tileHeightTwips); + }, + + _isValidTile: function (coords) { + if (coords.x < 0 || coords.y < 0) { + return false; + } + if ((coords.x / this._tileSize) * this._tileWidthTwips >= this._docWidthTwips || + (coords.y / this._tileSize) * this._tileHeightTwips >= this._docHeightTwips) { + return false; + } + return true; + }, + + _update: function (center, zoom) { + var map = this._map; + if (!map || this._documentInfo === '') { + return; + } + + if (center === undefined) { center = map.getCenter(); } + if (zoom === undefined) { zoom = Math.round(map.getZoom()); } + + var pixelBounds = map.getPixelBounds(center, zoom); + var tileRanges = this._pxBoundsToTileRanges(pixelBounds); + var queue = []; + + for (var key in this._tiles) { + var thiscoords = this._keyToTileCoords(key); + if (thiscoords.z !== zoom || + thiscoords.part !== this._selectedPart) { + this._tiles[key].current = false; + } + } + + // If there are panes that need new tiles for its entire area, cancel previous requests. + var cancelTiles = false; + var paneNewView; + // create a queue of coordinates to load tiles from + for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) { + paneNewView = true; + var tileRange = tileRanges[rangeIdx]; + for (var j = tileRange.min.y; j <= tileRange.max.y; ++j) { + for (var i = tileRange.min.x; i <= tileRange.max.x; ++i) { + var coords = new L.TileCoordData( + i * this._tileSize, + j * this._tileSize, + zoom, + this._selectedPart); + + if (!this._isValidTile(coords)) { continue; } + + key = this._tileCoordsToKey(coords); + var tile = this._tiles[key]; + var invalid = tile && tile._invalidCount && tile._invalidCount > 0; + if (tile && tile.loaded && !invalid) { + tile.current = true; + paneNewView = false; + } else if (invalid) { + tile._invalidCount = 1; + queue.push(coords); + } else { + queue.push(coords); + } + } + } + + if (paneNewView) { + cancelTiles = true; + } + } + + this._sendClientVisibleArea(true); + + this._sendClientZoom(true); + + if (queue.length !== 0) { + if (cancelTiles) { + // we know that a new set of tiles (that completely cover one/more panes) has been requested + // so we're able to cancel the previous requests that are being processed + this._cancelTiles(); + } + + // if its the first batch of tiles to load + if (this._noTilesToLoad()) { + this.fire('loading'); + } + + this._addTiles(queue); + } + }, + + _sendClientVisibleArea: function (forceUpdate) { + + var splitPos = this._splitPanesContext ? this._splitPanesContext.getSplitPos() : new L.Point(0, 0); + + var visibleArea = this._map.getPixelBounds(); + visibleArea = new L.Bounds( + this._pixelsToTwips(visibleArea.min), + this._pixelsToTwips(visibleArea.max) + ); + splitPos = this._pixelsToTwips(splitPos); + var size = visibleArea.getSize(); + var visibleTopLeft = visibleArea.min; + var newClientVisibleArea = 'clientvisiblearea x=' + Math.round(visibleTopLeft.x) + + ' y=' + Math.round(visibleTopLeft.y) + + ' width=' + Math.round(size.x) + + ' height=' + Math.round(size.y) + + ' splitx=' + Math.round(splitPos.x) + + ' splity=' + Math.round(splitPos.y); + + if (this._clientVisibleArea !== newClientVisibleArea || forceUpdate) { + // Visible area is dirty, update it on the server + this._map._socket.sendMessage(newClientVisibleArea); + if (!this._map._fatal && this._map._active && this._map._socket.connected()) + this._clientVisibleArea = newClientVisibleArea; + if (this._debug) { + this._debugInfo.clearLayers(); + for (var key in this._tiles) { + this._tiles[key]._debugPopup = null; + this._tiles[key]._debugTile = null; + } + } + } + }, + + _updateOnChangePart: function () { + var map = this._map; + if (!map || this._documentInfo === '') { + return; + } + var key, coords, tile; + var center = map.getCenter(); + var zoom = Math.round(map.getZoom()); + + var pixelBounds = map.getPixelBounds(center, zoom); + var tileRanges = this._pxBoundsToTileRanges(pixelBounds); + var queue = []; + + for (key in this._tiles) { + var thiscoords = this._keyToTileCoords(key); + if (thiscoords.z !== zoom || + thiscoords.part !== this._selectedPart) { + this._tiles[key].current = false; + } + } + + // If there are panes that need new tiles for its entire area, cancel previous requests. + var cancelTiles = false; + var paneNewView; + // create a queue of coordinates to load tiles from + for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) { + paneNewView = true; + var tileRange = tileRanges[rangeIdx]; + for (var j = tileRange.min.y; j <= tileRange.max.y; j++) { + for (var i = tileRange.min.x; i <= tileRange.max.x; i++) { + coords = new L.TileCoordData( + i * this._tileSize, + j * this._tileSize, + zoom, + this._selectedPart); + + if (!this._isValidTile(coords)) { continue; } + + key = this._tileCoordsToKey(coords); + tile = this._tiles[key]; + if (tile) { + tile.current = true; + paneNewView = false; + } else { + queue.push(coords); + } + } + } + if (paneNewView) { + cancelTiles = true; + } + } + + if (queue.length !== 0) { + if (cancelTiles) { + // we know that a new set of tiles (that completely cover one/more panes) has been requested + // so we're able to cancel the previous requests that are being processed + this._cancelTiles(); + } + + // if its the first batch of tiles to load + if (this._noTilesToLoad()) { + this.fire('loading'); + } + + var tilePositionsX = ''; + var tilePositionsY = ''; + + for (i = 0; i < queue.length; i++) { + coords = queue[i]; + key = this._tileCoordsToKey(coords); + + if (coords.part === this._selectedPart) { + tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords)); + + // if createTile is defined with a second argument ("done" callback), + // we know that tile is async and will be ready later; otherwise + if (this.createTile.length < 2) { + // mark tile as ready, but delay one frame for opacity animation to happen + setTimeout(L.bind(this._tileReady, this, coords, null, tile), 0); + } + + // save tile in cache + this._tiles[key] = { + el: tile, + coords: coords, + current: true + }; + + this.fire('tileloadstart', { + tile: tile, + coords: coords + }); + } + + if (!this._tileCache[key]) { + var twips = this._coordsToTwips(coords); + if (tilePositionsX !== '') { + tilePositionsX += ','; + } + tilePositionsX += twips.x; + if (tilePositionsY !== '') { + tilePositionsY += ','; + } + tilePositionsY += twips.y; + } + else { + tile.src = this._tileCache[key]; + } + } + + if (tilePositionsX !== '' && tilePositionsY !== '') { + var message = 'tilecombine ' + + 'nviewid=0 ' + + 'part=' + this._selectedPart + ' ' + + 'width=' + this._tileWidthPx + ' ' + + 'height=' + this._tileHeightPx + ' ' + + 'tileposx=' + tilePositionsX + ' ' + + 'tileposy=' + tilePositionsY + ' ' + + 'tilewidth=' + this._tileWidthTwips + ' ' + + 'tileheight=' + this._tileHeightTwips; + + this._map._socket.sendMessage(message, ''); + } + + } + + if (typeof (this._prevSelectedPart) === 'number' && + this._prevSelectedPart !== this._selectedPart + && this._docType === 'spreadsheet') { + this._map.fire('updatescrolloffset', { x: 0, y: 0, updateHeaders: false }); + this._map.scrollTop(0); + this._map.scrollLeft(0); + } + }, + + _tileReady: function (coords, err, tile) { + if (!this._map) { return; } + + if (err) { + this.fire('tileerror', { + error: err, + tile: tile, + coords: coords + }); + } + + var key = this._tileCoordsToKey(coords); + + tile = this._tiles[key]; + if (!tile) { return; } + + tile.loaded = +new Date(); + tile.active = true; + + if (this._cypressHelperDiv) { + var container = this._cypressHelperDiv; + var newIndicator = L.DomUtil.create('div', 'leaflet-tile-loaded', this._cypressHelperDiv); + setTimeout(function () { + container.removeChild(newIndicator); + }, 1000); + } + + // paint this tile on canvas. + this._painter.paint(tile); + + if (this._noTilesToLoad()) { + this.fire('load'); + this._pruneTiles(); + } + }, + + _addTiles: function (coordsQueue) { + var coords, key; + // first take care of the DOM + for (var i = 0; i < coordsQueue.length; i++) { + coords = coordsQueue[i]; + + key = this._tileCoordsToKey(coords); + + if (coords.part === this._selectedPart) { + if (!this._tiles[key]) { + var tile = this.createTile(this._wrapCoords(coords), L.bind(this._tileReady, this, coords)); + + // if createTile is defined with a second argument ("done" callback), + // we know that tile is async and will be ready later; otherwise + if (this.createTile.length < 2) { + // mark tile as ready, but delay one frame for opacity animation to happen + setTimeout(L.bind(this._tileReady, this, coords, null, tile), 0); + } + + // save tile in cache + this._tiles[key] = { + el: tile, + coords: coords, + current: true + }; + + this.fire('tileloadstart', { + tile: tile, + coords: coords + }); + + if (tile && this._tileCache[key]) { + tile.src = this._tileCache[key]; + } + } + } + } + + // sort the tiles by the rows + coordsQueue.sort(function (a, b) { + if (a.y !== b.y) { + return a.y - b.y; + } else { + return a.x - b.x; + } + }); + + // try group the tiles into rectangular areas + var rectangles = []; + while (coordsQueue.length > 0) { + coords = coordsQueue[0]; + + // tiles that do not interest us + key = this._tileCoordsToKey(coords); + if (this._tileCache[key] || coords.part !== this._selectedPart) { + coordsQueue.splice(0, 1); + continue; + } + + var rectQueue = [coords]; + var bound = coords.getPos(); // L.Point + + // remove it + coordsQueue.splice(0, 1); + + // find the close ones + var rowLocked = false; + var hasHole = false; + i = 0; + while (i < coordsQueue.length) { + var current = coordsQueue[i]; + + // extend the bound vertically if possible (so far it was + // continuous) + if (!hasHole && (current.y === bound.y + this._tileSize)) { + rowLocked = true; + bound.y += this._tileSize; + } + + if (current.y > bound.y) { + break; + } + + if (!rowLocked) { + if (current.y === bound.y && current.x === bound.x + this._tileSize) { + // extend the bound horizontally + bound.x += this._tileSize; + rectQueue.push(current); + coordsQueue.splice(i, 1); + } else { + // ignore the rest of the row + rowLocked = true; + ++i; + } + } else if (current.x <= bound.x && current.y <= bound.y) { + // we are inside the bound + rectQueue.push(current); + coordsQueue.splice(i, 1); + } else { + // ignore this one, but there still may be other tiles + hasHole = true; + ++i; + } + } + + rectangles.push(rectQueue); + } + + var twips, msg; + for (var r = 0; r < rectangles.length; ++r) { + rectQueue = rectangles[r]; + var tilePositionsX = ''; + var tilePositionsY = ''; + for (i = 0; i < rectQueue.length; i++) { + coords = rectQueue[i]; + twips = this._coordsToTwips(coords); + + if (tilePositionsX !== '') { + tilePositionsX += ','; + } + tilePositionsX += twips.x; + + if (tilePositionsY !== '') { + tilePositionsY += ','; + } + tilePositionsY += twips.y; + } + + twips = this._coordsToTwips(coords); + msg = 'tilecombine ' + + 'nviewid=0 ' + + 'part=' + coords.part + ' ' + + 'width=' + this._tileWidthPx + ' ' + + 'height=' + this._tileHeightPx + ' ' + + 'tileposx=' + tilePositionsX + ' ' + + 'tileposy=' + tilePositionsY + ' ' + + 'tilewidth=' + this._tileWidthTwips + ' ' + + 'tileheight=' + this._tileHeightTwips; + this._map._socket.sendMessage(msg, ''); + } + }, + + _cancelTiles: function () { + this._map._socket.sendMessage('canceltiles'); + for (var key in this._tiles) { + var tile = this._tiles[key]; + // When _invalidCount > 0 the tile has been invalidated, however the new tile content + // has not yet been fetched and because of `canceltiles` message it will never be + // so we need to remove the tile, or when the tile is back inside the visible area + // its content would be the old invalidated one. Drop only those tiles which are not in + // the new visible area. + // example: a tile is invalidated but a sudden scroll to the cell cursor position causes + // to move the tile out of the visible area before the new content is fetched + + var dropTile = !tile.loaded; + var coords = tile.coords; + if (coords.part === this._selectedPart) { + + var tileBounds; + if (!this._splitPanesContext) { + var tileTopLeft = this._coordsToTwips(coords); + var tileBottomRight = new L.Point(this._tileWidthTwips, this._tileHeightTwips); + tileBounds = new L.Bounds(tileTopLeft, tileTopLeft.add(tileBottomRight)); + var visibleTopLeft = this._latLngToTwips(this._map.getBounds().getNorthWest()); + var visibleBottomRight = this._latLngToTwips(this._map.getBounds().getSouthEast()); + var visibleArea = new L.Bounds(visibleTopLeft, visibleBottomRight); + + dropTile |= (tile._invalidCount > 0 && !visibleArea.intersects(tileBounds)); + } + else + { + var tilePos = coords.getPos(); + tileBounds = new L.Bounds(tilePos, tilePos.add(new L.Point(this._tileSize, this._tileSize))); + dropTile |= (tile._invalidCount > 0 && + !this._splitPanesContext.intersectsVisible(tileBounds)); + } + } + else { + dropTile |= tile._invalidCount > 0; + } + + + if (dropTile) { + delete this._tiles[key]; + if (this._debug && this._debugDataCancelledTiles) { + this._debugCancelledTiles++; + this._debugDataCancelledTiles.setPrefix('Cancelled tiles: ' + this._debugCancelledTiles); + } + } + } + this._emptyTilesCount = 0; + }, + + _checkTileMsgObject: function (msgObj) { + if (typeof msgObj !== 'object' || + typeof msgObj.x !== 'number' || + typeof msgObj.y !== 'number' || + typeof msgObj.tileWidth !== 'number' || + typeof msgObj.tileHeight !== 'number' || + typeof msgObj.part !== 'number') { + console.error('Unexpected content in the parsed tile message.'); + } + }, + + _tileMsgToCoords: function (tileMsg) { + var coords = this._twipsToCoords(tileMsg); + coords.z = tileMsg.zoom; + coords.part = tileMsg.part; + return coords; + }, + + _tileCoordsToKey: function (coords) { + return coords.key(); + }, + + _keyToTileCoords: function (key) { + return L.TileCoordData.parseKey(key); + }, + + _keyToBounds: function (key) { + return this._tileCoordsToBounds(this._keyToTileCoords(key)); + }, + + _tileCoordsToBounds: function (coords) { + + var map = this._map; + var tileSize = this._getTileSize(); + + var nwPoint = new L.Point(coords.x, coords.y); + var sePoint = nwPoint.add([tileSize, tileSize]); + + var nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)); + var se = map.wrapLatLng(map.unproject(sePoint, coords.z)); + + return new L.LatLngBounds(nw, se); + }, + + _removeTile: function (key) { + var tile = this._tiles[key]; + if (!tile) { return; } + + // FIXME: this _tileCache is used for prev/next slide; but it is + // dangerous in connection with typing / invalidation + if (!(this._tiles[key]._invalidCount > 0)) { + this._tileCache[key] = tile.el.src; + } + + if (!tile.loaded && this._emptyTilesCount > 0) { + this._emptyTilesCount -= 1; + } + + if (this._debug && this._debugInfo && this._tiles[key]._debugPopup) { + this._debugInfo.removeLayer(this._tiles[key]._debugPopup); + } + delete this._tiles[key]; + + this.fire('tileunload', { + tile: tile.el, + coords: this._keyToTileCoords(key) + }); + }, + + _preFetchTiles: function () { + if (this._emptyTilesCount > 0 || !this._map) { + return; + } + var center = this._map.getCenter(); + var zoom = this._map.getZoom(); + var tilesToFetch = 10; + var maxBorderWidth = 5; + var tileBorderSrcs; + + if (this._map._permission === 'edit') { + tilesToFetch = 5; + maxBorderWidth = 3; + } + + if (!this._preFetchBorders) { + var pixelBounds = this._map.getPixelBounds(center, zoom); + tileBorderSrcs = this._pxBoundsToTileRanges(pixelBounds); + this._preFetchBorders = tileBorderSrcs; + } + else { + tileBorderSrcs = this._preFetchBorders; + } + + var queue = []; + var finalQueue = []; + var visitedTiles = {}; + var borderWidth = 0; + // don't search on a border wider than 5 tiles because it will freeze the UI + + for (var rangeIdx = 0; rangeIdx < tileBorderSrcs.length; ++rangeIdx) { + var tileBorder = new L.Bounds( + tileBorderSrcs[rangeIdx].min, + tileBorderSrcs[rangeIdx].max + ); + + while ((tileBorder.min.x >= 0 || tileBorder.min.y >= 0 || + tileBorder.max.x * this._tileWidthTwips < this._docWidthTwips || + tileBorder.max.y * this._tileHeightTwips < this._docHeightTwips) && + tilesToFetch > 0 && borderWidth < maxBorderWidth) { + // while the bounds do not fully contain the document + + for (var i = tileBorder.min.x; i <= tileBorder.max.x; i++) { + // tiles below the visible area + var coords = new L.TileCoordData( + i * this._tileSize, + tileBorder.max.y * this._tileSize); + queue.push(coords); + } + for (i = tileBorder.min.x; i <= tileBorder.max.x; i++) { + // tiles above the visible area + coords = new L.TileCoordData( + i * this._tileSize, + tileBorder.min.y * this._tileSize); + queue.push(coords); + } + for (i = tileBorder.min.y; i <= tileBorder.max.y; i++) { + // tiles to the right of the visible area + coords = new L.TileCoordData( + tileBorder.max.x * this._tileSize, + i * this._tileSize); + queue.push(coords); + } + for (i = tileBorder.min.y; i <= tileBorder.max.y; i++) { + // tiles to the left of the visible area + coords = new L.TileCoordData( + tileBorder.min.x * this._tileSize, + i * this._tileSize); + queue.push(coords); + } + + for (i = 0; i < queue.length && tilesToFetch > 0; i++) { + coords = queue[i]; + coords.z = zoom; + coords.part = this._preFetchPart; + var key = this._tileCoordsToKey(coords); + + if (!this._isValidTile(coords) || + this._tiles[key] || + this._tileCache[key] || + visitedTiles[key]) { + continue; + } + + visitedTiles[key] = true; + finalQueue.push(coords); + tilesToFetch -= 1; + } + if (tilesToFetch === 0) { + // don't update the border as there are still + // some tiles to be fetched + continue; + } + if (tileBorder.min.x >= 0) { + tileBorder.min.x -= 1; + } + if (tileBorder.min.y >= 0) { + tileBorder.min.y -= 1; + } + if (tileBorder.max.x * this._tileWidthTwips <= this._docWidthTwips) { + tileBorder.max.x += 1; + } + if (tileBorder.max.y * this._tileHeightTwips <= this._docHeightTwips) { + tileBorder.max.y += 1; + } + borderWidth += 1; + } + } + + if (finalQueue.length > 0) { + this._addTiles(finalQueue); + } else { + clearInterval(this._tilesPreFetcher); + this._tilesPreFetcher = undefined; + } + }, + + _resetPreFetching: function (resetBorder) { + if (!this._map) { + return; + } + if (this._tilesPreFetcher) + clearInterval(this._tilesPreFetcher); + if (this._preFetchIdle) + clearTimeout(this._preFetchIdle); + if (resetBorder) { + this._preFetchBorders = null; + } + var interval = 750; + var idleTime = 5000; + this._preFetchPart = this._selectedPart; + this._preFetchIdle = setTimeout(L.bind(function () { + this._tilesPreFetcher = setInterval(L.bind(this._preFetchTiles, this), interval); + this._prefetchIdle = undefined; + }, this), idleTime); + }, + + _onTileMsg: function (textMsg, img) { + var tileMsgObj = this._map._socket.parseServerCmd(textMsg); + this._checkTileMsgObject(tileMsgObj); + var coords = this._tileMsgToCoords(tileMsgObj); + var key = this._tileCoordsToKey(coords); + var tile = this._tiles[key]; + if (this._debug && tile) { + if (tile._debugLoadCount) { + tile._debugLoadCount++; + this._debugLoadCount++; + } else { + tile._debugLoadCount = 1; + tile._debugInvalidateCount = 1; + } + if (!tile._debugPopup) { + var tileBound = this._keyToBounds(key); + tile._debugPopup = L.popup({ className: 'debug', offset: new L.Point(0, 0), autoPan: false, closeButton: false, closeOnClick: false }) + .setLatLng(new L.LatLng(tileBound.getSouth(), tileBound.getWest() + (tileBound.getEast() - tileBound.getWest()) / 5)); + this._debugInfo.addLayer(tile._debugPopup); + if (this._debugTiles[key]) { + this._debugInfo.removeLayer(this._debugTiles[key]); + } + tile._debugTile = L.rectangle(tileBound, { color: 'blue', weight: 1, fillOpacity: 0, pointerEvents: 'none' }); + this._debugTiles[key] = tile._debugTile; + tile._debugTime = this._debugGetTimeArray(); + this._debugInfo.addLayer(tile._debugTile); + } + if (tile._debugTime.date === 0) { + tile._debugPopup.setContent('requested: ' + this._tiles[key]._debugInvalidateCount + '<br>received: ' + this._tiles[key]._debugLoadCount); + } else { + tile._debugPopup.setContent('requested: ' + this._tiles[key]._debugInvalidateCount + '<br>received: ' + this._tiles[key]._debugLoadCount + + '<br>' + this._debugSetTimes(tile._debugTime, +new Date() - tile._debugTime.date).replace(/, /g, '<br>')); + } + if (tile._debugTile) { + tile._debugTile.setStyle({ fillOpacity: (tileMsgObj.renderid === 'cached') ? 0.1 : 0, fillColor: 'yellow' }); + } + this._debugShowTileData(); + } + if (tileMsgObj.id !== undefined) { + this._map.fire('tilepreview', { + tile: img, + id: tileMsgObj.id, + width: tileMsgObj.width, + height: tileMsgObj.height, + part: tileMsgObj.part, + docType: this._docType + }); + } + else if (tile && typeof (img) == 'object') { + console.error('Not implemented'); + } + else if (tile) { + if (this._tiles[key]._invalidCount > 0) { + this._tiles[key]._invalidCount -= 1; + } + if (!tile.loaded) { + this._emptyTilesCount -= 1; + if (this._emptyTilesCount === 0) { + this._map.fire('statusindicator', { statusType: 'alltilesloaded' }); + } + } + tile.el.src = img; + } + L.Log.log(textMsg, 'INCOMING', key); + + // Send acknowledgment, that the tile message arrived + var tileID = tileMsgObj.part + ':' + tileMsgObj.x + ':' + tileMsgObj.y + ':' + tileMsgObj.tileWidth + ':' + tileMsgObj.tileHeight + ':' + tileMsgObj.nviewid; + this._map._socket.sendMessage('tileprocessed tile=' + tileID); + }, + +}); _______________________________________________ Libreoffice-commits mailing list [email protected] https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits
