///
///
module tf.scene {
/** Show minimap when the viewpoint area is less than X% of the whole area. */
const FRAC_VIEWPOINT_AREA: number = 0.8;
export class Minimap {
/** The minimap container. */
private minimap: HTMLElement;
/** The canvas used for drawing the mini version of the svg. */
private canvas: HTMLCanvasElement;
/** A buffer canvas used for temporary drawing to avoid flickering. */
private canvasBuffer: HTMLCanvasElement;
/** The minimap svg used for holding the viewpoint rectangle. */
private minimapSvg: SVGSVGElement;
/** The rectangle showing the current viewpoint. */
private viewpoint: SVGRectElement;
/**
* The scale factor for the minimap. The factor is determined automatically
* so that the minimap doesn't violate the maximum width/height specified
* in the constructor. The minimap maintains the same aspect ratio as the
* original svg.
*/
private scaleMinimap: number;
/** The main svg element. */
private svg: SVGSVGElement;
/** The svg group used for panning and zooming the main svg. */
private zoomG: SVGGElement;
/** The zoom behavior of the main svg. */
private mainZoom: d3.behavior.Zoom;
/** The maximum width and height for the minimap. */
private maxWandH: number;
/** The last translation vector used in the main svg. */
private translate: [number, number];
/** The last scaling factor used in the main svg. */
private scaleMain: number;
/** The coordinates of the viewpoint rectangle. */
private viewpointCoord: {x: number, y: number};
/** The current size of the minimap */
private minimapSize: {width: number, height: number};
/** Padding (px) due to the main labels of the graph. */
private labelPadding: number;
/**
* Constructs a new minimap.
*
* @param svg The main svg element.
* @param zoomG The svg group used for panning and zooming the main svg.
* @param mainZoom The main zoom behavior.
* @param minimap The minimap container.
* @param maxWandH The maximum width/height for the minimap.
* @param labelPadding Padding in pixels due to the main graph labels.
*/
constructor(svg: SVGSVGElement, zoomG: SVGGElement,
mainZoom: d3.behavior.Zoom, minimap: HTMLElement,
maxWandH: number, labelPadding: number) {
this.svg = svg;
this.labelPadding = labelPadding;
this.zoomG = zoomG;
this.mainZoom = mainZoom;
this.maxWandH = maxWandH;
let $minimap = d3.select(minimap);
// The minimap will have 2 main components: the canvas showing the content
// and an svg showing a rectangle of the currently zoomed/panned viewpoint.
let $minimapSvg = $minimap.select("svg");
// Make the viewpoint rectangle draggable.
let $viewpoint = $minimapSvg.select("rect");
let dragmove = (d) => {
this.viewpointCoord.x = (d3.event).x;
this.viewpointCoord.y = (d3.event).y;
this.updateViewpoint();
};
this.viewpointCoord = {x: 0, y: 0};
let drag = d3.behavior.drag().origin(Object).on("drag", dragmove);
$viewpoint.datum(this.viewpointCoord).call(drag);
// Make the minimap clickable.
$minimapSvg.on("click", () => {
if ((d3.event).defaultPrevented) {
// This click was part of a drag event, so suppress it.
return;
}
// Update the coordinates of the viewpoint.
let width = Number($viewpoint.attr("width"));
let height = Number($viewpoint.attr("height"));
let clickCoords = d3.mouse($minimapSvg.node());
this.viewpointCoord.x = clickCoords[0] - width / 2;
this.viewpointCoord.y = clickCoords[1] - height / 2;
this.updateViewpoint();
});
this.viewpoint = $viewpoint.node();
this.minimapSvg = $minimapSvg.node();
this.minimap = minimap;
this.canvas = $minimap.select("canvas.first").node();
this.canvasBuffer =
$minimap.select("canvas.second").node();
}
/**
* Updates the position and the size of the viewpoint rectangle.
* It also notifies the main svg about the new panned position.
*/
private updateViewpoint(): void {
// Update the coordinates of the viewpoint rectangle.
d3.select(this.viewpoint)
.attr("x", this.viewpointCoord.x)
.attr("y", this.viewpointCoord.y);
// Update the translation vector of the main svg to reflect the
// new viewpoint.
let mainX = - this.viewpointCoord.x * this.scaleMain / this.scaleMinimap;
let mainY = - this.viewpointCoord.y * this.scaleMain / this.scaleMinimap;
let zoomEvent = this.mainZoom.translate([mainX, mainY]).event;
d3.select(this.zoomG).call(zoomEvent);
}
/**
* Redraws the minimap. Should be called whenever the main svg
* was updated (e.g. when a node was expanded).
*/
update(): void {
let $svg = d3.select(this.svg);
// Read all the style rules in the document and embed them into the svg.
// The svg needs to be self contained, i.e. all the style rules need to be
// embedded so the canvas output matches the origin.
let stylesText = "";
for (let k = 0; k < document.styleSheets.length; k++) {
try {
let cssRules = (document.styleSheets[k]).cssRules ||
(document.styleSheets[k]).rules;
if (cssRules == null) {
continue;
}
for (let i = 0; i < cssRules.length; i++) {
stylesText += cssRules[i].cssText + "\n";
}
} catch (e) {
if (e.name !== "SecurityError") {
throw e;
}
}
}
// Temporarily add the css rules to the main svg.
let svgStyle = $svg.append("style");
svgStyle.text(stylesText);
// Temporarily remove the zoom/pan transform from the main svg since we
// want the minimap to show a zoomed-out and centered view.
let $zoomG = d3.select(this.zoomG);
let zoomTransform = $zoomG.attr("transform");
$zoomG.attr("transform", null);
// Get the size of the entire scene.
let sceneSize = this.zoomG.getBBox();
// Since we add padding, account for that here.
sceneSize.height += this.labelPadding;
// Temporarily assign an explicit width/height to the main svg, since
// it doesn't have one (uses flex-box), but we need it for the canvas
// to work.
$svg.attr({
width: sceneSize.width,
height: sceneSize.height,
});
// Since the content inside the svg changed (e.g. a node was expanded),
// the aspect ratio have also changed. Thus, we need to update the scale
// factor of the minimap. The scale factor is determined such that both
// the width and height of the minimap are <= maximum specified w/h.
this.scaleMinimap =
this.maxWandH / Math.max(sceneSize.width, sceneSize.height);
this.minimapSize = {
width: sceneSize.width * this.scaleMinimap,
height: sceneSize.height * this.scaleMinimap
};
// Update the size of the minimap's svg, the buffer canvas and the
// viewpoint rect.
d3.select(this.minimapSvg).attr(this.minimapSize);
d3.select(this.canvasBuffer).attr(this.minimapSize);
if (this.translate != null && this.zoom != null) {
// Update the viewpoint rectangle shape since the aspect ratio of the
// map has changed.
requestAnimationFrame(() => this.zoom());
}
// Serialize the main svg to a string which will be used as the rendering
// content for the canvas.
let svgXml = (new XMLSerializer()).serializeToString(this.svg);
// Now that the svg is serialized for rendering, remove the temporarily
// assigned styles, explicit width and height and bring back the pan/zoom
// transform.
svgStyle.remove();
$svg.attr({
width: null,
height: null
});
$zoomG.attr("transform", zoomTransform);
let image = new Image();
image.onload = () => {
// Draw the svg content onto the buffer canvas.
let context = this.canvasBuffer.getContext("2d");
context.clearRect(0, 0, this.canvasBuffer.width,
this.canvasBuffer.height);
context.drawImage(image, 0, 0,
this.minimapSize.width, this.minimapSize.height);
requestAnimationFrame(() => {
// Hide the old canvas and show the new buffer canvas.
d3.select(this.canvasBuffer).style("display", null);
d3.select(this.canvas).style("display", "none");
// Swap the two canvases.
[this.canvas, this.canvasBuffer] = [this.canvasBuffer, this.canvas];
});
};
image.src = "data:image/svg+xml;base64," + btoa(svgXml);
}
/**
* Handles changes in zooming/panning. Should be called from the main svg
* to notify that a zoom/pan was performed and this minimap will update it's
* viewpoint rectangle.
*
* @param translate The translate vector, or none to use the last used one.
* @param scale The scaling factor, or none to use the last used one.
*/
zoom(translate?: [number, number], scale?: number): void {
// Update the new translate and scale params, only if specified.
this.translate = translate || this.translate;
this.scaleMain = scale || this.scaleMain;
// Update the location of the viewpoint rectangle.
let svgRect = this.svg.getBoundingClientRect();
let $viewpoint = d3.select(this.viewpoint);
this.viewpointCoord.x = -this.translate[0] * this.scaleMinimap /
this.scaleMain;
this.viewpointCoord.y = -this.translate[1] * this.scaleMinimap /
this.scaleMain;
let viewpointWidth = svgRect.width * this.scaleMinimap / this.scaleMain;
let viewpointHeight = svgRect.height * this.scaleMinimap / this.scaleMain;
$viewpoint.attr({
x: this.viewpointCoord.x,
y: this.viewpointCoord.y,
width: viewpointWidth,
height: viewpointHeight
});
// Show/hide the minimap depending on the viewpoint area as fraction of the
// whole minimap.
let mapWidth = this.minimapSize.width;
let mapHeight = this.minimapSize.height;
let x = this.viewpointCoord.x;
let y = this.viewpointCoord.y;
let w = Math.min(Math.max(0, x + viewpointWidth), mapWidth) -
Math.min(Math.max(0, x), mapWidth);
let h = Math.min(Math.max(0, y + viewpointHeight), mapHeight) -
Math.min(Math.max(0, y), mapHeight);
let fracIntersect = (w * h) / (mapWidth * mapHeight);
if (fracIntersect < FRAC_VIEWPOINT_AREA) {
this.minimap.classList.remove("hidden");
} else {
this.minimap.classList.add("hidden");
}
}
}
} // close module tf.scene