为什么添加SVG支持?
可伸缩矢量图形 (Scalable Vector Graphics)简称SVG,相对于普通的PNG图形文件,SVG格式有更好的展示效果,可以明显增加用户的体验。FoxBPM流程引擎同时支持PNG和SVG两种格式的输出。开发者可以根据用户需求动态的抉择。针对BPM产品SVG本身有着更多的优势:首先SVG本质就是一个XML,其对应的字符串可以直接在支持SVG的浏览器中展示,所以相对普通的图形文件SVG可以更加容易的输出到前端浏览器,在网络带宽不足的情况下这个优势就更加明显; 其次既然SVG本身是一个XML,那么我们就可以在前端像操作其他的DOM模型一样操作SVG,从而我们在浏览器前端也可以动态的改变流程图内容,从而添加更多功能(比如流程实例的运行轨迹,运行状态等)。
主要知识点和难点:
主要知识点包括:采用JAXB实现XML 和POJO之间的映射,VO的克隆,三次贝塞尔曲线的控制点计算,线条拐点的中心点计算等等。相关知识点核心代码如下所示:
VO对象克隆方法:
/**
* DefsVO克隆
* @param DefsVO 原对象
* @return clone之后的对象
*/
public final static GVO cloneGVO(GVO gVo) {
return (GVO) clone(gVo);
}
public final static DefsVO cloneDefsVO(DefsVO defsVo) {
return (DefsVO) clone(defsVo);
}
/**
* SvgVO模板对象需要多次引用,所以要克隆,避免产生问题
* @param SvgVO 原对象
* @return clone之后的对象
*/
public final static SvgVO cloneSVGVo(SvgVO svgVo) {
return (SvgVO) clone(svgVo);
}
/**
* 克隆对象
* @param object 原对象
* @return 目标对象
*/
public final static Object clone(Object object) {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
Object cloneObject = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(object);
ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
cloneObject = ois.readObject();
} catch (Exception e) {
throw new FoxBPMException("SVG对象G节点克隆出现问题", e);
} finally {
try {
if (bos != null) {
bos.close();
}
if (oos != null) {
oos.close();
}
if (ois != null) {
ois.close();
}
} catch (Exception e) {
throw new FoxBPMException("克隆之后关闭对象流时出现问题", e);
}
}
return cloneObject;
}
JAXB映射方法(templateName是bpmn2.0官网提供的SVG模板名称):
/**
* 第一次需要从svg文档加载
*
* @param templateName
*/
private void init(String templateName) {
try {
JAXBContext context = JAXBContext.newInstance(SvgVO.class);
Unmarshaller unMarshaller = context.createUnmarshaller();
SAXParserFactory factory = SAXParserFactory.newInstance();
// 解析的时候忽略SVG命名空间,否则会出错
factory.setNamespaceAware(true);
XMLReader reader = factory.newSAXParser().getXMLReader();
String sourcePath = new StringBuffer(BPMN_PATH).append(FILE_SPERATOR)
.append(templateName).toString();
Source source = new SAXSource(reader, new InputSource(
ReflectUtil.getResourceAsStream(sourcePath)));
VONode object = (VONode) unMarshaller.unmarshal(source);
//将模板VO对象加入系统缓存
this.svgTemplets.put(templateName, object);
} catch (Exception e) {
throw new FoxBPMException("template svg file load exception", e);
} finally {
}
}
/**
* 操作之后的SVG转化成String字符串
* @param svgVo 容器对象
* @return SVG字符串
*/
public final static String createSVGString(VONode svgVo) {
try {
JAXBContext context = JAXBContext.newInstance(SvgVO.class);
Marshaller marshal = context.createMarshaller();
marshal.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter writer = new StringWriter();
marshal.marshal(svgVo, writer);
return writer.toString();
} catch (Exception e) {
throw new FoxBPMException("svg object convert to String exception", e);
}
}
三次贝塞尔曲线的控制点以及线条中心点计算方法:
/**
* Copyright 1996-2014 FoxBPM ORG.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author MAENLIANG
*/
package org.foxbpm.engine.impl.diagramview.svg;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* 坐标工具类
* @author MAENLIANG
* @date 2014-06-19
*
*/
public final class PointUtils {
/**
* X、Y允许的最大偏移量,超过最大偏移量需要设置文本相对线条的中心位置,如果小于这个 * 值则不需要设置
*/
private static final float X_Y_LOCATION_MAXSHIFT = 100F;
// TODO后续改善成动态规划算法
/**
* 默认圆角的大小,暂时分三个梯度
*/
private final static float SEQUENCE_ROUNDCONTROL_FLAG = 15.0f;
private final static float SEQUENCE_ROUNDCONTROL_MIN_FLAG = 3.0f;
private final static float NONE_SCALE = 1.0f;
/**
* 计算三次贝塞尔曲线的起始点,控制点以及终点
* @param start
* @param center
* @param end
* @return 起始点 控制点 终点坐标数组
*/
public final static Point[] caclBeralPoints(Point start, Point center, Point end) {
float lengthStartHerizon = 0.0f;
float lengthStartVertical = 0.0f;
float lengthStartCenter = 0.0f;
float lengthEndHerizon = 0.0f;
float lengthEndVertical = 0.0f;
float lengthEndCenter = 0.0f;
lengthStartHerizon = Math.abs(start.getX() - center.getX());
lengthStartVertical = Math.abs(start.getY() - center.getY());
lengthStartCenter = (float) Math.sqrt(lengthStartHerizon * lengthStartHerizon
+ lengthStartVertical * lengthStartVertical);
lengthEndHerizon = Math.abs(end.getX() - center.getX());
lengthEndVertical = Math.abs(end.getY() - center.getY());
lengthEndCenter = (float) Math.sqrt(lengthEndHerizon * lengthEndHerizon + lengthEndVertical
* lengthEndVertical);
float scale = SEQUENCE_ROUNDCONTROL_FLAG;
if (lengthEndCenter <= SEQUENCE_ROUNDCONTROL_FLAG
|| lengthStartCenter <= SEQUENCE_ROUNDCONTROL_FLAG) {
if (lengthEndCenter <= SEQUENCE_ROUNDCONTROL_MIN_FLAG
|| lengthStartCenter <= SEQUENCE_ROUNDCONTROL_MIN_FLAG) {
scale = NONE_SCALE;
} else {
scale = SEQUENCE_ROUNDCONTROL_MIN_FLAG;
}
}
float controlScale = scale / 3;
Point[] points = new Point[4];
points[0] = caclBeralControlPoint(start, center, scale, lengthStartHerizon, lengthStartVertical, lengthStartCenter);
points[1] = caclBeralControlPoint(start, center, controlScale, lengthStartHerizon, lengthStartVertical, lengthStartCenter);
points[2] = caclBeralControlPoint(end, center, controlScale, lengthEndHerizon, lengthEndVertical, lengthEndCenter);
points[3] = caclBeralControlPoint(end, center, scale, lengthEndHerizon, lengthEndVertical, lengthEndCenter);
return points;
}
/**
* 计算三次贝塞尔曲线控制点
*
* @param startEndPoint
* @param center
* @return
*/
private final static Point caclBeralControlPoint(Point startEndPoint, Point center,
float scale, float lengthHerizon, float lengthVertical, float lengthCenter) {
float startX = 0.0f;
float startY = 0.0f;
if (startEndPoint.getX() > center.getX()) {
startX = ((scale * lengthHerizon) / lengthCenter) + center.getX();
} else {
startX = (((lengthCenter - scale) * lengthHerizon) / lengthCenter)
+ startEndPoint.getX();
}
if (startEndPoint.getY() > center.getY()) {
startY = ((scale * lengthVertical) / lengthCenter) + center.getY();
} else {
startY = (((lengthCenter - scale) * lengthVertical) / lengthCenter)
+ startEndPoint.getY();
}
Point resultPoint = new Point(Math.round(startX), Math.round(startY));
return resultPoint;
}
/**
* 计算中心点位置,包括复杂情况,和简单情况
*
* @param pointList
* @return
*/
public final static Point caclDetailCenterPoint(List<Point> pointList) {
Float[][] xyArrays = getXYLocationArray(pointList);
Float[] xArrays = xyArrays[0];
Float[] yArrays = xyArrays[1];
float xShift = calCariance(xArrays);
float yShift = calCariance(yArrays);
// 如果偏差过大就计算中心点
if (xShift > X_Y_LOCATION_MAXSHIFT && yShift < X_Y_LOCATION_MAXSHIFT) {
// X坐标拐点幅度大,Y不大,计算线条X中心位置,Y取平均值
return caclXCenter(xArrays, yArrays);
} else if (yShift > X_Y_LOCATION_MAXSHIFT && xShift < X_Y_LOCATION_MAXSHIFT) {
// Y坐标拐点幅度大,X不大,计算线条Y中心位置,X取平均值
return caclYCenter(xArrays, yArrays);
} else if (yShift > X_Y_LOCATION_MAXSHIFT && xShift > X_Y_LOCATION_MAXSHIFT) {
// XY拐点幅度都比较大
return caclCenterPoint(pointList);
}
// 如果没有计算中心点
return null;
}
/**
* 勾股定理, 取线段长度
*
* @param pointA
* @param pointB
* @return
*/
public final static float segmentLength(Point pointA, Point pointB) {
double length = 0;
float width = pointA.getX() - pointA.getX();
float height = pointA.getY() - pointB.getY();
double dWidth = (double) width;
double dHeight = (double) height;
length = Math.sqrt(Math.pow(dWidth, 2) + Math.pow(dHeight, 2));
return (float) length;
}
/**
* 统计所有线段的长度
*
* @param pointList
* @return
*/
private final static List<Float> getSegmentsLength(List<Point> pointList) {
List<Float> segList = new ArrayList<Float>();
float segLen = 0f;
int size = pointList.size();
size = size - 1;
Point pointA = null;
Point pointB = null;
for (int i = 0; i < size; i++) {
pointA = pointList.get(i);
pointB = pointList.get(i + 1);
segLen = segmentLength(pointA, pointB);
segList.add(segLen);
}
return segList;
}
/**
* 计算所有x坐标值或者y坐标值的方差,判断是否需要重新设置所有拐点的中心点
*
* @param array
* @return
*/
public final static Float calCariance(Float[] array) {
int arrayLength = array.length;
Float ave = 0.0F;
for (int i = 0; i < arrayLength; i++) {
ave += array[i];
}
ave /= arrayLength;
Float sum = 0.0F;
for (int i = 0; i < arrayLength; i++) {
sum += (array[i] - ave) * (array[i] - ave);
}
sum = sum / arrayLength;
return sum;
}
/**
* 获取所有点的XY坐标
*
* @param pointList
* @return
*/
private final static Float[][] getXYLocationArray(List<Point> pointList) {
int pointListsize = pointList.size();
Float[][] xyLocationArray = new Float[2][pointListsize];
Iterator<Point> iterator = pointList.iterator();
for (int i = 0; i < pointListsize; i++) {
Point next = iterator.next();
xyLocationArray[0][i] = next.getX();
xyLocationArray[1][i] = next.getY();
}
return xyLocationArray;
}
/**
* 计算线条在X方向上的中心位置
*
* @param xArrays
* @param yArrays
* @return
*/
public final static Point caclXCenter(Float[] xArrays, Float[] yArrays) {
Arrays.sort(xArrays);
int length = yArrays.length;
Float xCenter = xArrays[0] + (xArrays[xArrays.length - 1] - xArrays[0]) / 2;
Float tempV = 0.0f;
for (int i = 0; i < length; i++) {
tempV += yArrays[i];
}
Float y = tempV / length;
return new Point(xCenter, y);
}
/**
* 计算线条在y方向上的中心位置
*
* @param xArrays
* @param yArrays
* @return
*/
public final static Point caclYCenter(Float[] xArrays, Float[] yArrays) {
Arrays.sort(yArrays);
Float yCenter = yArrays[0] + (yArrays[yArrays.length - 1] - yArrays[0]) / 2;
Float tempV = 0.0f;
int length = xArrays.length;
for (int i = 0; i < length; i++) {
tempV += xArrays[i];
}
Float x = tempV / length;
return new Point(yCenter, x);
}
/**
* 计算中心点
*
* @param svgLine
* @return
*/
public final static Point caclCenterPoint(List<Point> pointList) {
List<Float> segList = PointUtils.getSegmentsLength(pointList);
float totalLength = 0f;
for (float seg : segList) {
totalLength += seg;
}
float centerLen = totalLength / 2;
int keySegIndex = 0;
float keySeg = 0f;
float tempLen = 0f;
for (float seg : segList) {
keySeg = seg;
keySegIndex++;
if ((tempLen + keySeg) > centerLen) {
break;
} else {
tempLen += keySeg;
}
}
float lastLen = centerLen - tempLen;
Point startPoint = pointList.get(keySegIndex - 1);
Point endPoint = pointList.get(keySegIndex);
float scale = lastLen / keySeg;
float x = startPoint.getX() + (endPoint.getX() - startPoint.getX()) * scale;
float y = startPoint.getY() + (endPoint.getY() - startPoint.getY()) * scale;
Point point = new Point(x, y);
return point;
}
}
模块结构详细说明:
结构如下图所示:
1、模块构建基于工厂和创建者设计模式,独立于SVG,对其他矢量标记语言扩展友好。
2、FoxBpmnViewBuilder为VO属性构建的上层接口,当创建新的元素时,可以针对元素的特性进行实现。流程引擎后期的维护和功能扩展也主要是针对该接口的实现。
3、AbstractFlowElementVOFactory也是VO属性构建的上层抽象类,负责VO对象的获取、过滤、以及根据特定元素选择特定Builder构建VO属性。
4、svg包模块是针对FoxBpmnViewBuilder和AbstractFlowElementVOFactory的svg实现,包括svg工厂、svg 构建者、svg VO模型、svg工具类。
SVG构建详细说明:
1、流程引擎启动初始化的时候,将BPMN2.0官网提供的所有SVG模版(其模版以单个组件的形式提供)初始化,采用映射工具JAXB 将模版转化成VO对象并且加入缓存。
VO定义如下代码所示:
/**
* Copyright 1996-2014 FoxBPM ORG.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author MAENLIANG
*/
package org.foxbpm.engine.impl.diagramview.vo;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlValue;
/**
* SVG对象的超类,包括 节点SVG对象,连接线SVG对象
*
* @author MAENLIANG
* @date 2014-06-10
*
*/
@XmlType
@XmlAccessorType(XmlAccessType.FIELD)
public abstract class VONode implements Serializable {
private static final long serialVersionUID = -817550474604039751L;
/**
* ID Name 用于标记流程元素ID用于 流程图的前端操作
*/
@XmlAttribute(name = "id")
protected String id;
@XmlAttribute(name = "name")
protected String name;
@XmlAttribute(name = "style")
protected String style;
@XmlAttribute(name = "fill")
protected String fill;
@XmlAttribute(name = "stroke")
protected String stroke;
@XmlAttribute(name = "stroke-width")
protected Float strokeWidth;
@XmlAttribute(name = "stroke-linecap")
protected String strokeLinecap;
@XmlAttribute(name = "stroke-linejoin")
protected String strokeLinejoin;
@XmlAttribute(name = "width")
protected Float width;
@XmlAttribute(name = "height")
protected Float height;
@XmlAttribute(name = "x")
protected Float x;
@XmlAttribute(name = "y")
protected Float y;
@XmlValue
protected String elementValue;
public String getElementValue() {
return elementValue;
}
public void setElementValue(String elementValue) {
this.elementValue = elementValue;
}
public Float getX() {
return x;
}
public void setX(Float x) {
this.x = x;
}
public Float getY() {
return y;
}
public void setY(Float y) {
this.y = y;
}
public String getStyle() {
return style;
}
public void setStyle(String style) {
this.style = style;
}
public String getFill() {
return fill;
}
public void setFill(String fill) {
this.fill = fill;
}
public String getStroke() {
return stroke;
}
public void setStroke(String stroke) {
this.stroke = stroke;
}
public float getStrokeWidth() {
return strokeWidth;
}
public void setStrokeWidth(float strokeWidth) {
this.strokeWidth = strokeWidth;
}
public float getWidth() {
return width;
}
public void setWidth(float width) {
this.width = width;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStrokeLinecap() {
return strokeLinecap;
}
public void setStrokeLinecap(String strokeLinecap) {
this.strokeLinecap = strokeLinecap;
}
public String getStrokeLinejoin() {
return strokeLinejoin;
}
public void setStrokeLinejoin(String strokeLinejoin) {
this.strokeLinejoin = strokeLinejoin;
}
}
2、SVG模版相对独立,当创建时流程引擎提供包含所有流程元素定义的流程定义对象,流程元素包括 节点、线条、泳道、部件,其中节点包括活动任务、事件、网关。SVG创建者首先创建一个VO集合,然后根据流程元素从缓存中获取每个元素对应的自己的VO对象,获取VO 对象之后根据每个元素的式样属性来设置其VO属性,需要实现的接口为FoxBpmnViewBuilder,VO对象属性设置好之后添加到VO集合中。
FoxBpmnViewBuilder代码如下:
/**
* Copyright 1996-2014 FoxBPM ORG.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author MAENLIANG
*/
package org.foxbpm.engine.impl.diagramview.builder;
import java.util.List;
import org.foxbpm.engine.impl.diagramview.svg.Point;
/**
* 流程图形信息构造接口
*
* @author MAENLIANG
* @date 2014-06-10
*/
public interface FoxBpmnViewBuilder {
public static final String SEQUENCE_STROKEWIDTH_DEFAULT = "2";
public static final String COLOR_FLAG = "#";
public static final String STROKE_DEFAULT = "black";
public static final String STROKEWIDTH_DEFAULT = "1";
public static final int LINEARGRADIENT_INDEX = 0;
public static final String COMMA = ",";
public static final String BACK_GROUND_PREFIX = "url(#";
public static final String BACK_GROUND_SUFFIX = ") #";
public static final String BRACKET_SUFFIX = ")";
public static final String TRANSLANT_PREFIX = "translate(";
public static final String ARIAL = "arial";
/**
* 设置节点信息
*/
public void setWayPoints(List<Point> pointList);
public void setWidth(float width);
public void setHeight(float height);
public void setXAndY(float x, float y);
public void setStroke(String stroke);
public void setStrokeWidth(float strokeWidth);
public void setFill(String fill);
public void setID(String id);
public void setName(String name);
public void setStyle(String style);
/**
* 设置文本信息
*/
public void setText(String text);
public void setTextFont(String font);
public void setTextStrokeWidth(float textStrokeWidth);
public void setTextX(float textX);
public void setTextY(float textY);
public void setTextFontSize(String textFontSize);
public void setTextStroke(String textStroke);
public void setTextFill(String textFill);
/**
* 设置子类型
*/
public void setTypeStroke(String stroke);
public void setTypeStrokeWidth(float strokeWidth);
public void setTypeFill(String fill);
public void setTypeStyle(String style);
/**
* 泳道负责重写该方法 setTextLocationByHerizonFlag
*/
public void setTextLocationByHerizonFlag(boolean herizonFlag);
}
3、所有元素的VO对象设置好之后,再根据流程定义提供的基本信息创建SVG模版对象,然后将所有的VO对象克隆并且添加到模版对象,然后将模版对象用JAXB 工具转化成最终的SVG XML文档
流程定义对象所包含的元素如下所以:
List<KernelFlowNodeImpl> flowNodes = new ArrayList<KernelFlowNodeImpl>();
Map<String, KernelSequenceFlowImpl> sequenceFlows = new HashMap<String, KernelSeque nceFlowImpl>();
List<KernelLaneSet> laneSets = new ArrayList<KernelLaneSet>();
List<KernelArtifact> artifacts = new ArrayList<KernelArtifact>();
foxbpm流程引擎构建SVG示意图如下: