@bokeh/bokehjs
Version:
Interactive, novel data visualization
240 lines • 8.62 kB
JavaScript
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