@bokeh/bokehjs
Version:
Interactive, novel data visualization
316 lines • 12.2 kB
JavaScript
import { TileSource } from "./tile_source";
import { WMTSTileSource } from "./wmts_tile_source";
import { Renderer, RendererView } from "../renderers/renderer";
import { Range1d } from "../ranges/range1d";
import { HTML } from "../dom/html";
import { ImageLoader } from "../../core/util/image";
import { includes } from "../../core/util/array";
import { logger } from "../../core/logging";
export class TileRendererView extends RendererView {
static __name__ = "TileRendererView";
_tiles = null;
extent;
initial_extent;
_last_height;
_last_width;
map_initialized = false;
render_timer;
prefetch_timer;
connect_signals() {
super.connect_signals();
this.connect(this.model.change, () => this.request_paint());
this.connect(this.model.tile_source.change, () => this.request_paint());
}
force_finished() {
super.force_finished();
if (this._tiles == null) {
this._tiles = [];
}
}
get_extent() {
const { x_range, y_range } = this;
const x_start = x_range.start;
const y_start = y_range.start;
const x_end = x_range.end;
const y_end = y_range.end;
if (!(isFinite(x_start) && isFinite(y_start) && isFinite(x_end) && isFinite(y_end))) {
logger.warn("tile extent is not fully defined");
}
return [x_start, y_start, x_end, y_end];
}
get x_range() {
return this.plot_model.x_range;
}
get y_range() {
return this.plot_model.y_range;
}
_set_data() {
this.extent = this.get_extent();
this._last_height = undefined;
this._last_width = undefined;
}
get attribution() {
return new HTML({ html: [this.model.tile_source.attribution] });
}
_map_data() {
this.initial_extent = this.get_extent();
const { width, height } = this.plot_view.frame.bbox;
const zoom_level = this.model.tile_source.get_level_by_extent(this.initial_extent, height, width);
const new_extent = this.model.tile_source.snap_to_zoom_level(this.initial_extent, height, width, zoom_level);
this.x_range.start = new_extent[0];
this.y_range.start = new_extent[1];
this.x_range.end = new_extent[2];
this.y_range.end = new_extent[3];
if (this.x_range instanceof Range1d) {
this.x_range.reset_start = new_extent[0];
this.x_range.reset_end = new_extent[2];
}
if (this.y_range instanceof Range1d) {
this.y_range.reset_start = new_extent[1];
this.y_range.reset_end = new_extent[3];
}
}
_create_tile(x, y, z, bounds, cache_only = false) {
const quadkey = this.model.tile_source.tile_xyz_to_quadkey(x, y, z);
const cache_key = this.model.tile_source.tile_xyz_to_key(x, y, z);
if (this.model.tile_source.tiles.has(cache_key)) {
return;
}
const [nx, ny, nz] = this.model.tile_source.normalize_xyz(x, y, z);
const src = this.model.tile_source.get_image_url(nx, ny, nz);
const tile = {
img: undefined,
tile_coords: [x, y, z],
normalized_coords: [nx, ny, nz],
quadkey,
cache_key,
bounds,
loaded: false,
finished: false,
x_coord: bounds[0],
y_coord: bounds[3],
};
this.model.tile_source.tiles.set(cache_key, tile);
if (this._tiles == null) {
this._tiles = [];
}
this._tiles.push(tile);
new ImageLoader(src, {
loaded: (img) => {
Object.assign(tile, { img, loaded: true });
if (cache_only) {
tile.finished = true;
this.notify_finished();
}
else {
this.request_paint();
}
},
failed() {
tile.finished = true;
},
});
}
_enforce_aspect_ratio() {
// brute force way of handling resize or sizing_mode event -------------------------------------------------------------
const { width, height } = this.plot_view.frame.bbox;
if (this._last_width !== width || this._last_height !== height) {
const extent = this.get_extent();
const zoom_level = this.model.tile_source.get_level_by_extent(extent, height, width);
const { tile_source } = this.model;
const new_extent = (() => {
const { _last_width, _last_height } = this;
if (_last_width !== undefined && _last_height !== undefined) {
return tile_source.rescale(extent, height, width, _last_height, _last_width);
}
return tile_source.snap_to_zoom_level(extent, height, width, zoom_level);
})();
this.x_range.setv({ start: new_extent[0], end: new_extent[2] });
this.y_range.setv({ start: new_extent[1], end: new_extent[3] });
this.extent = new_extent;
this._last_width = width;
this._last_height = height;
}
}
has_finished() {
if (!super.has_finished()) {
return false;
}
if (this._tiles == null) {
return false;
}
for (const tile of this._tiles) {
if (!tile.finished) {
return false;
}
}
return true;
}
_paint(ctx) {
if (!this.map_initialized) {
this._set_data();
this._map_data();
this.map_initialized = true;
}
this._enforce_aspect_ratio();
this._update(ctx);
if (this.prefetch_timer != null) {
clearTimeout(this.prefetch_timer);
}
this.prefetch_timer = setTimeout(this._prefetch_tiles.bind(this), 500);
if (this.has_finished()) {
this.notify_finished();
}
}
_draw_tile(ctx, tile_key) {
const tile_data = this.model.tile_source.tiles.get(tile_key);
if (tile_data != null && tile_data.loaded) {
const [[sxmin], [symin]] = this.coordinates.map_to_screen([tile_data.bounds[0]], [tile_data.bounds[3]]);
const [[sxmax], [symax]] = this.coordinates.map_to_screen([tile_data.bounds[2]], [tile_data.bounds[1]]);
const sw = sxmax - sxmin;
const sh = symax - symin;
const sx = sxmin;
const sy = symin;
const old_smoothing = ctx.imageSmoothingEnabled;
ctx.imageSmoothingEnabled = this.model.smoothing;
ctx.drawImage(tile_data.img, sx, sy, sw, sh);
ctx.imageSmoothingEnabled = old_smoothing;
tile_data.finished = true;
}
}
_set_rect(ctx) {
const outline_width = this.plot_model.outline_line_width;
const l = this.plot_view.frame.bbox.left + (outline_width / 2);
const t = this.plot_view.frame.bbox.top + (outline_width / 2);
const w = this.plot_view.frame.bbox.width - outline_width;
const h = this.plot_view.frame.bbox.height - outline_width;
ctx.rect(l, t, w, h);
ctx.clip();
}
_render_tiles(ctx, tile_keys) {
ctx.save();
this._set_rect(ctx);
ctx.globalAlpha = this.model.alpha;
for (const tile_key of tile_keys) {
this._draw_tile(ctx, tile_key);
}
ctx.restore();
}
_prefetch_tiles() {
const { tile_source } = this.model;
const extent = this.get_extent();
const w = this.plot_view.frame.bbox.width;
const h = this.plot_view.frame.bbox.height;
const zoom_level = this.model.tile_source.get_level_by_extent(extent, h, w);
const tiles = this.model.tile_source.get_tiles_by_extent(extent, zoom_level);
for (let t = 0, end = Math.min(10, tiles.length); t < end; t++) {
const [x, y, z] = tiles[t];
const children = this.model.tile_source.children_by_tile_xyz(x, y, z);
for (const c of children) {
const [cx, cy, cz, cbounds] = c;
if (tile_source.tiles.has(tile_source.tile_xyz_to_key(cx, cy, cz))) {
continue;
}
else {
this._create_tile(cx, cy, cz, cbounds, true);
}
}
}
}
_fetch_tiles(tiles) {
for (const tile of tiles) {
const [x, y, z, bounds] = tile;
this._create_tile(x, y, z, bounds);
}
}
_update(ctx) {
const { tile_source } = this.model;
const { min_zoom } = tile_source;
const { max_zoom } = tile_source;
let extent = this.get_extent();
const zooming_out = (this.extent[2] - this.extent[0]) < (extent[2] - extent[0]);
const w = this.plot_view.frame.bbox.width;
const h = this.plot_view.frame.bbox.height;
let zoom_level = tile_source.get_level_by_extent(extent, h, w);
let snap_back = false;
if (zoom_level < min_zoom) {
extent = this.extent;
zoom_level = min_zoom;
snap_back = true;
}
else if (zoom_level > max_zoom) {
extent = this.extent;
zoom_level = max_zoom;
snap_back = true;
}
if (snap_back) {
this.x_range.setv({ start: extent[0], end: extent[2] });
this.y_range.setv({ start: extent[1], end: extent[3] });
}
this.extent = extent;
const tiles = tile_source.get_tiles_by_extent(extent, zoom_level);
const need_load = [];
const cached = [];
const parents = [];
const children = [];
for (const t of tiles) {
const [x, y, z] = t;
const key = tile_source.tile_xyz_to_key(x, y, z);
const tile = tile_source.tiles.get(key);
if (tile != null && tile.loaded) {
cached.push(key);
}
else {
if (this.model.render_parents) {
const [px, py, pz] = tile_source.get_closest_parent_by_tile_xyz(x, y, z);
const parent_key = tile_source.tile_xyz_to_key(px, py, pz);
const parent_tile = tile_source.tiles.get(parent_key);
if ((parent_tile != null) && parent_tile.loaded && !includes(parents, parent_key)) {
parents.push(parent_key);
}
if (zooming_out) {
const child_tiles = tile_source.children_by_tile_xyz(x, y, z);
for (const [cx, cy, cz] of child_tiles) {
const child_key = tile_source.tile_xyz_to_key(cx, cy, cz);
if (tile_source.tiles.has(child_key)) {
children.push(child_key);
}
}
}
}
}
if (tile == null) {
need_load.push(t);
}
}
// draw stand-in parents ----------
this._render_tiles(ctx, parents);
this._render_tiles(ctx, children);
// draw cached ----------
this._render_tiles(ctx, cached);
// fetch missing -------
if (this.render_timer != null) {
clearTimeout(this.render_timer);
}
this.render_timer = setTimeout((() => this._fetch_tiles(need_load)), 65);
}
}
export class TileRenderer extends Renderer {
static __name__ = "TileRenderer";
constructor(attrs) {
super(attrs);
}
static {
this.prototype.default_view = TileRendererView;
this.define(({ Bool, Float, Ref }) => ({
alpha: [Float, 1.0],
smoothing: [Bool, true],
tile_source: [Ref(TileSource), () => new WMTSTileSource()],
render_parents: [Bool, true],
}));
this.override({
level: "image",
});
}
}
//# sourceMappingURL=tile_renderer.js.map