var DiagramViewer = Class.create(JSControl, { initialize: function($super, element){ $super(element); this.loader = new RequestLoader(); this.renderer = Element.readAttribute(this.element, "renderer") || "graphviz"; Element.setStyle(this.element, {"padding": "0px"}); if (this.renderer == "simple") Element.setStyle(this.element, {"overflow": "hidden"}); this.loadStatic(); new MouseObserver(this.element); this.diagram_processor = new DiagramProcessor(MouseObserver.events, MouseObserver.triggers_document, this.renderer); Event.observe(this.element, DiagramProcessor.events["new-edge"], this.onnewlink.bindAsEventListener(this)); Event.observe(this.element, DiagramProcessor.events["node-click"], this.onentityclick.bindAsEventListener(this)); Event.observe(this.element, DiagramProcessor.events["edge-click"], this.onrelationclick.bindAsEventListener(this)); Event.observe(this.element, DiagramProcessor.events["null-click"], this.onnullclick.bindAsEventListener(this)); this.panel = Element.previousSiblings(this.element).find(function(element){ return Element.hasClassName(element, "DiagramPanel"); }); if (this.panel) Event.observe(this.panel, "click", this.onpanelclick.bindAsEventListener(this)); this.awindow = AControl.GetParentWindow(this.ID()); this.awindow.RegisterResizeableChild(this.ID()); this.awindow.Subscribe("DIAGRAM:REFRESH", this.onrefresh, this); }, loadStatic: function(){ if (!SVGElement.IsSupported) return; switch(this.renderer){ case "graphviz": this.loader.AddRequest({ "id": "xsl-svg-graphviz", "src": "/xslt/diagram-svg-graphviz.xsl", "static": true }); break; case "simple": default: this.loader.AddRequest({ "id": "xsl-intermediate", "src": "/xslt/diagram-intermediate.xsl", "static": true }); this.loader.AddRequest({ "id": "xsl-svg", "src": "/xslt/diagram-svg.xsl", "static": true }); break; } }, getURL: function(format){ if (typeof this.url == "undefined") return ""; var url = String(this.url); var ctx = URLSerializeVS({"__context": {"Format": format}}); var qind = url.indexOf("?"); if (qind != -1) return url.substr(0, qind + 1) + ctx + url.substr(qind + 1); else return url + "?" + ctx; }, onrefresh: function(aevent){ if (!aevent || !Object.isFunction(aevent.Data) || aevent.Data()["id"] != Element.readAttribute(this.element, "localid")) return; this.reload(); }, reload: function(){ if (!SVGElement.IsSupported){ this.updateDimensions(); this.updateContent(new Element("img", {"src": this.getURL("png")})); Element.setStyle(this.element, {"visibility": "visible"}); if (Prototype.Browser.IE) this.updateDimensions.bind(this).delay(.2); return; } switch (this.renderer){ case "simple": this.loader.ClearRequests(["DBData", "DBDataToIntermediate", "SVGOutput"]); this.intermediate_document = null; this.loader.AddRequest({ "id": "DBData", "src": this.url }); this.loader.AddRequest({ "id": "DBDataToIntermediate", "required": ["DBData", "xsl-intermediate"], "action": function(){ var dbdataxml = this.loader.GetRequest("DBData").GetResponseXML(); var preprocxml = this.loader.GetRequest("xsl-intermediate").GetResponseXML(); this.intermediate_document = JSControl.TransformToDocument(dbdataxml, preprocxml); if (this.intermediate_document) XMLElement.WriteAttributes(this.intermediate_document.documentElement, this.getDimensions()); }.bind(this) }); this.loader.AddRequest({ "id": "SVGOutput", "required": ["DBDataToIntermediate", "xsl-svg"], "action": this.transformIntermediateToSVG.bind(this) }); break; case "graphviz": default: this.loader.ClearRequests(["SVGImage", "TransformedSVGImage"]); this.loader.AddRequest({ "id": "SVGImage", "src": this.getURL("svg") }); this.loader.AddRequest({ "id": "TransformedSVGImage", "required": ["xsl-svg-graphviz", "SVGImage"], "action": function(){ this.updateDimensions(); Element.setStyle(this.element, {"visibility": "visible"}); var svgimage = this.loader.GetRequest("SVGImage").GetResponseXML(); var svgtransform = this.loader.GetRequest("xsl-svg-graphviz").GetResponseXML(); this.updateContent(JSControl.TransformToDocument(svgimage, svgtransform)); }.bind(this) }); break; } }, updateContent: function(content){ this.diaelem = new DiagramElement(content, this.element, {"renderer": this.renderer}); if (this.diagram_processor) this.diagram_processor.SetDiagramElement(this.diaelem); }, transformIntermediateToSVG: function(){ var svgout = this.loader.GetRequest("xsl-svg").GetResponseXML(); this.updateContent(JSControl.TransformToDocument(this.intermediate_document, svgout)); Element.setStyle(this.element, {"visibility": "visible"}); }, SetURL: function(url){ if (this.url == url) return; this.url = url; this.reload(); }, Value: function(){ return null; }, getEntityID: function(ent){ if (!ent) return ""; return ent.getAttribute("eid"); }, getRelationID: function(rel){ if (!rel) return ""; return rel.getAttribute("rid"); }, onnewlink: function(event){ var entities = event.memo["entities"]; if (entities.all()){ var eids = entities.collect(this.getEntityID, this); if ("simple" === this.renderer){ new XMLElement(this.intermediate_document, "", "relation", {"e1" : eids[0], "e2": eids[1]}).AttachTo(this.intermediate_document.documentElement); this.transformIntermediateToSVG(); } new AEvent("DIAGRAM:NEW-LINK", {"args": {"E1": eids[0], "E2": eids[1]}}, this); }else if (entities[0]) new AEvent("DIAGRAM:NEW-LINK-FROM", {"args": {"E1": this.getEntityID(entities[0])}}, this); }, select_elem: function(elem){ if (this.selem_ == elem) return; if (this.selem_) XMLElement.RemoveClassName(this.selem_, "selected"); if (this.selem_ = elem || null) XMLElement.AddClassName(this.selem_, "selected"); }, is_entity: function(elem){ return elem && XMLElement.HasClassName(this.selem_, "node"); }, is_relation: function(elem){ return elem && XMLElement.HasClassName(this.selem_, "edge"); }, onentityclick: function(event){ // if (this.is_relation(this.selem_)) new AEvent("DIAGRAM:RELATION-SELECTION", {"args": {"ID": optional()}}, this); var ent = event && event.memo ? event.memo["entity"] : null; this.select_elem(ent); var eid = this.getEntityID(ent); new AEvent("DIAGRAM:ENTITY-SELECTION", {"args": {"ID": eid ? eid : optional()}}, this); }, onrelationclick: function(event){ // if (this.is_entity(this.selem_)) new AEvent("DIAGRAM:ENTITY-SELECTION", {"args": {"ID": optional()}}, this); var rel = event && event.memo ? event.memo["relation"] : null; this.select_elem(rel); var rid = this.getRelationID(rel); new AEvent("DIAGRAM:RELATION-SELECTION", {"args": {"ID": rid ? rid : optional()}}, this); }, onnullclick: function(event){ if (this.selem_){ if (this.is_entity(this.selem_)) this.onentityclick(null); else if (this.is_relation(this.selem_)) this.onrelationclick(null); } }, SetAttribute: function($super, attrName, attrValue){ switch (attrName){ case "title": break; case "src": this.SetURL(attrValue); break; case "visibility": if (["yes", "ro", "vo", "link", "no"].indexOf(attrValue) === -1) return; [this.element, this.panel].each(function(elem){ if (elem) Element.setStyle(elem, {"display": "no" === attrValue ? "none" : ""}); }); if (this.diagram_processor) this.diagram_processor.SetVisibility(attrValue); break; default: $super(attrName, attrValue); } }, getDimensions: function(unbounded){ if (!this.grid_frame) this.grid_frame = Element.up(this.element, "div[type='gridFrame']"); var maxdims = { "width": Element.getWidth(this.grid_frame ? this.grid_frame : this.element.parentNode) - // Diagram.GetStyleSum(this.element, ["paddingLeft", "paddingRight", "borderLeftWidth", "borderRightWidth", "marginLeft", "marginRight"]) - 20, "height": this.grid_frame ? Element.getHeight(this.grid_frame) - 5 - (this.element.offsetTop || 0) -// Diagram.GetStyleSum(this.element, ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth", "marginTop", "marginBottom"]) - 65 : "" }; if (!unbounded){ maxdims["width"] = Math.max(maxdims["width"], 600); maxdims["height"] = Math.max(maxdims["height"], 400); } if (!this.DiagramSize) this.DiagramSize = maxdims; return maxdims; }, updateDimensions: function(){ switch (this.renderer){ case "graphviz": var maxdims = this.getDimensions(true); // FIXME Element.setStyle(this.element, {"height": maxdims["height"] + "px", "width": maxdims["width"] + "px"}); //Element.setStyle(this.element, {"height": "100%", "width": "98%"}); break; case "simple": default: if (!this.intermediate_document) return; XMLElement.WriteAttributes(this.intermediate_document.documentElement, this.getDimensions()); if (this.loader.IsLoaded("xsl-svg")) this.transformIntermediateToSVG(); break; } }, OnResize: function(){ if (!Object.isUndefined(this.refresh_id)){ window.clearTimeout(this.refresh_id); delete this.refresh_id; } this.refresh_id = function(){ this.updateDimensions(); delete this.refresh_id; }.bind(this).delay(.3); }, fitDiagram: function () { var dims = this.diaelem.GetDimensions(); var deltaX = this.DiagramSize["width"] / dims["width"]; var deltaY = this.DiagramSize["height"] / dims["height"]; if (deltaX == 1 || deltaY == 1) return; if (this.DiagramSize["width"] > dims["width"] && this.DiagramSize["height"] > dims["height"]) { // увеличиваем this.diaelem.ZoomRelative(Math.min(deltaX, deltaY)); } else { // уменьшаем if (deltaX < 1 && deltaY < 1) this.diaelem.ZoomRelative(Math.max(deltaX, deltaY)); else this.diaelem.ZoomRelative(Math.min(deltaX, deltaY)); } }, onpanelclick: function(event){ var element = Event.findElement(event, "button"); if (!element || !this.diaelem) return; if (Element.hasClassName(element, "ZoomIn")) this.diaelem.ZoomRelative(Diagram.scale_factor); else if (Element.hasClassName(element, "ZoomOut")) this.diaelem.ZoomRelative(1.0 / Diagram.scale_factor); else if (Element.hasClassName(element, "ZoomOriginal")) this.diaelem.ZoomAbsolute(1.0); else if (Element.hasClassName(element, "ZoomFit")) { this.fitDiagram(); } else if (Element.hasClassName(element, "Print")) this.diaelem.Print(); }, SetTabOrder: function(nTabBase){ if (this.panel){ Element.select(this.panel, "button").each(function(button){ button.tabIndex = nTabBase++; }); } return nTabBase; } }); var Diagram = {}; Diagram.scale_factor = 1.3; Diagram.GetStyleSum = function(element, style_attrs){ if (!element || !Object.isArray(style_attrs)) return 0; return $A(style_attrs).inject(0, function(acc, nam){ return acc + (parseInt(Element.getStyle(element, nam), 10) || 0); }); } Diagram.isEntity = function(element, renderer){ if (!element) return false; switch (renderer){ case "graphviz": return Node.localName(element) == "g" && XMLElement.HasClassName(element, "node"); case "simple": default: return Boolean(Node.localName(element) == "svg" && element.getAttribute("eid")); } } Diagram.GetEntity = function(event, renderer){ if (Object.isUndefined(renderer)) renderer = "simple"; var element = Event.element(event); while (element && !Diagram.isEntity(element, renderer)) element = element.parentNode; return element; }; Diagram.isRelation = function(element, renderer){ return Boolean(element && element.nodeType == Node.ELEMENT_NODE && element.getAttribute("rid") && XMLElement.HasClassName(element, "edge")); }; Diagram.GetRelation = function(event, renderer){ if (Object.isUndefined(renderer)) renderer = "simple"; var element = Event.element(event); while (element && !Diagram.isRelation(element, renderer)) element = element.parentNode; return element; }; var DiagramProcessor = Class.create({ initialize: function(events, triggers_document, renderer){ this.events = events; this.triggers_document = triggers_document; this.renderer = renderer; this.binded = {}; Object.keys(this.events).each(function(evtnam){ var callback = this["on" + evtnam]; if (!Object.isFunction(callback)) return; this.binded[evtnam] = callback.bindAsEventListener(this); }, this); if (SVGElement.IsSupported){ this.svg_arrow = new SVGElement("line", { "marker-start": "url(#aux-cross)", "marker-end": "url(#aux-arrowhead)", "stroke": "rgb(64,64,64)", "stroke-width": 2, "stroke-dasharray": "6,3" }); this.highlighted_entities = new Array(2); this.svg_style = new SVGElement("style", {"type": "text/css"}); this.svg_style.AppendChild(document.createTextNode([ ".node, .edge{ cursor: pointer; }", ".node{ fill-opacity: .1; }", ".node text{ fill-opacity: 1; }", ".node.selected, .edge.selected{ stroke-width: 4; }", ".node.selected{ fill-opacity: .3; stroke-dasharray: 4 2; }"].join("\n"))); } }, SetDiagramElement: function(diaelem){ if (this.diaelem == diaelem) return; this.diaelem = diaelem; this.setElement(diaelem.GetElement()); }, setElement: function(element){ if (element == this.element) return; if (this.container){ for (var evtnam in this.binded){ Event.stopObserving(this.triggers_document.include(evtnam) ? document : this.container, // this.events[evtnam], this.binded[evtnam]); } } this.element = element; this.container = null; if (this.element){ this.container = this.element.parentNode; for (var evtnam in this.binded){ Event.observe(this.triggers_document.include(evtnam) ? document : this.container, // this.events[evtnam], this.binded[evtnam]); } this.padl = (parseInt(Element.getStyle(this.container, "paddingLeft"), 10) || 0) + // (parseInt(Element.getStyle(this.container, "borderLeftWidth"), 10) || 0); this.padt = (parseInt(Element.getStyle(this.container, "paddingTop"), 10) || 0) + // (parseInt(Element.getStyle(this.container, "borderTopWidth"), 10) || 0); if (this.svg_style) this.svg_style.AttachTo(this.element); } }, getSvgViewportOffset: function(){ var elvo = Element.viewportOffset(this.container); return { "left": this.padl + elvo["left"], "top": this.padt + elvo["top"] }; }, getCoords: function(event){ var svgvo = this.getSvgViewportOffset(); var vbox = this.diaelem.GetViewBox(); var dims = this.diaelem.GetDimensions(); return { "x": (event.memo["x"] - svgvo["left"]) * vbox[2] / dims["width"], "y": (event.memo["y"] - svgvo["top"]) * vbox[3] / dims["height"] }; }, attached: false, attach: function(){ this.svg_arrow.AttachTo(this.element); this.attachActiveFrames(); this.attached = true; }, detach: function(){ this.svg_arrow.Remove(); this.detachActiveFrames(); this.attached = false; }, mode: "", getEntity: function(event){ return Diagram.GetEntity(event, this.renderer); }, getRelation: function(event){ return Diagram.GetRelation(event, this.renderer); }, onmousedown: function(event){ var entity = this.getEntity(event.memo["original-event"]); if (entity && this.IsEditable()){ var pos = this.getCoords(event); this.svg_arrow.WriteAttributes({"x1": pos.x, "y1": pos.y, "x2": pos.x, "y2": pos.y}); this.highlightEntities([entity, null]); this.delayed_attach = this.attach.bind(this).delay(.3); this.setMode("arrow"); }else if (this.renderer == "graphviz") this.setMode("move"); else alert(this.renderer) }, getMode: function(){ return this.mode; }, setMode: function(newmode){ if (newmode == this.mode) return; this.mode = newmode; if (this.container) Element.setStyle(this.container, {"cursor": newmode == "move" ? "move" : "default"}); }, clearMode: function(){ return this.setMode(""); }, onmousemove: function(event){ switch (this.getMode()){ case "arrow": var pos = this.getCoords(event); this.svg_arrow.WriteAttributes({"x2": pos.x, "y2": pos.y}); var orig_event = event.memo["original-event"]; if (Event.element(orig_event) != this.svg_arrow.GetElement()) this.highlightEntity(this.getEntity(orig_event), 1); break; case "move": if (this.diaelem) this.diaelem.MoveBy(event.memo["dx-recent"], event.memo["dy-recent"]); break; } }, onmouseup: function(event){ switch (this.getMode()){ case "arrow": if (!this.attached){ window.clearTimeout(this.delayed_attach); break; } this.detach(); if (this.highlighted_entities[0]) Element.fire(this.element, DiagramProcessor.events["new-edge"], {"entities": this.highlighted_entities}); this.highlightEntities([null, null]); break; case "move": if (this.diaelem) this.diaelem.MoveBy(event.memo["dx-recent"], event.memo["dy-recent"]); break; } this.clearMode(); }, onclick: function(event){ var orig_event = event.memo["original-event"]; var entity = this.getEntity(orig_event); if (entity) Element.fire(this.element, DiagramProcessor.events["node-click"], { "entity": entity }); else { var relation = this.getRelation(orig_event); if (relation) Element.fire(this.element, DiagramProcessor.events["edge-click"], { "relation": relation }); else Element.fire(this.element, DiagramProcessor.events["null-click"], {}); } }, highlightEntities: function(ents){ ents.each(this.highlightEntity, this); }, getEntityFrame: function(ent){ if (!ent) return null; if (!ent.frame){ switch (this.renderer){ case "graphviz": var shp = $A(ent.childNodes).find(function(element){ return element.nodeType == Node.ELEMENT_NODE && !(["title", "text"].include(Node.localName(element).toLowerCase())); }); if (shp){ ent.frame = new SVGElement(Node.localName(shp), DiagramProcessor.frame_attrs); ["x", "y", "width", "height", "points", "cx", "cy", "r", "rx", "ry", "d"].each(function(nam){ var val = shp.getAttribute(nam); if (val) ent.frame.WriteAttribute(nam, val); }); } break; case "simple": default: ent.frame = new SVGElement("rect", DiagramProcessor.frame_attrs); ["x", "y", "width", "height"].each(function(nam){ var val = ent.getAttribute(nam); if (val) ent.frame.WriteAttribute(nam, val); }); break; } } return ent.frame; }, attachFrame: function(ent){ var frm = this.getEntityFrame(ent); if (!frm) return; if (ent.nextSibling) ent.parentNode.insertBefore(frm.GetElement(), ent.nextSibling); else frm.AttachTo(ent.parentNode); }, detachFrame: function(ent){ var frm = this.getEntityFrame(ent); if (frm) frm.Remove(); }, attachActiveFrames: function(){ this.highlighted_entities.each(this.attachFrame, this); }, detachActiveFrames: function(){ this.highlighted_entities.each(this.detachFrame, this); }, highlightEntity: function(ent, num){ if (this.highlighted_entities[num] == ent) return; var oldent = this.highlighted_entities[num]; this.highlighted_entities[num] = ent; if (!(this.highlighted_entities.include(oldent))) this.detachFrame(oldent); if (!ent) return; if (this.attached) this.attachFrame(ent); }, onscroll: function(event){ if (!this.diaelem) return; var ticks = event.memo["ticks"]; if (!ticks) return; this.diaelem.ZoomRelative(Math.pow(DiagramProcessor.scroll_scale_factor, -ticks)); Event.stop(event.memo["original-event"]); }, editable: true, SetVisibility: function(val){ this.editable = val == "yes"; }, IsEditable: function(){ return this.editable && SVGElement.IsSupported; } }); DiagramProcessor.events = { "new-edge": "diagram-processor:new-edge", "edge-from": "diagram-processor:edge-from", "node-click": "diagram-processor:node-click", "edge-click": "diagram-processor:edge-click", "null-click": "diagram-processor:null-click" } DiagramProcessor.frame_attrs = { "stroke": "Gold", "stroke-width": 3, "stroke-opacity": .5, "fill": "none" }; DiagramProcessor.scroll_scale_factor = 1.025; var DiagramElement = Class.create({ initialize: function(element, container, opts){ if (!element) return; this.opts = opts || {}; this.container = container; var noimport = Object.isString(element) || !Object.isFunction(document.importNode) || !element.documentElement; Element.update(this.container, noimport ? element : document.importNode(element.documentElement, true)); this.svg = $A(this.container.childNodes).find(function(element){ return Node.localName(element) == "svg"; }); if (this.svg){ this.initial = this.getDimensions(); XMLElement.WriteAttributes(this.svg, this.initial); this.initial["viewBox"] = this.getViewBox(); }else this.element = $(element); }, getInitialDimensions: function(){ if (this.svg) return {"width": this.initial["width"], "height": this.initial["height"]}; else if (this.element){ if (!this.elementClone){ this.elementClone = this.element.cloneNode(true); Element.setStyle(this.elementClone, {"visibility": "hidden", "position": "absolute", // "top": "-10000px", "left": "-10000px", "width": "", "height": ""}); this.container.appendChild(this.elementClone); } return Element.getDimensions(this.elementClone); } }, getDimensions: function(){ var result = {}; if (this.svg){ ["width", "height"].each(function(key){ var mtch = this.svg.getAttribute(key).match(DiagramElement.units_regexp); if (!mtch) return; result[key] = parseFloat(mtch[1], 10) * (DiagramElement.units[String(mtch[2]).toLowerCase()] || 1); }, this); }else if (this.element) result = Element.getDimensions(this.element); return result; }, GetDimensions: function(){ return this.getDimensions(); }, getViewBox: function(){ var vbox = this.svg.getAttribute("viewBox"); if (vbox) return vbox.split(" ").collect(function(numstr){ return parseInt(numstr, 10); }); else{ var dims = this.getDimensions(); return [0, 0, dims["width"], dims["height"]]; } }, GetViewBox: function(){ return this.getViewBox(); }, GetElement: function(){ return this.svg || this.element || null; }, scale_dims: function(dims, factor){ if (dims["width"] <= 0) dims["width"] = 1; if (dims["height"] <= 0) dims["height"] = 1; var minW = 20; var minH = 20; factor = Math.max(factor, Math.max(minW / dims["width"], minH / dims["height"])); return {"width": Math.ceil(dims["width"] * factor), "height": Math.ceil(dims["height"] * factor)}; }, zoom: function(dims, factor){ if (!Object.isNumber(factor) || factor <= 0 || // Object.isUndefined(dims["width"]) || Object.isUndefined(dims["height"])) return; var newdims = this.scale_dims(dims, factor); if (this.svg) XMLElement.WriteAttributes(this.svg, newdims); else if (this.element) Element.setStyle(this.element, // {"width": newdims["width"] + "px", "height": newdims["height"] + "px"}); }, ZoomAbsolute: function(factor){ return this.zoom(this.getInitialDimensions(), factor); }, ZoomRelative: function(factor){ return this.zoom(this.getDimensions(), factor); }, cloneElement: function(){ var node = null if (this.svg){ node = this.svg.cloneNode(true); XMLElement.WriteAttributes(node, this.getInitialDimensions()); }else if (this.element){ node = this.element.cloneNode(true); Element.setStyle(node, {"width": "", "height": ""}); } return node; }, Print: function(){ var node = this.cloneElement(); if (!node) return; var wnd = AControl.GetParentWindow(this.container); return JSControl.PrintNode(node, Object.extend(this.getInitialDimensions(), {"title": wnd ? wnd.Title() : "Diagram"})); }, MoveBy: function(dx, dy){ if (!this.container || !Object.isNumber(dx) || !Object.isNumber(dy)) return; var sl = Math.max(this.container.scrollLeft - dx); var st = Math.max(this.container.scrollTop - dy, 0); this.container.scrollLeft = sl; this.container.scrollTop = st; } }); DiagramElement.units = { "pt": 1.25, "pc": 15, "mm": 3.543307, "cm": 35.43307, "in": 90 }; DiagramElement.units_regexp = /^\s*([\d\.]*)([a-z]*)\s*$/i;