UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

316 lines 12.2 kB
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