gstreamer/subprojects/gst-devtools/dots-viewer/static/js/jquery.graphviz.svg.js
Thibault Saunier 61159bd992 devtools: Add dots-viewer set of tools
This adds `gstdump` and `gst-dots-viewer` server, see the
README for more details about what those tools do.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7999>
2025-02-15 18:01:36 +00:00

593 lines
17 KiB
JavaScript
Executable file

/*
* Copyright (c) 2015 Mountainstorm
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
+function ($) {
'use strict'
// Cross Browser starts/endsWith support
// =====================================
String.prototype.startsWith = function (prefix) {
return this.indexOf(prefix) == 0;
};
String.prototype.endsWith = function (suffix) {
return this.indexOf(suffix, this.length - suffix.length) !== -1;
};
// GRAPHVIZSVG PUBLIC CLASS DEFINITION
// ===================================
var GraphvizSvg = function (element, options) {
this.type = null
this.options = null
this.enabled = null
this.$element = null
this.init('graphviz.svg', element, options)
}
GraphvizSvg.VERSION = '1.0.1'
GraphvizSvg.GVPT_2_PX = 32.5 // used to ease removal of extra space
GraphvizSvg.DEFAULTS = {
url: null,
svg: null,
shrink: '0.10pt',
tooltips: {
init: function ($graph) {
var $a = $(this)
$a.tooltip({
container: $graph,
placement: 'auto left',
animation: false,
viewport: null
}).on('hide.bs.tooltip', function () {
// keep them visible even if you acidentally mouse over
if ($a.attr('data-tooltip-keepvisible')) {
return false
}
})
},
show: function () {
var $a = $(this)
$a.attr('data-tooltip-keepvisible', true)
$a.tooltip('show')
},
hide: function () {
var $a = $(this)
$a.removeAttr('data-tooltip-keepvisible')
$a.tooltip('hide')
},
update: function () {
var $this = $(this)
if ($this.attr('data-tooltip-keepvisible')) {
$this.tooltip('show')
return
}
}
},
zoom: true,
highlight: {
selected: function (col, bg) {
return col
},
unselected: function (col, bg) {
return jQuery.Color(col).transition(bg, 0.9)
}
},
ready: null
}
GraphvizSvg.prototype.onMouseUpdate = function (e) {
this.mousePosition = {
x: e.pageX,
y: e.pageY
};
}
GraphvizSvg.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
if (options.url) {
var that = this
$.get(options.url, null, function (data) {
var svg = $("svg", data)
that.$element.html(document.adoptNode(svg[0]))
that.setup()
}, "xml")
} else {
if (options.svg) {
this.$element.html(options.svg)
}
this.setup()
}
document.addEventListener('mousemove', this.onMouseUpdate.bind(this), false);
document.addEventListener('mouseenter', this.onMouseUpdate.bind(this), false);
}
GraphvizSvg.prototype.getDefaults = function () {
return GraphvizSvg.DEFAULTS
}
GraphvizSvg.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.shrink) {
if (typeof options.shrink != 'object') {
options.shrink = {
x: options.shrink,
y: options.shrink
}
}
options.shrink.x = this.convertToPx(options.shrink.x)
options.shrink.y = this.convertToPx(options.shrink.y)
}
return options
}
GraphvizSvg.prototype.setup = function () {
var options = this.options
// save key elements in the graph for easy access
var $svg = $(this.$element.children('svg'))
var $graph = $svg.children('g:first')
this.$svg = $svg
this.$graph = $graph
this.$background = $graph.children('polygon:first') // might not exist
this.$nodes = $graph.children('.node')
this.$edges = $graph.children('.edge')
this._nodesByName = {}
this._edgesByName = {}
// add top level class and copy background color to element
this.$element.addClass('graphviz-svg')
if (this.$background.length) {
this.$element.css('background', this.$background.attr('fill'))
}
// setup all the nodes and edges
var that = this
this.$nodes.each(function () {that.setupNodesEdges($(this), true)})
this.$edges.each(function () {that.setupNodesEdges($(this), false)})
// remove the graph title element
var $title = this.$graph.children('title')
this.$graph.attr('data-name', $title.text())
$title.remove()
if (options.zoom) {
this.setupZoom()
}
// tell people we're done
if (options.ready) {
options.ready.call(this)
}
}
GraphvizSvg.prototype.setupNodesEdges = function ($el, isNode) {
var that = this
var options = this.options
// save the colors of the paths, ellipses and polygons
$el.find('polygon, ellipse, path').each(function () {
var $this = $(this)
// save original colors
$this.data('graphviz.svg.color', {
fill: $this.attr('fill'),
stroke: $this.attr('stroke')
})
// shrink it if it's a node
if (isNode && options.shrink) {
that.scaleNode($this)
}
})
// save the node name and check if theres a comment above; save it
var $title = $el.children('title')
if ($title[0]) {
// remove any compass points:
var title = $title.text().replace(/:[snew][ew]?/g, '')
$el.attr('data-name', title)
$title.remove()
if (isNode) {
this._nodesByName[title] = $el[0]
} else {
this._edgesByName[title] = $el[0]
}
// without a title we can't tell if its a user comment or not
var previousSibling = $el[0].previousSibling
while (previousSibling && previousSibling.nodeType != 8) {
previousSibling = previousSibling.previousSibling
}
if (previousSibling != null && previousSibling.nodeType == 8) {
var htmlDecode = function (input) {
var e = document.createElement('div')
e.innerHTML = input
return e.childNodes[0].nodeValue
}
var value = htmlDecode(previousSibling.nodeValue.trim())
if (value != title) {
// user added comment
$el.attr('data-comment', value)
}
}
}
// remove namespace from a[xlink:title]
$el.children('a').filter(function () {return $(this).attr('xlink:title')}).each(function () {
var $a = $(this)
$a.attr('title', $a.attr('xlink:title'))
$a.removeAttr('xlink:title')
if (options.tooltips) {
options.tooltips.init.call(this, that.$element)
}
})
}
GraphvizSvg.prototype.setupZoom = function () {
var that = this
var $element = this.$element
var $svg = this.$svg
this.zoom = {width: $svg.attr('width'), height: $svg.attr('height'), percentage: null}
this.scaleView(100.0)
$element.mousewheel(function (evt) {
if (evt.ctrlKey) {
var percentage = that.zoom.percentage
percentage += evt.deltaY * evt.deltaFactor
if (percentage < 100.0) {
percentage = 100.0
}
// get pointer offset in view
// ratio offset within svg
var dx = evt.pageX - $svg.offset().left
var dy = evt.pageY - $svg.offset().top
var rx = dx / $svg.width()
var ry = dy / $svg.height()
// offset within frame ($element)
var px = evt.pageX - $element.offset().left
var py = evt.pageY - $element.offset().top
that.scaleView(percentage)
// scroll so pointer is still in same place
$element.scrollLeft((rx * $svg.width()) + 0.5 - px)
$element.scrollTop((ry * $svg.height()) + 0.5 - py)
return false // stop propogation
}
})
$element.on("keydown", function (evt) {
console.log(evt)
if (evt.shiftKey) {
$element.css('cursor', 'move')
}
})
$element.on("keyup", function (evt) {
console.log(evt)
if (evt.shiftKey) {
$element.css('cursor', 'auto')
}
})
$element
.on('mousemove', function (e) {
var $svg = this.$svg
$element.css({'transform-origin': ((e.pageX - $(this).offset().left) / $(this).width()) * 100 + '% ' + ((e.pageY - $(this).offset().top) / $(this).height()) * 100 + '%'});
})
}
GraphvizSvg.prototype.scaleView = function (percentage) {
var that = this
var $svg = this.$svg
$svg.attr('width', percentage + '%')
$svg.attr('height', percentage + '%')
this.zoom.percentage = percentage
// now callback to update tooltip position
var $everything = this.$nodes.add(this.$edges)
$everything.children('a[title]').each(function () {
that.options.tooltips.update.call(this)
})
}
GraphvizSvg.prototype.scaleInView = function (percentage) {
var that = this
var $svg = this.$svg
var $element = this.$element
// get pointer offset in view
// ratio offset within svg
var dx = this.mousePosition.x - $svg.offset().left
var dy = this.mousePosition.y - $svg.offset().top
var rx = dx / $svg.width()
var ry = dy / $svg.height()
// offset within frame ($element)
var px = this.mousePosition.x - $element.offset().left
var py = this.mousePosition.y - $element.offset().top
$svg.attr('width', percentage + '%')
$svg.attr('height', percentage + '%')
this.zoom.percentage = percentage
// now callback to update tooltip position
var $everything = this.$nodes.add(this.$edges)
$everything.children('a[title]').each(function () {
that.options.tooltips.update.call(this)
})
// scroll so pointer is still in same place
$element.scrollLeft((rx * $svg.width()) + 0.5 - px)
$element.scrollTop((ry * $svg.height()) + 0.5 - py)
}
GraphvizSvg.prototype.scaleNode = function ($node) {
var dx = this.options.shrink.x
var dy = this.options.shrink.y
var tagName = $node.prop('tagName')
if (tagName == 'ellipse') {
$node.attr('rx', parseFloat($node.attr('rx')) - dx)
$node.attr('ry', parseFloat($node.attr('ry')) - dy)
} else if (tagName == 'polygon') {
// this is more complex - we need to scale it manually
var bbox = $node[0].getBBox()
var cx = bbox.x + (bbox.width / 2)
var cy = bbox.y + (bbox.height / 2)
var pts = $node.attr('points').split(' ')
var points = '' // new value
for (var i in pts) {
var xy = pts[i].split(',')
var ox = parseFloat(xy[0])
var oy = parseFloat(xy[1])
points += (((cx - ox) / (bbox.width / 2) * dx) + ox) +
',' +
(((cy - oy) / (bbox.height / 2) * dy) + oy) +
' '
}
$node.attr('points', points)
}
}
GraphvizSvg.prototype.convertToPx = function (val) {
var retval = val
if (typeof val == 'string') {
var end = val.length
var factor = 1.0
if (val.endsWith('px')) {
end -= 2
} else if (val.endsWith('pt')) {
end -= 2
factor = GraphvizSvg.GVPT_2_PX
}
retval = parseFloat(val.substring(0, end)) * factor
}
return retval
}
GraphvizSvg.prototype.findEdge = function (nodeName, testEdge, $retval) {
var retval = []
for (var name in this._edgesByName) {
var match = testEdge(nodeName, name)
if (match) {
if ($retval) {
$retval.push(this._edgesByName[name])
}
retval.push(match)
}
}
return retval
}
GraphvizSvg.prototype.findLinked = function (node, includeEdges, testEdge, $retval) {
var that = this
var $node = $(node)
var $edges = null
if (includeEdges) {
$edges = $retval
}
var names = this.findEdge($node.attr('data-name'), testEdge, $edges)
for (var i in names) {
var n = this._nodesByName[names[i]]
if (!$retval.is(n)) {
$retval.push(n)
that.findLinked(n, includeEdges, testEdge, $retval)
}
}
}
GraphvizSvg.prototype.colorElement = function ($el, getColor) {
var bg = this.$element.css('background')
$el.find('polygon, ellipse, path').each(function () {
var $this = $(this)
var color = $this.data('graphviz.svg.color')
if (color.fill && $this.prop('tagName') != 'path') {
$this.attr('fill', getColor(color.fill, bg)) // don't set fill if it's a path
}
if (color.stroke) {
$this.attr('stroke', getColor(color.stroke, bg))
}
})
}
GraphvizSvg.prototype.restoreElement = function ($el) {
$el.find('polygon, ellipse, path').each(function () {
var $this = $(this)
var color = $this.data('graphviz.svg.color')
if (color.fill) {
$this.attr('fill', color.fill) // don't set fill if it's a path
}
if (color.stroke) {
$this.attr('stroke', color.stroke)
}
})
}
// methods users can actually call
GraphvizSvg.prototype.nodes = function () {
return this.$nodes
}
GraphvizSvg.prototype.edges = function () {
return this.$edges
}
GraphvizSvg.prototype.nodesByName = function () {
return this._nodesByName
}
GraphvizSvg.prototype.edgesByName = function () {
return this._edgesByName
}
GraphvizSvg.prototype.linkedTo = function (node, includeEdges) {
var $retval = $()
this.findLinked(node, includeEdges, function (nodeName, edgeName) {
var other = null;
var match = '->' + nodeName
if (edgeName.endsWith(match)) {
other = edgeName.substring(0, edgeName.length - match.length);
}
return other;
}, $retval)
return $retval
}
GraphvizSvg.prototype.linkedFrom = function (node, includeEdges) {
var $retval = $()
this.findLinked(node, includeEdges, function (nodeName, edgeName) {
var other = null;
var match = nodeName + '->'
if (edgeName.startsWith(match)) {
other = edgeName.substring(match.length);
}
return other;
}, $retval)
return $retval
}
GraphvizSvg.prototype.linked = function (node, includeEdges) {
var $retval = $()
this.findLinked(node, includeEdges, function (nodeName, edgeName) {
return '^' + name + '--(.*)$'
}, $retval)
this.findLinked(node, includeEdges, function (nodeName, edgeName) {
return '^(.*)--' + name + '$'
}, $retval)
return $retval
}
GraphvizSvg.prototype.tooltip = function ($elements, show) {
var that = this
var options = this.options
$elements.each(function () {
$(this).children('a[title]').each(function () {
if (show) {
options.tooltips.show.call(this)
} else {
options.tooltips.hide.call(this)
}
})
})
}
GraphvizSvg.prototype.bringToFront = function ($elements) {
$elements.detach().appendTo(this.$graph)
}
GraphvizSvg.prototype.sendToBack = function ($elements) {
if (this.$background.length) {
$element.insertAfter(this.$background)
} else {
$elements.detach().prependTo(this.$graph)
}
}
GraphvizSvg.prototype.highlight = function ($nodesEdges, tooltips) {
var that = this
var options = this.options
var $everything = this.$nodes.add(this.$edges)
if ($nodesEdges && $nodesEdges.length > 0) {
// create set of all other elements and dim them
$everything.not($nodesEdges).each(function () {
that.colorElement($(this), options.highlight.unselected)
that.tooltip($(this))
})
$nodesEdges.each(function () {
that.colorElement($(this), options.highlight.selected)
})
if (tooltips) {
this.tooltip($nodesEdges, true)
}
} else {
$everything.each(function () {
that.restoreElement($(this))
})
this.tooltip($everything)
}
}
GraphvizSvg.prototype.destroy = function () {
var that = this
this.hide(function () {
that.$element.off('.' + that.type).removeData(that.type)
})
}
// GRAPHVIZSVG PLUGIN DEFINITION
// =============================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('graphviz.svg')
var options = typeof option == 'object' && option
if (!data && /destroy/.test(option)) return
if (!data) $this.data('graphviz.svg', (data = new GraphvizSvg(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.graphviz
$.fn.graphviz = Plugin
$.fn.graphviz.Constructor = GraphvizSvg
// GRAPHVIZ NO CONFLICT
// ====================
$.fn.graphviz.noConflict = function () {
$.fn.graphviz = old
return this
}
}(jQuery)