UNPKG

@bokeh/bokehjs

Version:

Interactive, novel data visualization

240 lines 8.62 kB
import { XYGlyph, XYGlyphView } from "./xy_glyph"; import { inherit } from "./glyph"; import { ScreenArray, to_screen, Indices } from "../../core/types"; import { Anchor } from "../../core/enums"; import * as p from "../../core/properties"; import { resize } from "../../core/util/array"; import { minmax2 } from "../../core/util/arrayable"; import { ImageLoader } from "../../core/util/image"; import * as resolve from "../common/resolve"; export class ImageURLView extends XYGlyphView { static __name__ = "ImageURLView"; _images_rendered = false; _bounds_rect; anchor; /*protected*/ image = new Array(0); loaders; resolved; connect_signals() { super.connect_signals(); this.connect(this.model.properties.global_alpha.change, () => this.renderer.request_paint()); } _index_data(index) { const { data_size } = this; for (let i = 0; i < data_size; i++) { // TODO: add a proper implementation (same as ImageBase?) index.add_empty(); } } _set_data_iteration = 0; _set_data() { if (this.inherited_url) { return; } this._set_data_iteration++; const { url } = this; const n_url = url.length; this.image = resize(this.image, n_url, null); this.loaders = new Array(n_url).fill(null); this.resolved = new Indices(n_url); const { retry_attempts, retry_timeout } = this.model; const { _set_data_iteration } = this; for (let i = 0; i < n_url; i++) { const url_i = url.get(i); if (url_i == "") { continue; } const loader = new ImageLoader(url_i, { loaded: (image_i) => { if (this._set_data_iteration == _set_data_iteration && !this.resolved.get(i)) { this.resolved.set(i); this.image[i] = image_i; this.loaders[i] = null; this.renderer.request_paint(); } }, failed: () => { if (this._set_data_iteration == _set_data_iteration) { this.resolved.set(i); this.loaders[i] = null; const image_i = this.image[i]; if (image_i != null) { this.image[i] = null; this.renderer.request_paint(); } } }, attempts: retry_attempts + 1, timeout: retry_timeout, }); this.loaders[i] = loader; } const w_data = this.model.properties.w.units == "data"; const h_data = this.model.properties.h.units == "data"; const n = this.data_size; const xs = new ScreenArray(w_data ? 2 * n : n); const ys = new ScreenArray(h_data ? 2 * n : n); this.anchor = resolve.anchor(this.model.anchor); const { x: x_anchor, y: y_anchor } = this.anchor; function x0x1(x, w) { const x0 = x - x_anchor * w; return [x0, x0 + w]; } function y0y1(y, h) { const y0 = y + y_anchor * h; return [y0, y0 - h]; } // if the width/height are in screen units, don't try to include them in bounds if (w_data) { for (let i = 0; i < n; i++) { [xs[i], xs[n + i]] = x0x1(this.x[i], this.w.get(i) ?? 0); } } else { xs.set(this.x, 0); } if (h_data) { for (let i = 0; i < n; i++) { [ys[i], ys[n + i]] = y0y1(this.y[i], this.h.get(i) ?? 0); } } else { ys.set(this.y, 0); } const [x0, x1, y0, y1] = minmax2(xs, ys); this._bounds_rect = { x0, x1, y0, y1 }; } has_finished() { return super.has_finished() && this._images_rendered; } _map_data() { const w = () => this.w.map((w_i) => w_i ?? NaN); const h = () => this.h.map((h_i) => h_i ?? NaN); this._define_or_inherit_attr("sw", () => { if (this.model.properties.w.units == "data") { if (this.inherited_x && this.inherited_w) { return inherit; } else { return this.sdist(this.renderer.xscale, this.x, w(), "edge", this.model.dilate); } } else { return this.inherited_w ? inherit : to_screen(w()); } }); this._define_or_inherit_attr("sh", () => { if (this.model.properties.h.units == "data") { if (this.inherited_y && this.inherited_h) { return inherit; } else { return this.sdist(this.renderer.yscale, this.y, h(), "edge", this.model.dilate); } } else { return this.inherited_h ? inherit : to_screen(h()); } }); } _paint(ctx, indices, data) { const { sx, sy, sw, sh, angle, global_alpha } = { ...this, ...data }; const { image, loaders, resolved } = this; // TODO (bev): take actual border width into account when clipping const { frame } = this.renderer.plot_view; const { left, top, width, height } = frame.bbox; ctx.beginPath(); ctx.rect(left + 1, top + 1, width - 2, height - 2); ctx.clip(); let finished = true; for (const i of indices) { const loader_i = loaders[i]; if (!isFinite(sx[i] + sy[i] + angle.get(i) + global_alpha.get(i))) { continue; } if (!resolved.get(i)) { if (loader_i != null && loader_i.image.complete) { image[i] = loader_i.image; loaders[i] = null; resolved.set(i); } else { finished = false; } } const image_i = image[i]; if (image_i == null) { continue; } if (image_i.naturalWidth == 0 && image_i.naturalHeight == 0) { // dumb way of detecting broken images continue; } this._render_image(ctx, i, image_i, sx, sy, sw, sh, angle, global_alpha); } if (finished && !this._images_rendered) { this._images_rendered = true; this.notify_finished(); } } _render_image(ctx, i, image, sx, sy, sw, sh, angle, alpha) { if (!isFinite(sw[i])) { sw[i] = image.width; } if (!isFinite(sh[i])) { sh[i] = image.height; } const sw_i = sw[i]; const sh_i = sh[i]; const { anchor } = this; const dx_i = anchor.x * sw_i; const dy_i = anchor.y * sh_i; const sx_i = sx[i] - dx_i; const sy_i = sy[i] - dy_i; const angle_i = angle.get(i); const alpha_i = alpha.get(i); ctx.save(); ctx.globalAlpha = alpha_i; const sw2 = sw_i / 2; const sh2 = sh_i / 2; if (angle_i != 0) { ctx.translate(sx_i, sy_i); //rotation about center of image ctx.translate(sw2, sh2); ctx.rotate(angle_i); ctx.translate(-sw2, -sh2); ctx.drawImage(image, 0, 0, sw_i, sh_i); ctx.translate(sw2, sh2); ctx.rotate(-angle_i); ctx.translate(-sw2, -sh2); ctx.translate(-sx_i, -sy_i); } else { ctx.drawImage(image, sx_i, sy_i, sw_i, sh_i); } ctx.restore(); } bounds() { return this._bounds_rect; } } export class ImageURL extends XYGlyph { static __name__ = "ImageURL"; constructor(attrs) { super(attrs); } static { this.prototype.default_view = ImageURLView; this.define(({ Bool, Int }) => ({ url: [p.StringSpec, { field: "url" }], anchor: [Anchor, "top_left"], global_alpha: [p.NumberSpec, { value: 1.0 }], angle: [p.AngleSpec, 0], w: [p.NullDistanceSpec, null], h: [p.NullDistanceSpec, null], dilate: [Bool, false], retry_attempts: [Int, 0], retry_timeout: [Int, 0], })); } } //# sourceMappingURL=image_url.js.map