[Libreoffice-commits] online.git: loleaflet/src

Dennis Francis (via logerrit) logerrit at kemper.freedesktop.org
Wed Aug 12 08:31:39 UTC 2020


 loleaflet/src/geometry/Bounds.js              |   39 ++
 loleaflet/src/layer/SplitPanesContext.js      |   37 ++
 loleaflet/src/layer/tile/CanvasTileLayer.js   |  458 ++++++++++++++++++--------
 loleaflet/src/layer/tile/GridLayer.js         |   23 +
 loleaflet/src/map/handler/Map.TouchGesture.js |    7 
 5 files changed, 427 insertions(+), 137 deletions(-)

New commits:
commit 06e4722cc93a4acdbf47700de97ea1a8e98ad6fa
Author:     Dennis Francis <dennis.francis at collabora.com>
AuthorDate: Fri Aug 7 18:37:11 2020 +0530
Commit:     Dennis Francis <dennis.francis at collabora.com>
CommitDate: Wed Aug 12 10:31:19 2020 +0200

    loleaflet: rewrite tile-prefetcher for L.CanvasTileLayer...
    
    with full support and optimizations for split-panes. This includes some
    refactors, for instance the prefetcher implementation is taken out of
    the doc-layer methods as a L.TilesPrefetcher class.
    
    Note: This only affects Calc as Writer/Impress are still using
    L.TileLayer.
    
    Change-Id: I0e43b58bad0a97d44cbffd1ee0d90d94a9426e29
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/100485
    Tested-by: Jenkins
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Dennis Francis <dennis.francis at collabora.com>

diff --git a/loleaflet/src/geometry/Bounds.js b/loleaflet/src/geometry/Bounds.js
index cadc8e225..8dacdc832 100644
--- a/loleaflet/src/geometry/Bounds.js
+++ b/loleaflet/src/geometry/Bounds.js
@@ -175,7 +175,44 @@ L.Bounds.prototype = {
 		}
 
 		return false;
-	}
+	},
+
+	clampX: function (x) {
+		return Math.max(this.min.x, Math.min(this.max.x, x));
+	},
+
+	clampY: function (y) {
+		return Math.max(this.min.y, Math.min(this.max.y, y));
+	},
+
+	clamp: function (obj) {  // (Point) -> Point or (Bounds) -> Bounds
+		if (obj instanceof L.Point) {
+			return new L.Point(
+				this.clampX(obj.x),
+				this.clampY(obj.y)
+			);
+		}
+
+		if (obj instanceof L.Bounds) {
+			return new L.Bounds(
+				new L.Point(
+					this.clampX(obj.min.x),
+					this.clampY(obj.min.y)
+				),
+
+				new L.Point(
+					this.clampX(obj.max.x),
+					this.clampY(obj.max.y)
+				)
+			);
+		}
+
+		console.error('invalid argument type');
+	},
+
+	equals: function (bounds) { // (Bounds) -> Boolean
+		return this.min.equals(bounds.min) && this.max.equals(bounds.max);
+	},
 };
 
 L.bounds = function (a, b) { // (Bounds) or (Point, Point) or (Point[])
diff --git a/loleaflet/src/layer/SplitPanesContext.js b/loleaflet/src/layer/SplitPanesContext.js
index 01920ec8e..b75272221 100644
--- a/loleaflet/src/layer/SplitPanesContext.js
+++ b/loleaflet/src/layer/SplitPanesContext.js
@@ -158,6 +158,41 @@ L.SplitPanesContext = L.Class.extend({
 		this._docLayer.updateVertPaneSplitter();
 	},
 
+	getPanesProperties: function () {
+		var paneStatusList = [];
+		if (this._splitPos.x && this._splitPos.y) {
+			// top-left pane
+			paneStatusList.push({
+				xFixed: true,
+				yFixed: true,
+			});
+		}
+
+		if (this._splitPos.y) {
+			// top-right pane or top half pane
+			paneStatusList.push({
+				xFixed: false,
+				yFixed: true,
+			});
+		}
+
+		if (this._splitPos.x) {
+			// bottom-left pane or left half pane
+			paneStatusList.push({
+				xFixed: true,
+				yFixed: false,
+			});
+		}
+
+		// bottom-right/bottom-half/right-half pane or the full pane (when there are no split-panes active)
+		paneStatusList.push({
+			xFixed: false,
+			yFixed: false,
+		});
+
+		return paneStatusList;
+	},
+
 	// returns all the pane rectangles for the provided full-map area (all in CSS pixels).
 	getPxBoundList: function (pxBounds) {
 		if (!pxBounds) {
@@ -191,7 +226,7 @@ L.SplitPanesContext = L.Class.extend({
 			));
 		}
 
-		// bottom-right pane or the full pane (when there are no split-panes active)
+		// bottom-right/bottom-half/right-half pane or the full pane (when there are no split-panes active)
 		boundList.push(new L.Bounds(
 			topLeft.add(this._splitPos).add(new L.Point(1, 1)),
 			bottomRight
diff --git a/loleaflet/src/layer/tile/CanvasTileLayer.js b/loleaflet/src/layer/tile/CanvasTileLayer.js
index 3e86d3f0f..4d7d5dc1f 100644
--- a/loleaflet/src/layer/tile/CanvasTileLayer.js
+++ b/loleaflet/src/layer/tile/CanvasTileLayer.js
@@ -1160,141 +1160,30 @@ L.CanvasTileLayer = L.TileLayer.extend({
 		});
 	},
 
-	_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.isPermissionEdit()) {
-			tilesToFetch = 5;
-			maxBorderWidth = 3;
+	_preFetchTiles: function (forceBorderCalc) {
+		if (this._prefetcher) {
+			this._prefetcher.preFetchTiles(forceBorderCalc);
 		}
+	},
 
-		if (!this._preFetchBorders) {
-			var pixelBounds = this._map.getPixelBounds(center, zoom);
-			tileBorderSrcs = this._pxBoundsToTileRanges(pixelBounds);
-			this._preFetchBorders = tileBorderSrcs;
-		}
-		else {
-			tileBorderSrcs = this._preFetchBorders;
+	_resetPreFetching: function (resetBorder) {
+		if (!this._prefetcher) {
+			this._prefetcher = new L.TilesPreFetcher(this, this._map);
 		}
 
-		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;
-			}
-		}
+		this._prefetcher.resetPreFetching(resetBorder);
+	},
 
-		if (finalQueue.length > 0) {
-			this._addTiles(finalQueue);
-		} else {
-			clearInterval(this._tilesPreFetcher);
-			this._tilesPreFetcher = undefined;
+	_clearPreFetch: function () {
+		if (this._prefetcher) {
+			this._prefetcher.clearPreFetch();
 		}
 	},
 
-	_resetPreFetching: function (resetBorder) {
-		if (!this._map) {
-			return;
+	_clearTilesPreFetcher: function () {
+		if (this._prefetcher) {
+			this._prefetcher.clearTilesPreFetcher();
 		}
-		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) {
@@ -1407,3 +1296,320 @@ L.CanvasTileLayer = L.TileLayer.extend({
 	},
 
 });
+
+L.TilesPreFetcher = L.Class.extend({
+
+	initialize: function (docLayer, map) {
+		this._docLayer = docLayer;
+		this._map = map;
+	},
+
+	preFetchTiles: function (forceBorderCalc) {
+
+		if (this._docLayer._emptyTilesCount > 0 || !this._map || !this._docLayer) {
+			return;
+		}
+
+		var center = this._map.getCenter();
+		var zoom = this._map.getZoom();
+		var part = this._docLayer._selectedPart;
+		var hasEditPerm = this._map.isPermissionEdit();
+
+		if (this._zoom === undefined) {
+			this._zoom = zoom;
+		}
+
+		if (this._preFetchPart === undefined) {
+			this._preFetchPart = part;
+		}
+
+		if (this._hasEditPerm === undefined) {
+			this._hasEditPerm = hasEditPerm;
+		}
+
+		var maxTilesToFetch = 10;
+		// don't search on a border wider than 5 tiles because it will freeze the UI
+		var maxBorderWidth = 5;
+
+		if (hasEditPerm) {
+			maxTilesToFetch = 5;
+			maxBorderWidth = 3;
+		}
+
+		var tileSize = this._docLayer._tileSize;
+		var pixelBounds = this._map.getPixelBounds(center, zoom);
+
+		if (this._pixelBounds === undefined) {
+			this._pixelBounds = pixelBounds;
+		}
+
+		var splitPanesContext = this._docLayer.getSplitPanesContext();
+		var splitPos = splitPanesContext ? splitPanesContext.getSplitPos() : new L.Point(0, 0);
+
+		if (this._splitPos === undefined) {
+			this._splitPos = splitPos;
+		}
+
+		var paneXFixed = false;
+		var paneYFixed = false;
+
+		if (forceBorderCalc ||
+			!this._borders || this._borders.length === 0 ||
+			zoom !== this._zoom ||
+			part !== this._preFetchPart ||
+			hasEditPerm !== this._hasEditPerm ||
+			!pixelBounds.equals(this._pixelBounds) ||
+			!splitPos.equals(this._splitPos)) {
+
+			this._zoom = zoom;
+			this._preFetchPart = part;
+			this._hasEditPerm = hasEditPerm;
+			this._pixelBounds = pixelBounds;
+			this._splitPos = splitPos;
+
+			// Need to compute borders afresh and fetch tiles for them.
+			this._borders = []; // Stores borders for each split-pane.
+			var tileRanges = this._docLayer._pxBoundsToTileRanges(pixelBounds);
+			var paneStatusList = splitPanesContext ? splitPanesContext.getPanesProperties() :
+				[ { xFixed: false, yFixed: false} ];
+
+			console.assert(tileRanges.length === paneStatusList.length, 'tileRanges and paneStatusList should agree on the number of split-panes');
+
+			for (var paneIdx = 0; paneIdx < tileRanges.length; ++paneIdx) {
+				paneXFixed = paneStatusList[paneIdx].xFixed;
+				paneYFixed = paneStatusList[paneIdx].yFixed;
+
+				if (paneXFixed && paneYFixed) {
+					continue;
+				}
+
+				var tileRange = tileRanges[paneIdx];
+				var paneBorder = new L.Bounds(
+					tileRange.min.add(new L.Point(-1, -1)),
+					tileRange.max.add(new L.Point(1, 1))
+				);
+
+				this._borders.push(new L.TilesPreFetcher.PaneBorder(paneBorder, paneXFixed, paneYFixed));
+			}
+
+		}
+
+		var finalQueue = [];
+		var visitedTiles = {};
+
+		var validTileRange = new L.Bounds(
+			new L.Point(0, 0),
+			new L.Point(
+				Math.floor((this._docLayer._docWidthTwips - 1) / this._docLayer._tileWidthTwips),
+				Math.floor((this._docLayer._docHeightTwips - 1) / this._docLayer._tileHeightTwips)
+			)
+		);
+
+		var tilesToFetch = maxTilesToFetch; // total tile limit per call of preFetchTiles()
+		var doneAllPanes = true;
+
+		for (paneIdx = 0; paneIdx < this._borders.length; ++paneIdx) {
+
+			var queue = [];
+			paneBorder = this._borders[paneIdx];
+			var borderBounds = paneBorder.getBorderBounds();
+
+			paneXFixed = paneBorder.isXFixed();
+			paneYFixed = paneBorder.isYFixed();
+
+			while (tilesToFetch > 0 && paneBorder.getBorderIndex() < maxBorderWidth) {
+
+				var clampedBorder = validTileRange.clamp(borderBounds);
+				var fetchTopBorder = !paneYFixed && borderBounds.min.y === clampedBorder.min.y;
+				var fetchBottomBorder = !paneYFixed && borderBounds.max.y === clampedBorder.max.y;
+				var fetchLeftBorder = !paneXFixed && borderBounds.min.x === clampedBorder.min.x;
+				var fetchRightBorder = !paneXFixed && borderBounds.max.x === clampedBorder.max.x;
+
+				if (!fetchLeftBorder && !fetchRightBorder && !fetchTopBorder && !fetchBottomBorder) {
+					break;
+				}
+
+				if (fetchBottomBorder) {
+					for (var i = clampedBorder.min.x; i <= clampedBorder.max.x; i++) {
+						// tiles below the visible area
+						var coords = new L.TileCoordData(
+							i * tileSize,
+							borderBounds.max.y * tileSize);
+						queue.push(coords);
+					}
+				}
+
+				if (fetchTopBorder) {
+					for (i = clampedBorder.min.x; i <= clampedBorder.max.x; i++) {
+						// tiles above the visible area
+						coords = new L.TileCoordData(
+							i * tileSize,
+							borderBounds.min.y * tileSize);
+						queue.push(coords);
+					}
+				}
+
+				if (fetchRightBorder) {
+					for (i = clampedBorder.min.y; i <= clampedBorder.max.y; i++) {
+						// tiles to the right of the visible area
+						coords = new L.TileCoordData(
+							borderBounds.max.x * tileSize,
+							i * tileSize);
+						queue.push(coords);
+					}
+				}
+
+				if (fetchLeftBorder) {
+					for (i = clampedBorder.min.y; i <= clampedBorder.max.y; i++) {
+						// tiles to the left of the visible area
+						coords = new L.TileCoordData(
+							borderBounds.min.x * tileSize,
+							i * tileSize);
+						queue.push(coords);
+					}
+				}
+
+				var tilesPending = false;
+				for (i = 0; i < queue.length; i++) {
+					coords = queue[i];
+					coords.z = zoom;
+					coords.part = this._preFetchPart;
+					var key = this._docLayer._tileCoordsToKey(coords);
+
+					if (!this._docLayer._isValidTile(coords) ||
+						this._docLayer._tiles[key] ||
+						this._docLayer._tileCache[key] ||
+						visitedTiles[key]) {
+						continue;
+					}
+
+					if (tilesToFetch > 0) {
+						visitedTiles[key] = true;
+						finalQueue.push(coords);
+						tilesToFetch -= 1;
+					}
+					else {
+						tilesPending = true;
+					}
+				}
+
+				if (tilesPending) {
+					// don't update the border as there are still
+					// some tiles to be fetched
+					continue;
+				}
+
+				if (!paneXFixed) {
+					if (borderBounds.min.x > 0) {
+						borderBounds.min.x -= 1;
+					}
+					if (borderBounds.max.x < validTileRange.max.x) {
+						borderBounds.max.x += 1;
+					}
+				}
+
+				if (!paneYFixed) {
+					if (borderBounds.min.y > 0) {
+						borderBounds.min.y -= 1;
+					}
+
+					if (borderBounds.max.y < validTileRange.max.y) {
+						borderBounds.max.y += 1;
+					}
+				}
+
+				paneBorder.incBorderIndex();
+
+			} // border width loop end
+
+			if (paneBorder.getBorderIndex() < maxBorderWidth) {
+				doneAllPanes = false;
+			}
+		} // pane loop end
+
+		console.assert(finalQueue.length <= maxTilesToFetch,
+			'finalQueue length(' + finalQueue.length + ') exceeded maxTilesToFetch(' + maxTilesToFetch + ')');
+
+		var tilesRequested = false;
+
+		if (finalQueue.length > 0) {
+			this._cumTileCount += finalQueue.length;
+			this._docLayer._addTiles(finalQueue);
+			tilesRequested = true;
+		}
+
+		if (!tilesRequested || doneAllPanes) {
+			this.clearTilesPreFetcher();
+			this._borders = undefined;
+		}
+	},
+
+	resetPreFetching: function (resetBorder) {
+
+		if (!this._map) {
+			return;
+		}
+
+		this.clearPreFetch();
+
+		if (resetBorder) {
+			this._borders = undefined;
+		}
+
+		var interval = 750;
+		var idleTime = 5000;
+		this._preFetchPart = this._docLayer._selectedPart;
+		this._preFetchIdle = setTimeout(L.bind(function () {
+			this._tilesPreFetcher = setInterval(L.bind(this.preFetchTiles, this), interval);
+			this._preFetchIdle = undefined;
+			this._cumTileCount = 0;
+		}, this), idleTime);
+	},
+
+	clearPreFetch: function () {
+		this.clearTilesPreFetcher();
+		if (this._preFetchIdle !== undefined) {
+			clearTimeout(this._preFetchIdle);
+			this._preFetchIdle = undefined;
+		}
+	},
+
+	clearTilesPreFetcher: function () {
+		if (this._tilesPreFetcher !== undefined) {
+			clearInterval(this._tilesPreFetcher);
+			this._tilesPreFetcher = undefined;
+		}
+	},
+
+});
+
+L.TilesPreFetcher.PaneBorder = L.Class.extend({
+
+	initialize: function(paneBorder, paneXFixed, paneYFixed) {
+		this._border = paneBorder;
+		this._xFixed = paneXFixed;
+		this._yFixed = paneYFixed;
+		this._index = 0;
+	},
+
+	getBorderIndex: function () {
+		return this._index;
+	},
+
+	incBorderIndex: function () {
+		this._index += 1;
+	},
+
+	getBorderBounds: function () {
+		return this._border;
+	},
+
+	isXFixed: function () {
+		return this._xFixed;
+	},
+
+	isYFixed: function () {
+		return this._yFixed;
+	},
+
+});
diff --git a/loleaflet/src/layer/tile/GridLayer.js b/loleaflet/src/layer/tile/GridLayer.js
index cd4f5eda2..5d3ce69db 100644
--- a/loleaflet/src/layer/tile/GridLayer.js
+++ b/loleaflet/src/layer/tile/GridLayer.js
@@ -44,9 +44,8 @@ L.GridLayer = L.Layer.extend({
 		map._removeZoomLimit(this);
 		this._container = null;
 		this._tileZoom = null;
-		clearTimeout(this._preFetchIdle);
+		this._clearPreFetch();
 		clearTimeout(this._previewInvalidator);
-		clearInterval(this._tilesPreFetcher);
 
 		if (this._selections) {
 			this._map.removeLayer(this._selections);
@@ -1178,7 +1177,7 @@ L.GridLayer = L.Layer.extend({
 		return true;
 	},
 
-	_preFetchTiles: function () {
+	_preFetchTiles: function (forceBorderCalc) {
 		if (this._emptyTilesCount > 0 || !this._map) {
 			return;
 		}
@@ -1193,7 +1192,7 @@ L.GridLayer = L.Layer.extend({
 			maxBorderWidth = 3;
 		}
 
-		if (!this._preFetchBorder) {
+		if (!this._preFetchBorder || forceBorderCalc) {
 			var pixelBounds = this._map.getPixelBounds(center, zoom);
 			tileBorderSrc = this._pxBoundsToTileRange(pixelBounds);
 			this._preFetchBorder = tileBorderSrc;
@@ -1301,10 +1300,24 @@ L.GridLayer = L.Layer.extend({
 		this._preFetchPart = this._selectedPart;
 		this._preFetchIdle = setTimeout(L.bind(function () {
 			this._tilesPreFetcher = setInterval(L.bind(this._preFetchTiles, this), interval);
-			this._prefetchIdle = undefined;
+			this._preFetchIdle = undefined;
 		}, this), idleTime);
 	},
 
+	_clearPreFetch: function () {
+		if (this._preFetchIdle !== undefined) {
+			clearTimeout(this._preFetchIdle);
+		}
+
+		this._clearTilesPreFetcher();
+	},
+
+	_clearTilesPreFetcher: function () {
+		if (this._tilesPreFetcher !== undefined) {
+			clearInterval(this._tilesPreFetcher);
+		}
+	},
+
 	_coordsToPixBounds: function (coords) {
 		// coords.x and coords.y are the grid indices of the tile.
 		var topLeft = new L.Point(coords.x, coords.y)._multiplyBy(this._tileSize);
diff --git a/loleaflet/src/map/handler/Map.TouchGesture.js b/loleaflet/src/map/handler/Map.TouchGesture.js
index 15bb932d4..dc37c9c75 100644
--- a/loleaflet/src/map/handler/Map.TouchGesture.js
+++ b/loleaflet/src/map/handler/Map.TouchGesture.js
@@ -661,10 +661,9 @@ L.Map.TouchGesture = L.Handler.extend({
 
 			this._map.dragging._draggable._onMove(e);
 
-			// Updates the tiles
-			clearInterval(this._map._docLayer._tilesPreFetcher);
-			this._map._docLayer._preFetchBorder = null;
-			this._map._docLayer._preFetchTiles();
+			// Prefetch border tiles for the current visible area after cancelling any scheduled calls to the prefetcher.
+			this._map._docLayer._clearPreFetch();
+			this._map._docLayer._preFetchTiles(true /* forceBorderCalc */);
 
 			if (!horizontalEnd || !verticalEnd) {
 				this.autoscrollAnimReq = L.Util.requestAnimFrame(this._autoscroll, this, true);


More information about the Libreoffice-commits mailing list