Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor CustomSource #12063

Merged
merged 12 commits into from
Sep 19, 2022
82 changes: 48 additions & 34 deletions debug/custom-source.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
<body>
<div id='map'></div>
<div id='controls'>
<input id="slider" type="range" list="colors" min="0" step="1">
<datalist id="colors"></datalist>
<input id="slider" type="range" value="0" min="0" step="1"><br />
<label><input id='custom-source-checkbox' type='checkbox' checked>custom source</label><br />
<label><input id='tile-boundaries-checkbox' type='checkbox'>tile boundaries</label><br />
<label><input id='terrain-checkbox' type='checkbox'>terrain</label><br />
</div>

<script src='../dist/mapbox-gl-dev.js'></script>
Expand All @@ -31,59 +33,71 @@
hash: true
});

const tileSize = 256;
const colors = ['#74a9cf', '#3690c0', '#0570b0', '#045a8d'];
const tileSize = 512;
const colors = ['red', 'green', 'blue', 'yellow'];
let currentColor = colors[0];

const canvas = document.createElement('canvas');
canvas.width = canvas.height = tileSize;
const context = canvas.getContext('2d', {willReadFrequently: true});

class CustomSource {
constructor() {
this.id = 'custom-source';
this.type = 'custom';
this.tileSize = tileSize;
}

async loadTile({z, x, y}) {
context.fillStyle = currentColor;
context.fillRect(0, 0, tileSize, tileSize);
return canvas;
}
}

map.on('load', () => {
map.addSource('custom-source', {
type: 'custom',
tileSize,
async loadTile({z, x, y}) {
context.fillStyle = 'red';
context.fillRect(0, 0, tileSize, tileSize);

context.font = '18px serif';
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(`${z}/${x}/${y}`, tileSize / 2, tileSize / 2, tileSize);
return canvas;
},
prepareTile({z, x, y}) {
context.fillStyle = currentColor;
context.fillRect(0, 0, tileSize, tileSize);

context.font = '18px serif';
context.fillStyle = 'white';
context.textAlign = 'center';
context.fillText(`${z}/${x}/${y}`, tileSize / 2, tileSize / 2, tileSize);
return canvas;
},
hasTile({z, x, y}) {
return (x + y) % 2 === 0;
}
});
const customSource = new CustomSource();

function triggerRepaint() {
customSource.clearTiles();
customSource.update();
}

map.addSource('custom-source', customSource);

map.addLayer({
id: 'custom-source',
type: 'raster',
source: 'custom-source',
paint: {
'raster-opacity': 0.75,
'raster-opacity': 0.25,
'raster-fade-duration': 0
}
});

map.addSource('mapbox-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
tileSize: 512
});

document.getElementById('terrain-checkbox').onclick = function () {
map.setTerrain(this.checked ? {source: 'mapbox-dem'} : null);
};

document.getElementById('tile-boundaries-checkbox').onclick = function () {
map.showTileBoundaries = this.checked;
};

document.getElementById('custom-source-checkbox').onclick = function () {
map.setLayoutProperty('custom-source', 'visibility', this.checked ? 'visible' : 'none');
};

const slider = document.getElementById('slider');
slider.value = 0;
slider.max = colors.length - 1;
slider.addEventListener('input', (e) => {
currentColor = colors[e.target.value];
map.triggerRepaint();
triggerRepaint();
});
});

Expand Down
75 changes: 30 additions & 45 deletions src/source/custom_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function isRaster(data: any): boolean {
* These sources can be added between any regular sources using {@link Map#addSource}.
*
* Custom sources must have a unique `id` and must have the `type` of `"custom"`.
* They must implement `loadTile` and may implement `unloadTile`, `prepareTile`, `onAdd` and `onRemove`.
* They must implement `loadTile` and may implement `unloadTile`, `onAdd` and `onRemove`.
* They can trigger rendering using {@link Map#triggerRepaint}.
*
* @interface CustomSourceInterface
Expand Down Expand Up @@ -116,17 +116,6 @@ function isRaster(data: any): boolean {
* @returns {boolean} True if tile exists, otherwise false.
*/

/**
* Optional method called during a render frame to allow a source to prepare and modify a tile texture if needed.
*
* @function
* @memberof CustomSourceInterface
* @instance
* @name prepareTile
* @param {{ z: number, x: number, y: number }} tile Tile name to prepare in the XYZ scheme format.
* @returns {TextureImage} The tile image data as an `HTMLImageElement`, `ImageData`, `ImageBitmap` or object with `width`, `height`, and `data`.
*/

/**
* Called when the map starts loading tile for the current animation frame.
*
Expand All @@ -136,8 +125,9 @@ function isRaster(data: any): boolean {
* @name loadTile
* @param {{ z: number, x: number, y: number }} tile Tile name to load in the XYZ scheme format.
* @param {Object} options Options.
* @param {AbortSignal} options.signal A signal object that allows the map to cancel tile loading request.
* @returns {Promise<TextureImage>} The tile image data as an `HTMLImageElement`, `ImageData`, `ImageBitmap` or object with `width`, `height`, and `data`.
* @param {AbortSignal} options.signal A signal object that communicates when the map cancels the tile loading request.
* @returns {Promise<TextureImage | undefined | null>} The promise that resolves to the tile image data as an `HTMLImageElement`, `ImageData`, `ImageBitmap` or object with `width`, `height`, and `data`.
* If `loadTile` resolves to `undefined`, a map will render an overscaled parent tile in the tile’s space. If `loadTile` resolves to `null`, a map will render nothing in the tile’s space.
*/
export type CustomSourceInterface<T> = {
id: string;
Expand All @@ -150,8 +140,7 @@ export type CustomSourceInterface<T> = {
attribution: ?string,
bounds: ?[number, number, number, number];
hasTile: ?(tileID: { z: number, x: number, y: number }) => boolean,
loadTile: (tileID: { z: number, x: number, y: number }, options: { signal: AbortSignal }) => Promise<T>,
prepareTile: ?(tileID: { z: number, x: number, y: number }) => ?T,
loadTile: (tileID: { z: number, x: number, y: number }, options: { signal: AbortSignal }) => Promise<?T>,
unloadTile: ?(tileID: { z: number, x: number, y: number }) => void,
onAdd: ?(map: Map) => void,
onRemove: ?(map: Map) => void,
Expand Down Expand Up @@ -210,6 +199,9 @@ class CustomSource<T> extends Evented implements Source {
// $FlowFixMe[prop-missing]
implementation.update = this._update.bind(this);

// $FlowFixMe[prop-missing]
implementation.clearTiles = this._clearTiles.bind(this);

// $FlowFixMe[prop-missing]
implementation.coveringTiles = this._coveringTiles.bind(this);

Expand Down Expand Up @@ -258,28 +250,20 @@ class CustomSource<T> extends Evented implements Source {
const controller = new window.AbortController();
const signal = controller.signal;

const request = this._implementation.loadTile({x, y, z}, {signal});
if (!request) {
// Create an empty image and set the tile state to `loaded`
// if the implementation didn't return the async tile request
const emptyImage = {width: this.tileSize, height: this.tileSize, data: null};
this.loadTileData(tile, (emptyImage: any));
tile.state = 'loaded';
return callback(null);
}

// $FlowFixMe[prop-missing]
request.cancel = () => controller.abort();

// $FlowFixMe[prop-missing]
tile.request = request.then(tileLoaded.bind(this))
tile.request = Promise
.resolve(this._implementation.loadTile({x, y, z}, {signal}))
.then(tileLoaded.bind(this))
.catch(error => {
// silence AbortError
if (error.code === 20) return;
tile.state = 'errored';
callback(error);
});

// $FlowFixMe[prop-missing]
tile.request.cancel = () => controller.abort();

function tileLoaded(data) {
delete tile.request;

Expand All @@ -288,9 +272,18 @@ class CustomSource<T> extends Evented implements Source {
return callback(null);
}

if (!data) {
// Create an empty image and set the tile state to `loaded`
// if the implementation returned no tile data
// If the implementation returned `undefined` as tile data,
// mark the tile as `errored` to indicate that we have no data for it.
// A map will render an overscaled parent tile in the tile’s space.
if (data === undefined) {
tile.state = 'errored';
return callback(null);
}

// If the implementation returned `null` as tile data,
// mark the tile as `loaded` and use an an empty image as tile data.
// A map will render nothing in the tile’s space.
if (data === null) {
const emptyImage = {width: this.tileSize, height: this.tileSize, data: null};
this.loadTileData(tile, (emptyImage: any));
tile.state = 'loaded';
Expand Down Expand Up @@ -318,18 +311,6 @@ class CustomSource<T> extends Evented implements Source {
RasterTileSource.unloadTileData(tile, this._map.painter);
}

prepareTile(tile: Tile): ?T {
if (!this._implementation.prepareTile) return null;

const {x, y, z} = tile.tileID.canonical;
const data = this._implementation.prepareTile({x, y, z});
if (!data) return null;

this.loadTileData(tile, data);
tile.state = 'loaded';
return data;
}

unloadTile(tile: Tile, callback: Callback<void>): void {
this.unloadTileData(tile);
if (this._implementation.unloadTile) {
Expand Down Expand Up @@ -364,6 +345,10 @@ class CustomSource<T> extends Evented implements Source {
return tileIDs.map(tileID => ({x: tileID.canonical.x, y: tileID.canonical.y, z: tileID.canonical.z}));
}

_clearTiles() {
this._map.style._clearSource(this.id);
}

_update() {
this.fire(new Event('data', {dataType: 'source', sourceDataType: 'content'}));
}
Expand Down
1 change: 0 additions & 1 deletion src/source/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ export interface Source {
+hasTile?: (tileID: OverscaledTileID) => boolean;
+abortTile?: (tile: Tile, callback: Callback<void>) => void;
+unloadTile?: (tile: Tile, callback: Callback<void>) => void;
+prepareTile?: (tile: Tile) => ?any;
+reload?: () => void;

/**
Expand Down
29 changes: 3 additions & 26 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,21 +172,6 @@ class SourceCache extends Evented {

this._state.coalesceChanges(this._tiles, this.map ? this.map.painter : null);

if (this._source.prepareTile) {
for (const i in this._tiles) {
const tile = this._tiles[i];
const data = this._source.prepareTile(tile);
if (data && this.map.painter.terrain) {
this.map.painter.terrain._clearRenderCacheForTile(this.id, tile.tileID);
}

tile.upload(context);
tile.prepare(this.map.style.imageManager);
}

return;
}

for (const i in this._tiles) {
const tile = this._tiles[i];
tile.upload(context);
Expand Down Expand Up @@ -759,10 +744,7 @@ class SourceCache extends Evented {
*/
_addTile(tileID: OverscaledTileID): Tile {
let tile = this._tiles[tileID.key];
if (tile) {
if (this._source.prepareTile) this._source.prepareTile(tile);
return tile;
}
if (tile) return tile;

tile = this._cache.getAndRemove(tileID);
if (tile) {
Expand All @@ -781,12 +763,7 @@ class SourceCache extends Evented {
if (!cached) {
const painter = this.map ? this.map.painter : null;
tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), this.transform.tileZoom, painter, this._isRaster);
if (this._source.prepareTile) {
const data = this._source.prepareTile(tile);
if (!data) this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state));
} else {
this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state));
}
this._loadTile(tile, this._tileLoaded.bind(this, tile, tileID.key, tile.state));
}

// Impossible, but silence flow.
Expand Down Expand Up @@ -1065,7 +1042,7 @@ function compareTileId(a: OverscaledTileID, b: OverscaledTileID): number {
}

function isRasterType(type): boolean {
return type === 'raster' || type === 'image' || type === 'video';
return type === 'raster' || type === 'image' || type === 'video' || type === 'custom';
}

function tileBoundsX(id: CanonicalTileID, wrap: number): [number, number] {
Expand Down
2 changes: 1 addition & 1 deletion src/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ class Tile {
setTexture(img: TextureImage, painter: Painter) {
const context = painter.context;
const gl = context.gl;
this.texture = painter.getTileTexture(img.width);
this.texture = this.texture || painter.getTileTexture(img.width);
if (this.texture) {
this.texture.update(img, {useMipmap: true});
} else {
Expand Down
Loading