[Libreoffice-commits] online.git: loleaflet/css loleaflet/Makefile.am loleaflet/src
Dennis Francis (via logerrit)
logerrit at kemper.freedesktop.org
Wed Jul 8 14:51:01 UTC 2020
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 <dennis.francis at collabora.com>
AuthorDate: Tue Jul 7 13:20:34 2020 +0530
Commit: Dennis Francis <dennis.francis at collabora.com>
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 <jenkinscollaboraoffice at gmail.com>
Reviewed-by: Dennis Francis <dennis.francis at collabora.com>
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);
+ },
+
+});
More information about the Libreoffice-commits
mailing list