//>>built // wrapped by build app define("dojox/drawing/stencil/_Base", ["dijit","dojo","dojox","dojo/require!dojo/fx/easing"], function(dijit,dojo,dojox){ dojo.provide("dojox.drawing.stencil._Base"); dojo.require("dojo.fx.easing"); /*===== StencilArgs = { // container: [readonly] dojo.gfx.group // The parent shape that contains all // shapes used in a Stencil container:null, // // anchorType: String // Optionally blank or 'group'. 'group' tells // an anchor point that it must constrain // itself to other anchor points. anchorType:"", // // isText: Boolean // Whether this is a text object or not // (either stencil.text or tools.TextBlock) isText:false, // // shortType: String // The type of stencil that corresponds with the types and // constructors used in Drawing.registerTool shortType:"", // // annotation: Boolean // A Stencil used within a Stencil. An annotation // is not selectable or clickable. A Label would // be one example. annotation:false, // // subShape: Boolean // A Stencil used within a Stencil. A subShape // is clickable. An arrow head would be an example. subShape:false, // // style: Object // An instance of the styles and defaults used within // the Stencil. style:null, // // util: Object // Pointer to util.common util:null, // // mouse: Object // Pointer to the mouse instance mouse:null, // // keys: Object // Pointer to the keys class keys:null, // // points: StencilPoints // Points is an array of objects that make up the // description of a Stencil. The points to a Rect // that is 100x100 and at x:10 and y:10 would look like: // [{x:10,y:10}, {x:110, y:10}, {x:110, y:110}, {x:10, y:110}] // Points go clockwise from the top left. In the case of Paths, // they would go in the order that the Stencil would be drawn. // Always when the points Array is set, a data Object is created // as well. So never set points directly, always use setPoints(). // See: // setPoints() points:[], // // data: StencilData // A data object typically (but not always) resembles the data // that is used to create the dojox.gfx Shape. The same Rect // example shown in points above would look like: // {x:10, y:10, width:100, height:100} // And an Ellipse with the same coordinates: // {cx:55, cy:55, rx:50, ry:50} // The only Stencil that does not support data (at this time) // is the Path. While x1,x2,x3... culd be used in a data object // it doesn't provide much benefit. // Always when a data object is set, a set of points is created // as well. So never set data directly, always use setData(). // See: // setData() data:null, // // marginZero [readonly] Number // How closely shape can get to y:0 or x:0. Less than zero has // bugs in VML. This is set with defaults, and should be equal // to half the size of an anchor point (5 px) marginZero:0, // // created [readonly] Boolean // Whether the Stencil has been rendered for the first time or // not. created: false, // // highlighted [readonly] Boolean // Whether the Stencil is highlighted or not. highlighted:false, // // selected [readonly] Boolean // Whether the Stencil is selected or not. selected:false, // // draws [readonly] Boolean // Whether the Stencil can draw with a mouse drag or can just // be created programmtically. If the Stencil comes from the // stencil package, it should be draw:false. If it comes from // the tools package it should be draw:true. draws:false } StencilPoint = { // summary: // One point Object in the points Array // x: Number // x position of point // y: Number // y position of point } ToolsSetup = { // summary: // An object attached to a Tool's constructor // used to inform the toolbar of its information // and properties. // description: // This object is inserted into the *function* of // a tool (not a stencil). Like: function.ToolsSetup // It must be attached after constructr creation, so // this object is found at the botton of the file. // // name:String // Fully qualified name of constructor // tooltip: Stirng // Text to display on toolbar button hover // iconClass: String // CSS class with icon information to attach // to toolbar button. } =====*/ dojox.drawing.stencil._Base = dojox.drawing.util.oo.declare( // summary: // The base class used for all Stencils. // description: // All stencils extend this base class. // Most methods and events can be found here. // function(options){ //console.log("______Base", this.type, options) // clone style so changes are reflected in future shapes dojo.mixin(this, options); this.style = options.style || dojox.drawing.defaults.copy(); if(options.stencil){ this.stencil = options.stencil; this.util = options.stencil.util; this.mouse = options.stencil.mouse; this.container = options.stencil.container; this.style = options.stencil.style; } // don't use the 'g' on these, it affects // the global RegExp var lineTypes = /Line|Vector|Axes|Arrow/; var textTypes = /Text/; this.shortType = this.util.abbr(this.type); this.isText = textTypes.test(this.type); this.isLine = lineTypes.test(this.type); this.renderHit = this.style.renderHitLayer; if(!this.renderHit && this.style.renderHitLines && this.isLine){ this.renderHit = true; } if(!this.renderHit && this.style.useSelectedStyle){ this.useSelectedStyle = true; this.selCopy = dojo.clone(this.style.selected); for(var nm in this.style.norm){ if(this.style.selected[nm]===undefined){ this.style.selected[nm] = this.style.norm[nm]; } } this.textSelected = dojo.clone(this.style.text); this.textSelected.color = this.style.selected.fill; } this.angleSnap = this.style.angleSnap || 1; this.marginZero = options.marginZero || this.style.anchors.marginZero; this.id = options.id || this.util.uid(this.type); this._cons = []; if(!this.annotation && !this.subShape){ this.util.attr(this.container, "id", this.id); } this.connect(this, "onBeforeRender", "preventNegativePos"); this._offX = this.mouse.origin.x; this._offY = this.mouse.origin.y; if(this.isText){ this.align = options.align || this.align; this.valign = options.valign || this.valign; if(options.data && options.data.makeFit){ var textObj = this.makeFit(options.data.text, options.data.width); this.textSize = this.style.text.size = textObj.size; this._lineHeight = textObj.box.h; }else{ this.textSize = parseInt(this.style.text.size, 10); this._lineHeight = this.textSize * 1.4; } // TODO: thinner text selection //this.style.hitSelected.width *= 0.5; // // ouch. how verbose. My mixin is weak.... this.deleteEmptyCreate = options.deleteEmptyCreate!==undefined ? options.deleteEmptyCreate : this.style.text.deleteEmptyCreate; this.deleteEmptyModify = options.deleteEmptyModify!==undefined ? options.deleteEmptyModify : this.style.text.deleteEmptyModify; } //this.drawingType this.attr(options.data); // make truthy // add to renders below // this.baseRender && render() //if(this.type == "dojox.drawing.tools.TextBlock"){ if(this.noBaseRender){ // TextBlock will handle rendering itself return; } //console.log("BASE OPTS:", options) if(options.points){ //console.log("__________Base.constr >>>> ", this.type, "points", options.points) if(options.data && options.data.closePath===false){ this.closePath = false; } this.setPoints(options.points); this.connect(this, "render", this, "onRender", true); this.baseRender && this.enabled && this.render(); options.label && this.setLabel(options.label); options.shadow && this.addShadow(options.shadow); }else if(options.data){ //console.log("___________Base.constr", this.type, "options data", options.data) options.data.width = options.data.width ? options.data.width : this.style.text.minWidth; options.data.height = options.data.height ? options.data.height : this._lineHeight; this.setData(options.data); this.connect(this, "render", this, "onRender", true); this.baseRender && this.enabled && this.render(options.data.text); this.baseRender && options.label && this.setLabel(options.label); this.baseRender && options.shadow && this.addShadow(options.shadow); }else if(this.draws){ //console.log("_____________Base.constr", this.type, "draws") this.points = []; this.data = {}; this.connectMouse(); this._postRenderCon = dojo.connect(this, "render", this, "_onPostRender"); } if(this.showAngle){ this.angleLabel = new dojox.drawing.annotations.Angle({stencil:this}); } if(!this.enabled){ this.disable(); this.moveToBack(); // some things render some don't... this.render(options.data.text); } }, { // type: String // The type of Stencil this is. Should be overridden // by extending classes. // FIXME: should this be declaredClass? type:"dojox.drawing.stencil", // // minimumSize: Number // The minimum size allowed for a render. If the size // is less, the shape is destroyed. minimumSize:10, // // enabled [readonly] Boolean // Whether the Stencil is enabled or not. enabled:true, drawingType:"stencil", //points:[], setData: function(/*StencilData*/data){ // summary: // Setter for Stencil data; also converts // data to points. See individual Stencils // for specific data properties. this.data = data; this.points = this.dataToPoints(); }, setPoints: function(/*StencilPoints*/points){ // summary: // Setter for Stencil points; also converts // points to data. See individual Stencils // for specific points properties. this.points = points; // Path doesn't do data if(this.pointsToData){ this.data = this.pointsToData(); } }, onDelete: function(/* Stencil */ stencil){ // summary: // Stub - fires before this is destroyed console.info("onDelete", this.id); }, onBeforeRender: function(/*Object*/ stencil){ // summary: // Stub - Fires before render occurs. }, onModify: function(/*Object*/stencil){ // summary: // Stub - fires on change of any property, // including style properties }, onChangeData: function(/*Object*/ stencil){ // summary: // Stub - fires on change of dimensional // properties or a text change }, onChangeText: function(value){ // value or 'this' ? // summary: // Stub - fires on change of text in a // TextBlock tool only }, onRender: function(/*Object*/ stencil){ // summary: // Stub - Fires on creation. // Drawing connects to this (once!) to be // notified of drag completion. But only if it // was registered as a Tool. Creating Stencil in and of // itself does not register it. // // This should fire // at the *end* of creation (not during drag) // // FIXME: // This should probably be onCreate. It should // only fire once. But the mechanism for determining // this is more complicated than it sounds. // this._postRenderCon = dojo.connect(this, "render", this, "_onPostRender"); this.created = true; this.disconnectMouse(); // for Silverlight if(this.shape){ this.shape.superClass = this; }else{ this.container.superClass = this; } this._setNodeAtts(this); //console.warn("ONRENDER", this.id, this) }, onChangeStyle: function(/*Object*/stencil){ // summary: // Fires when styles of shape has changed // this._isBeingModified = true; // need this to prevent onRender if(!this.enabled){ this.style.current = this.style.disabled; this.style.currentText = this.style.textDisabled; this.style.currentHit = this.style.hitNorm; }else{ this.style.current = this.style.norm; this.style.currentHit = this.style.hitNorm; this.style.currentText = this.style.text; } if(this.selected){ if(this.useSelectedStyle){ this.style.current = this.style.selected; this.style.currentText = this.textSelected; } this.style.currentHit = this.style.hitSelected; }else if(this.highlighted){ //this.style.current = this.style.highlighted; this.style.currentHit = this.style.hitHighlighted; //this.style.currentText = this.style.textHighlighted; } // NOTE: Can't just change props like setStroke // because Silverlight throws error this.render(); }, animate: function(options, create){ console.warn("ANIMATE..........................") var d = options.d || options.duration || 1000; var ms = options.ms || 20; var ease = options.ease || dojo.fx.easing.linear; var steps = options.steps; var ts = new Date().getTime(); var w = 100; var cnt = 0; var isArray = true; var sp, ep; if(dojo.isArray(options.start)){ sp = options.start; ep = options.end; }else if(dojo.isObject(options.start)){ sp = options.start; ep = options.end; isArray = false; }else{ console.warn("No data provided to animate") } var v = setInterval(dojo.hitch(this, function(){ var t = new Date().getTime() - ts; var p = ease(1-t/d); if(t > d || cnt++ > 100){ clearInterval(v); return; } if(isArray){ var pnts = []; dojo.forEach(sp, function(pt, i){ var o = { x: (ep[i].x-sp[i].x)*p + sp[i].x, y: (ep[i].y-sp[i].y)*p + sp[i].y }; pnts.push(o); }); this.setPoints(pnts); this.render(); }else{ var o = {}; for(var nm in sp){ o[nm] = (ep[nm] - sp[nm]) * p + sp[nm]; } this.attr(o); } //console.dir(pnts) //this.attr("height", w); ////console.log("W:", w) //w += 5; }), ms); }, attr: function(/*String | Object*/key, /* ? String | Number */value){ // summary // Changes properties in the style or disabled styles, // depending on whether the object is enabled. // Also can be used to change most position and size props. // NOTE: JUST A SETTTER!! TODO! // WARNING: // Not doing any Stencil-type checking here. Setting a height // on a line or an angle on a rectangle will just not render. // FIXME // 'width' attr is used for line width. How to change the width of a stencil? var n = this.enabled?this.style.norm:this.style.disabled; var t = this.enabled?this.style.text:this.style.textDisabled; var ts = this.textSelected || {}, o, nm, width, styleWas = dojo.toJson(n), textWas = dojo.toJson(t); var coords = { x:true, y:true, r:true, height:true, width:true, radius:true, angle:true }; var propChange = false; if(typeof(key)!="object"){ o = {}; o[key] = value; }else{ // prevent changing actual data o = dojo.clone(key); } if(o.width){ // using width for size, // borderWidth should be used // for line thickness width = o.width; delete o.width; } for(nm in o){ if(nm in n){ n[nm] = o[nm]; } if(nm in t){ t[nm] = o[nm]; } if(nm in ts){ ts[nm] = o[nm]; } if(nm in coords){ coords[nm] = o[nm]; propChange = true; if(nm == "radius" && o.angle===undefined){ o.angle = coords.angle = this.getAngle(); }else if(nm == "angle" && o.radius===undefined){ o.radius = coords.radius = this.getRadius(); } } if(nm == "text"){ this.setText(o.text); } if(nm == "label"){ this.setLabel(o.label); } } if(o.borderWidth!==undefined){ n.width = o.borderWidth; } if(this.useSelectedStyle){ // using the orginal selected style copy as // a reference map of what props to copy for(nm in this.style.norm){ if(this.selCopy[nm]===undefined){ this.style.selected[nm] = this.style.norm[nm]; } } this.textSelected.color = this.style.selected.color; } if(!this.created){ return; } // basic transform if(o.x!==undefined || o.y!==undefined){ var box = this.getBounds(true); var mx = { dx:0, dy:0 }; for(nm in o){ if(nm=="x" || nm =="y" || nm =="r"){ mx["d"+nm] = o[nm] - box[nm]; } } this.transformPoints(mx); } var p = this.points; if(o.angle!==undefined){ this.dataToPoints({ x:this.data.x1, y:this.data.y1, angle:o.angle, radius:o.radius }); } else if(width!==undefined){ p[1].x = p[2].x = p[0].x + width; this.pointsToData(p); } if(o.height!==undefined && o.angle===undefined){ console.log("Doing P2D-2"); p[2].y = p[3].y = p[0].y + o.height; this.pointsToData(p); } if(o.r!==undefined){ this.data.r = Math.max(0, o.r); } //console.dir(this.data); if(propChange || textWas!=dojo.toJson(t) || styleWas != dojo.toJson(n)){ // to trigger the render // other events will be called post render this.onChangeStyle(this); } o.width = width; if(o.cosphi!=undefined){ !this.data? this.data = {cosphi:o.cosphi} : this.data.cosphi = o.cosphi; this.style.zAxis = o.cosphi!=0 ? true : false; } }, exporter: function(){ // summary: // Exports Stencil data // var type = this.type.substring(this.type.lastIndexOf(".")+1).charAt(0).toLowerCase() + this.type.substring(this.type.lastIndexOf(".")+2); var o = dojo.clone(this.style.norm); o.borderWidth = o.width; delete o.width; if(type=="path"){ o.points = this.points; }else{ o = dojo.mixin(o, this.data); } o.type = type; if(this.isText){ o.text = this.getText(); o = dojo.mixin(o, this.style.text); delete o.minWidth; delete o.deleteEmptyCreate; delete o.deleteEmptyModify; } var lbl = this.getLabel(); if(lbl){ o.label = lbl; } return o; }, // TODO: // Makes these all called by att() // Should points and data be? // disable: function(){ // summary: // Disables Stencil so it is not selectable. // Changes the color to the disabled style. this.enabled = false; this.renderHit = false; this.onChangeStyle(this); }, enable: function(){ // summary: // Enables Stencil so it is not selectable (if // it was selectable to begin with). Changes the // color to the current style. this.enabled = true; this.renderHit = true; this.onChangeStyle(this); }, select: function(){ // summary: // Called when the Stencil is selected. // NOTE: Calling this will not select the Stencil // calling this just sets the style to the 'selected' // theme. 'manager.Stencil' should be used for selecting // Stencils. // this.selected = true; this.onChangeStyle(this); }, deselect: function(/*Boolean*/useDelay){ // summary: // Called when the Stencil is deselected. // NOTE: Calling this will not deselect the Stencil // calling this just sets the style to the current // theme. 'manager.Stencil' should be used for selecting // and deselecting Stencils. // // arguments: // useDelay: Boolean // Adds slight delay before the style is set. // // should not have to render here because the deselection // re-renders after the transform // but... oh well. if(useDelay){ setTimeout(dojo.hitch(this, function(){ this.selected = false; this.onChangeStyle(this); }),200); }else{ this.selected = false; this.onChangeStyle(this); } }, _toggleSelected: function(){ if(!this.selected){ return; } this.deselect(); setTimeout(dojo.hitch(this, "select"), 0); }, highlight: function(){ // summary: // Changes style to the highlight theme. this.highlighted = true; this.onChangeStyle(this); }, unhighlight: function(){ // summary: // Changes style to the current theme. this.highlighted = false; this.onChangeStyle(this); }, moveToFront: function(){ // summary: // Moves Stencil to the front of all other items // on the canvas. this.container && this.container.moveToFront(); }, moveToBack: function(){ // summary: // Moves Stencil to the back of all other items // on the canvas. this.container && this.container.moveToBack(); }, onTransformBegin: function(/* ? manager.Anchor */anchor){ // summary: // Fired at the start of a transform. This would be // an anchor drag or a selection. // this._isBeingModified = true; }, onTransformEnd: function(/* manager.Anchor */anchor){ // summary: // Called from anchor point up mouse up this._isBeingModified = false; this.onModify(this); }, onTransform: function(/* ? manager.Anchor */anchor){ // summary: // Called from anchor point mouse drag // also called from plugins.Pan.checkBounds if(!this._isBeingModified){ this.onTransformBegin(); } // this is not needed for anchor moves, but it // is for stencil move: this.setPoints(this.points); this.render(); }, transformPoints: function(mx){ // summary: // Moves object to a new X Y location // mx is additive. So mx.dx=1 will move the stencil // 1 pixel to the right from wherever it was. // // An attempt is made to prevent < 0 errors, but // this won't work on all shapes (like Axes) // if(!mx.dx && !mx.dy){ // no change return; } var backup = dojo.clone(this.points), abort = false; dojo.forEach(this.points, function(o){ o.x += mx.dx; o.y += mx.dy; if(o.x 180, -90 -> 270 angle<0 ? angle = 360 + angle : angle; return angle; }, getRadius: function(){ // summary: // Gets radius (length) of Stencil // NOTE: Only works for Lines, Arrows and Vectors // (not for Ellipse, Axes has its own version) // var box = this.getBounds(true); var line = {start:{x:box.x1, y:box.y1}, x:box.x2, y:box.y2}; return this.util.length(line); }, getBounds: function(/* ? Boolean*/absolute){ // summary: // Returns the coordinates of the Stencil. This is often // different than the data or the points. // arguments: // absolute: Boolean // Keeps lines from flipping (see note). // // NOTE: Won't work for paths or annotations (labels, Axes, arrow tips) // They should overwrite. // NOTE: Primarily used for checking for if shape is off // canvas. Therefore Lines could get flipped. Use absolute // to prevent this. // var p = this.points, x1, y1, x2, y2; if(p.length==2){ if(absolute){ x1 = p[0].x; y1 = p[0].y; x2 = p[1].x; y2 = p[1].y }else{ x1 = p[0].x < p[1].x ? p[0].x : p[1].x; y1 = p[0].y < p[1].y ? p[0].y : p[1].y; x2 = p[0].x < p[1].x ? p[1].x : p[0].x; y2 = p[0].y < p[1].y ? p[1].y : p[0].y; } return { x1:x1, y1:y1, x2:x2, y2:y2, x:x1, y:y1, w:x2-x1, h:y2-y1 }; // Object }else{ return { x1:p[0].x, y1:p[0].y, x2:p[2].x, y2:p[2].y, x:p[0].x, y:p[0].y, w:p[2].x - p[0].x, h:p[2].y - p[0].y }; // Object } }, preventNegativePos: function(){ // summary: // Internal. Prevent item from being drawn/rendered less // than zero on the X or Y. // // if being modified anchors will prevent less than zero. if(this._isBeingModified){ return; } // FIXME: why is this sometimes empty? if(!this.points || !this.points.length){ return; } if(this.type=="dojox.drawing.tools.custom.Axes"){ // this scenario moves all points if < 0 var minY = this.marginZero, minX = this.marginZero; dojo.forEach(this.points, function(p){ minY = Math.min(p.y, minY); }); dojo.forEach(this.points, function(p){ minX = Math.min(p.x, minX); }); if(minY : this.editMode:", this.editMode) this.onChangeData(this); this._prevData = dojo.clone(this.data); }else if(!this._prevData && (!this.isText || this.getText())){ //console.info("_Base no prevData.........................."); this._prevData = dojo.clone(this.data); } }, _setNodeAtts: function(shape){ // summary: // Internal. Sets the rawNode attribute. (Or in Silverlight // an "object attribute". "stencil" is // used by the application to determine if // something is selectable or not. This also // sets the mouse custom events like: // "onStencilUp". To disable the selectability, // make the att "", which causes a standard // mouse event. // Labels are special and used to select master stencils. var att = this.enabled && (!this.annotation || this.drawingType=="label") ? this.drawingType : ""; this.util.attr(shape, "drawingType", att); }, destroy: function(){ // summary: // Destroys this Stencil // Note: // Can connect to this, but it's better to // connect to onDelete // // prevent loops: if(this.destroyed){ return; } if(this.data || this.points && this.points.length){ this.onDelete(this); } this.disconnectMouse(); this.disconnect(this._cons); dojo.disconnect(this._postRenderCon); this.remove(this.shape, this.hit); this.destroyed = true; }, remove: function(/*Shape...*/){ // summary: // Removes shape(s), typically before a re-render // No args defaults to this.shape // Pass in multiple args to remove multiple shapes // // FIXME: Use an Array of all shapes // var a = arguments; if(!a.length){ if(!this.shape){ return; } a = [this.shape]; } for(var i=0;i1){ // arguments are the connect params this._cons.push(this.connect.apply(this, arguments)); }else if(dojo.isArray(arguments[0][0])){ // an array of arrays of params dojo.forEach(arguments[0], function(ar){ this._cons.push(this.connect.apply(this, ar)); }, this); }else{ //one array of params this._cons.push(this.connect.apply(this, arguments[0])); } }, // TODO: connect to a Shape event from outside class connect: function(o, e, s, m, /* Boolean*/once){ // summary: // Convenience method for quick connects // See comments below for possiblities // functions can be strings // once: // If true, the connection happens only // once then disconnects. Five args are required // for this functionality. // var c; if(typeof(o)!="object"){ if(s){ // ** function object function ** m = s; s = e; e=o; o = this; }else{ // ** function function ** m = e; e = o; o = s = this; } }else if(!m){ // ** object function function ** m = s; s = this; }else if(once){ // ** object function object function Boolean ** c = dojo.connect(o, e, function(evt){ dojo.hitch(s, m)(evt); dojo.disconnect(c); }); this._cons.push(c); return c; }else{ // ** object function object function ** } c = dojo.connect(o, e, s, m); this._cons.push(c); return c; }, disconnect: function(/*handle | Array*/handles){ // summary: // Removes connections based on passed // handles arguments if(!handles){ return } if(!dojo.isArray(handles)){ handles=[handles]; } dojo.forEach(handles, dojo.disconnect, dojo); }, connectMouse: function(){ // summary: // Internal. Registers this Stencil to receive // mouse events. this._mouseHandle = this.mouse.register(this); }, disconnectMouse: function(){ // summary: // Internal. Unregisters this Stencil from receiving // mouse events. this.mouse.unregister(this._mouseHandle); }, // Should be overwritten by sub class: render: function(){ // summary: // This Stencil's render description. Often // calls 'sub render' methods. }, //renderOutline: function(){}, dataToPoints: function(/*Object*/data){ // summary: // Converts data to points. }, pointsToData: function(/*Array*/points){ // summary: // Converts points to data }, onDown: function(/*EventObject*/obj){ // summary: // Mouse event, fired on mousedown on canvas // // by default, object is ready to accept data // turn this off for dragging or onRender will // keep firing and register the shape // NOTE: Not needed for all stencils. Axes needs it. this._downOnCanvas = true; dojo.disconnect(this._postRenderCon); this._postRenderCon = null; }, onMove: function(/*EventObject*/obj){ // summary: // Mouse event, fired on mousemove while mouse // is not down. // NOTE: Not currently implemented }, onDrag: function(/*EventObject*/obj){ // summary: // Mouse event, fired on mousemove while mouse // is down on canvas }, onUp: function(/*EventObject*/obj){ // summary: // Mouse event, fired on mouseup } } ); });