diff --git a/examples/canvas-tiles-tms.html b/examples/canvas-tiles-tms.html
new file mode 100644
index 0000000000..28fc084ab7
--- /dev/null
+++ b/examples/canvas-tiles-tms.html
@@ -0,0 +1,14 @@
+---
+layout: example.html
+title: Custom Canvas Tiles
+shortdesc: Renders tiles with TMS coordinates for debugging.
+docs: >
+ The black grid tiles are generated on the client with an HTML5 canvas.
+ The displayed TMS tile coordinates are produced using a custom template
+ for TMS, the vector tile source's 512 pixel tile grid and the
+ zDirection setting for vector tiles.
+ Notice how the country polygons can be split between tiles and vector
+ labels may appear in each tile.
+tags: "layers, vector tiles, tms, canvas"
+---
+
diff --git a/examples/canvas-tiles-tms.js b/examples/canvas-tiles-tms.js
new file mode 100644
index 0000000000..e7d37c8a8e
--- /dev/null
+++ b/examples/canvas-tiles-tms.js
@@ -0,0 +1,61 @@
+import MVT from '../src/ol/format/MVT.js';
+import Map from '../src/ol/Map.js';
+import TileDebug from '../src/ol/source/TileDebug.js';
+import TileLayer from '../src/ol/layer/Tile.js';
+import VectorTileLayer from '../src/ol/layer/VectorTile.js';
+import VectorTileSource from '../src/ol/source/VectorTile.js';
+import View from '../src/ol/View.js';
+import {Fill, Stroke, Style, Text} from '../src/ol/style.js';
+
+const style = new Style({
+ fill: new Fill({
+ color: 'rgba(255, 255, 255, 0.6)',
+ }),
+ stroke: new Stroke({
+ color: '#319FD3',
+ width: 1,
+ }),
+ text: new Text({
+ font: '12px Calibri,sans-serif',
+ fill: new Fill({
+ color: '#000',
+ }),
+ stroke: new Stroke({
+ color: '#fff',
+ width: 3,
+ }),
+ }),
+});
+
+const vtLayer = new VectorTileLayer({
+ declutter: true,
+ source: new VectorTileSource({
+ maxZoom: 15,
+ format: new MVT(),
+ url:
+ 'https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/' +
+ 'ne:ne_10m_admin_0_countries@EPSG%3A900913@pbf/{z}/{x}/{-y}.pbf',
+ }),
+ style: function (feature) {
+ style.getText().setText(feature.get('name'));
+ return style;
+ },
+});
+
+const debugLayer = new TileLayer({
+ source: new TileDebug({
+ template: 'z:{z} x:{x} y:{-y}',
+ projection: vtLayer.getSource().getProjection(),
+ tileGrid: vtLayer.getSource().getTileGrid(),
+ zDirection: 1,
+ }),
+});
+
+const map = new Map({
+ layers: [vtLayer, debugLayer],
+ target: 'map',
+ view: new View({
+ center: [0, 6000000],
+ zoom: 4,
+ }),
+});
diff --git a/examples/reprojection-by-code.html b/examples/reprojection-by-code.html
index df386d4864..d09702f50f 100644
--- a/examples/reprojection-by-code.html
+++ b/examples/reprojection-by-code.html
@@ -10,18 +10,24 @@ tags: "reprojection, projection, proj4js, epsg.io, graticule"
---
+
diff --git a/examples/reprojection-by-code.js b/examples/reprojection-by-code.js
index 2926a94486..2e169c1340 100644
--- a/examples/reprojection-by-code.js
+++ b/examples/reprojection-by-code.js
@@ -2,7 +2,7 @@ import Graticule from '../src/ol/layer/Graticule.js';
import Map from '../src/ol/Map.js';
import OSM from '../src/ol/source/OSM.js';
import Stroke from '../src/ol/style/Stroke.js';
-import TileImage from '../src/ol/source/TileImage.js';
+import TileDebug from '../src/ol/source/TileDebug.js';
import TileLayer from '../src/ol/layer/Tile.js';
import View from '../src/ol/View.js';
import proj4 from 'proj4';
@@ -10,6 +10,16 @@ import {applyTransform} from '../src/ol/extent.js';
import {get as getProjection, getTransform} from '../src/ol/proj.js';
import {register} from '../src/ol/proj/proj4.js';
+const osmSource = new OSM();
+
+const debugLayer = new TileLayer({
+ source: new TileDebug({
+ tileGrid: osmSource.getTileGrid(),
+ projection: osmSource.getProjection(),
+ }),
+ visible: false,
+});
+
const graticule = new Graticule({
// the style to use for the lines, optional.
strokeStyle: new Stroke({
@@ -25,8 +35,9 @@ const graticule = new Graticule({
const map = new Map({
layers: [
new TileLayer({
- source: new OSM(),
+ source: osmSource,
}),
+ debugLayer,
graticule,
],
target: 'map',
@@ -41,6 +52,7 @@ const queryInput = document.getElementById('epsg-query');
const searchButton = document.getElementById('epsg-search');
const resultSpan = document.getElementById('epsg-result');
const renderEdgesCheckbox = document.getElementById('render-edges');
+const showTilesCheckbox = document.getElementById('show-tiles');
const showGraticuleCheckbox = document.getElementById('show-graticule');
function setProjection(code, name, proj4def, bbox) {
@@ -125,22 +137,14 @@ searchButton.onclick = function (event) {
};
/**
- * Handle checkbox change event.
+ * Handle checkbox change events.
*/
renderEdgesCheckbox.onchange = function () {
- map.getLayers().forEach(function (layer) {
- if (layer instanceof TileLayer) {
- const source = layer.getSource();
- if (source instanceof TileImage) {
- source.setRenderReprojectionEdges(renderEdgesCheckbox.checked);
- }
- }
- });
+ osmSource.setRenderReprojectionEdges(renderEdgesCheckbox.checked);
+};
+showTilesCheckbox.onchange = function () {
+ debugLayer.setVisible(showTilesCheckbox.checked);
};
-
-/**
- * Handle checkbox change event.
- */
showGraticuleCheckbox.onchange = function () {
graticule.setVisible(showGraticuleCheckbox.checked);
};
diff --git a/src/ol/ImageTile.js b/src/ol/ImageTile.js
index f6367490ac..a2fce7f973 100644
--- a/src/ol/ImageTile.js
+++ b/src/ol/ImageTile.js
@@ -72,6 +72,17 @@ class ImageTile extends Tile {
return this.image_;
}
+ /**
+ * Sets an HTML image element for this tile (may be a Canvas or preloaded Image).
+ * @param {HTMLCanvasElement|HTMLImageElement} element Element.
+ */
+ setImage(element) {
+ this.image_ = element;
+ this.state = TileState.LOADED;
+ this.unlistenImage_();
+ this.changed();
+ }
+
/**
* Tracks loading or read errors.
*
diff --git a/src/ol/reproj/Tile.js b/src/ol/reproj/Tile.js
index 02fe5c75db..a44fcba169 100644
--- a/src/ol/reproj/Tile.js
+++ b/src/ol/reproj/Tile.js
@@ -334,15 +334,15 @@ class ReprojTile extends Tile {
}.bind(this)
);
- this.sourceTiles_.forEach(function (tile, i, arr) {
- const state = tile.getState();
- if (state == TileState.IDLE) {
- tile.load();
- }
- });
-
if (leftToLoad === 0) {
setTimeout(this.reproject_.bind(this), 0);
+ } else {
+ this.sourceTiles_.forEach(function (tile, i, arr) {
+ const state = tile.getState();
+ if (state == TileState.IDLE) {
+ tile.load();
+ }
+ });
}
}
}
diff --git a/src/ol/source/TileDebug.js b/src/ol/source/TileDebug.js
index 36353da531..7f14bcf9a3 100644
--- a/src/ol/source/TileDebug.js
+++ b/src/ol/source/TileDebug.js
@@ -2,82 +2,10 @@
* @module ol/source/TileDebug
*/
-import Tile from '../Tile.js';
-import TileState from '../TileState.js';
import XYZ from './XYZ.js';
import {createCanvasContext2D} from '../dom.js';
-import {getKeyZXY} from '../tilecoord.js';
import {toSize} from '../size.js';
-class LabeledTile extends Tile {
- /**
- * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
- * @param {import("../size.js").Size} tileSize Tile size.
- * @param {string} text Text.
- */
- constructor(tileCoord, tileSize, text) {
- super(tileCoord, TileState.LOADED);
-
- /**
- * @private
- * @type {import("../size.js").Size}
- */
- this.tileSize_ = tileSize;
-
- /**
- * @private
- * @type {string}
- */
- this.text_ = text;
-
- /**
- * @private
- * @type {HTMLCanvasElement}
- */
- this.canvas_ = null;
- }
-
- /**
- * Get the image element for this tile.
- * @return {HTMLCanvasElement} Image.
- */
- getImage() {
- if (this.canvas_) {
- return this.canvas_;
- } else {
- const tileSize = this.tileSize_;
- const context = createCanvasContext2D(tileSize[0], tileSize[1]);
-
- context.strokeStyle = 'grey';
- context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5);
-
- context.fillStyle = 'grey';
- context.strokeStyle = 'white';
- context.textAlign = 'center';
- context.textBaseline = 'middle';
- context.font = '24px sans-serif';
- context.lineWidth = 4;
- context.strokeText(
- this.text_,
- tileSize[0] / 2,
- tileSize[1] / 2,
- tileSize[0]
- );
- context.fillText(
- this.text_,
- tileSize[0] / 2,
- tileSize[1] / 2,
- tileSize[0]
- );
-
- this.canvas_ = context.canvas;
- return context.canvas;
- }
- }
-
- load() {}
-}
-
/**
* @typedef {Object} Options
* @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Optional projection.
@@ -88,6 +16,8 @@ class LabeledTile extends Tile {
* the view resolution does not match any resolution of the tile source. If 0, the nearest
* resolution will be used. If 1, the nearest lower resolution will be used. If -1, the
* nearest higher resolution will be used.
+ * @property {string} [template='z:{z} x:{x} y:{y}'] Template for labeling the tiles.
+ * Should include `{x}`, `{y}` or `{-y}`, and `{z}` placeholders.
*/
/**
@@ -95,8 +25,6 @@ class LabeledTile extends Tile {
* A pseudo tile source, which does not fetch tiles from a server, but renders
* a grid outline for the tile grid/projection along with the coordinates for
* each tile. See examples/canvas-tiles for an example.
- *
- * Uses Canvas context2d, so requires Canvas support.
* @api
*/
class TileDebug extends XYZ {
@@ -115,39 +43,29 @@ class TileDebug extends XYZ {
tileGrid: options.tileGrid,
wrapX: options.wrapX !== undefined ? options.wrapX : true,
zDirection: options.zDirection,
- });
- }
+ url: options.template || 'z:{z} x:{x} y:{y}',
+ tileLoadFunction: (tile, text) => {
+ const z = tile.getTileCoord()[0];
+ const tileSize = toSize(this.tileGrid.getTileSize(z));
+ const context = createCanvasContext2D(tileSize[0], tileSize[1]);
- /**
- * @param {number} z Tile coordinate z.
- * @param {number} x Tile coordinate x.
- * @param {number} y Tile coordinate y.
- * @return {!LabeledTile} Tile.
- */
- getTile(z, x, y) {
- const tileCoordKey = getKeyZXY(z, x, y);
- if (this.tileCache.containsKey(tileCoordKey)) {
- return /** @type {!LabeledTile} */ (this.tileCache.get(tileCoordKey));
- } else {
- const tileSize = toSize(this.tileGrid.getTileSize(z));
- const tileCoord = [z, x, y];
- const textTileCoord = this.getTileCoordForTileUrlFunction(tileCoord);
- let text;
- if (textTileCoord) {
- text =
- 'z:' +
- textTileCoord[0] +
- ' x:' +
- textTileCoord[1] +
- ' y:' +
- textTileCoord[2];
- } else {
- text = 'none';
- }
- const tile = new LabeledTile(tileCoord, tileSize, text);
- this.tileCache.set(tileCoordKey, tile);
- return tile;
- }
+ context.strokeStyle = 'grey';
+ context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5);
+
+ context.fillStyle = 'grey';
+ context.strokeStyle = 'white';
+ context.textAlign = 'center';
+ context.textBaseline = 'middle';
+ context.font = '24px sans-serif';
+ context.lineWidth = 4;
+ context.strokeText(text, tileSize[0] / 2, tileSize[1] / 2, tileSize[0]);
+ context.fillText(text, tileSize[0] / 2, tileSize[1] / 2, tileSize[0]);
+
+ /** @type {import("../ImageTile.js").default} */ (tile).setImage(
+ context.canvas
+ );
+ },
+ });
}
}
diff --git a/test/rendering/cases/reproj-tile-4326-debug/expected.png b/test/rendering/cases/reproj-tile-4326-debug/expected.png
new file mode 100644
index 0000000000..7e5f6784b3
Binary files /dev/null and b/test/rendering/cases/reproj-tile-4326-debug/expected.png differ
diff --git a/test/rendering/cases/reproj-tile-4326-debug/main.js b/test/rendering/cases/reproj-tile-4326-debug/main.js
new file mode 100644
index 0000000000..d9447b275a
--- /dev/null
+++ b/test/rendering/cases/reproj-tile-4326-debug/main.js
@@ -0,0 +1,49 @@
+import Map from '../../../../src/ol/Map.js';
+import TileLayer from '../../../../src/ol/layer/Tile.js';
+import View from '../../../../src/ol/View.js';
+import {TileDebug, XYZ} from '../../../../src/ol/source.js';
+import {createForProjection, createXYZ} from '../../../../src/ol/tilegrid.js';
+import {get, toLonLat} from '../../../../src/ol/proj.js';
+
+const tileGrid = createXYZ();
+const extent = tileGrid.getTileCoordExtent([5, 5, 12]);
+const center = [(extent[0] + extent[2]) / 2, extent[1]];
+
+const source = new XYZ({
+ transition: 0,
+ minZoom: 5,
+ maxZoom: 5,
+ url: '/data/tiles/osm/{z}/{x}/{y}.png',
+});
+
+const sourceDebug = new TileDebug({tileGrid: source.getTileGrid()});
+
+source.setTileGridForProjection(
+ get('EPSG:4326'),
+ createForProjection(get('EPSG:4326'), 7, [64, 64])
+);
+
+sourceDebug.setTileGridForProjection(
+ get('EPSG:4326'),
+ createForProjection(get('EPSG:4326'), 7, [64, 64])
+);
+
+new Map({
+ pixelRatio: 1,
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: source,
+ }),
+ new TileLayer({
+ source: sourceDebug,
+ }),
+ ],
+ view: new View({
+ projection: 'EPSG:4326',
+ center: toLonLat(center),
+ zoom: 5,
+ }),
+});
+
+render();