/** * Created by wisestone on 2018-11-13. */ 'use strict'; define(['app', 'angular', 'd3'], function (app, angular, d3) { app.directive('jsWorkflowDisabled', ["$log", "$rootScope", function ($log, $rootScope) { return { scope : { issueStatusList : '=issueStatusList', vm : '=ngModel' }, restrict : 'E', replace : true, templateUrl : 'custom_components/js-workflow/js-workflow-disabled.html', controller : function ($scope, $element, $attrs) { // 함수 모음 $scope.fn = { initializeLayout : initializeLayout, makeNodeAndLink : makeNodeAndLink, forceLayout : forceLayout, initCircleColor : initCircleColor, initLinkColor : initLinkColor, makeNode : makeNode, makeLink : makeLink, makeGridLine : makeGridLine, executeForce : executeForce, setLink : setLink, setNode : setNode, setNodeTexts : setNodeTexts, targetClick : targetClick, tick : tick, startDiagram : startDiagram, setLastStatusPosition : setLastStatusPosition, setLastTransitionPosition : setLastTransitionPosition, updateClientChangePosition : updateClientChangePosition, }; // 변수 모음 $scope.vm.targetIssueStatusList = []; $scope.vm.issueStatuses = []; // 선택 가능한 이슈 상태 목록 $scope.vm.addIssueStatusId = null; // 추가하는 이슈 상태 아이디 $scope.vm.width = 0; // 넓이 $scope.vm.height = 0; // 높이 $scope.vm.svg = null; // svg 정보 $scope.vm.rect = null; // 다이어그램이 그려지는 사각형 정보 $scope.vm.nodes = []; // 원 정보 모음 $scope.vm.links = []; // 링크 정보 모음 $scope.vm.activeTarget = null; // 현재 선택한 대상 정보 $scope.vm.drag = null; $scope.vm.force = null; $scope.vm.node = null; $scope.vm.link = null; $scope.vm.nodeTexts = null; // 원에 표시되는 텍스트 $scope.vm.sourceStatusId = null; // 출발하는 이슈 상태 아이디 $scope.vm.targetStatusId = null; // 연결하는 이슈 상태 아이디 $scope.vm.transitionIdGenerator = 100; // 전이선 임시 id 값 $scope.$watch("issueStatusList", function (newValue) { if ($rootScope.isDefined(newValue)) { $scope.fn.startDiagram(); } }); // 다이어그램을 그리기 시작한다. function startDiagram() { $("#svgContent").empty(); $scope.vm.width = $(".workflowbox").width(); // 넓이 $scope.vm.height = $(".workflowbox").height(); // 높이 $scope.vm.activeTarget = null; // 타겟 초기화 $scope.fn.initCircleColor(null, true); // 마지막 선택 노드 색상 초기화 $scope.fn.forceLayout(); // 다이어그램 에디터 그리기 시작 } // 상태 마지막 위치 정보 저장 function setLastStatusPosition(issueStatus) { issueStatus.xLocation = $scope.vm.nodes[issueStatus.name].x; issueStatus.yLocation = $scope.vm.nodes[issueStatus.name].y; } // 전이선 마지막 위치 정보 저장 function setLastTransitionPosition(workflowTransition, index) { workflowTransition.correctX = $scope.vm.links[index].correctX; workflowTransition.correctY = $scope.vm.links[index].correctY; workflowTransition.direct = $scope.vm.links[index].direct; } // 클라이언트에서 변경한 위치 정보를 클라이언트에서 관리하는 데이터에 업데이트한다. function updateClientChangePosition() { // 서버에서 내려온 데이터에 전이선 변경 정보를 업데이트한다. angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo) { angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { for (var count in $scope.vm.links) { if ($scope.vm.links[count].id == workflowTransition.id) { $scope.fn.setLastTransitionPosition(workflowTransition, count); break; } } }); $scope.fn.setLastStatusPosition(issueStatusVo); }); } // 노드 색상을 초기화한다. function initCircleColor(target, all) { d3.selectAll("circle").each(function () { var circle = d3.select(this)[0][0]["__data__"]; if (all) { d3.select(this).style("fill", circle.originalColor); } else { if (circle.id != target.id) { d3.select(this).style("fill", circle.originalColor); } } }); } // 전이선 색상을 초기화한다. function initLinkColor(target, all) { d3.selectAll(".link").each(function () { var link = d3.select(this)[0][0]["__data__"]; // 전체 초기화일경우 if (all) { d3.select(this).classed("linkGreen", false); d3.select(this).classed(link.colorClass, true); } else { if (link.id != target.id) { d3.select(this).classed("linkGreen", false); d3.select(this).classed(link.colorClass, true); } } d3.select(this).attr("marker-end", "url(#licensing)"); }); } // 타겟을 클릭했을 때 색상을 변경한다. function targetClick(target) { $scope.vm.activeTarget = angular.copy(target[0][0]["__data__"]); // node 일경우 if ($scope.vm.activeTarget.type == "01") { // 노드, 링크 전체 초기화 $scope.fn.initCircleColor($scope.vm.activeTarget, false); $scope.fn.initLinkColor(null, true); // 대상 노드 색 변경 target.style("fill", "#1DDB16"); } else if ($scope.vm.activeTarget.type == "02") { // 노드, 링크 전체 초기화 $scope.fn.initCircleColor(null, true); $scope.fn.initLinkColor($scope.vm.activeTarget, false); // 대상 링크 색 변경 target.classed("linkGreen", true); target.classed($scope.vm.activeTarget.colorClass, false); target.attr("marker-end", "url(#greenLicensing)"); } if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') { $scope.$apply(); } } // 다이어그램에서 도형을 움직일 때 실행 function tick() { //IE mark bug fix $scope.vm.link.each(function () { this.parentNode.insertBefore(this, this); }); $scope.vm.link.attr("d", function (d) { //svg 영역 내에 표시 if (d.target.x > $scope.vm.width) { d.target.x = ($scope.vm.width - 31); } else if (d.target.x < 30) { d.target.x = 30; } if (d.source.x > $scope.vm.width) { d.source.x = ($scope.vm.width - 31); } else if (d.source.x < 30) { d.source.x = 30; } if (d.target.y > $scope.vm.height) { d.target.y = ($scope.vm.height - 31); } else if (d.target.y < 30) { d.target.y = 30; } if (d.source.y > $scope.vm.height) { d.source.y = ($scope.vm.height - 31); } else if (d.source.y < 30) { d.source.y = 30; } if (d.correctX == null) { d.correctX = d.source.x; } if (d.correctY == null) { d.correctY = d.source.y; } // 직선화 유지 상태 일 경우 if (d.direct) { d.correctX = d.source.x; d.correctY = d.source.y; } return "M" + d.source.x + "," + d.source.y + " Q" + d.correctX + ", " + d.correctY + " " + d.target.x + ", " + d.target.y; }); $scope.vm.node.attr("transform", function (d) { if (d.x > $scope.vm.width) { d.x = ($scope.vm.width - 31); } else if (d.x < 30) { d.x = 30; } if (d.y > $scope.vm.height) { d.y = ($scope.vm.height - 31); } else if (d.y < 30) { d.y = 30; } return "translate(" + d.x + "," + d.y + ")"; }); $scope.vm.nodeTexts.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }); // on 이벤트가 과다하게 발생하는 현상을 방지 $scope.vm.force.stop(); } // 노드를 만든다. function makeNode(issueStatus, sXLocation, sYLocation) { var xLocation = 0; var yLocation = 0; var color = "FAED7D"; // x좌표 if (issueStatus.xLocation == 0 || issueStatus.xLocation == null) { xLocation = sXLocation; } else { xLocation = issueStatus.xLocation; } // y좌표 if (issueStatus.yLocation == 0 || issueStatus.yLocation == null) { yLocation = sYLocation; } else { yLocation = issueStatus.yLocation; } if (issueStatus.color != null) { color = issueStatus.color; } return { id : issueStatus.id, name : issueStatus.name, x : xLocation, y : yLocation, color : color, issueStatusType : issueStatus.issueStatusType, originalColor : issueStatus.color, fixed : true, type : "01", // 수정 모드시 구분 issueStatus : issueStatus } } // 전이선을 만든다. function makeLink(workflowTransition) { return { id : workflowTransition.id, source : workflowTransition.sourceStatusName, target : workflowTransition.targetStatusName, correctX : workflowTransition.correctX, correctY : workflowTransition.correctY, direct : workflowTransition.direct, type : "02", colorClass : "linkBlue", workflowTransition : workflowTransition } } // 1. 서버에서 데이터가 내려올때 사용된 이슈 상태 전체 목록과 각 이슈 상태의 전이선 정보가 내려와야 한다. // 노드와 링크 정보를 생성한다. function makeNodeAndLink() { $scope.vm.nodes = []; // 원 정보 모음 $scope.vm.links = []; // 링크 정보 모음 var startX = 50; // 새로운 도형의 X축 위치 var startY = 50; // 새로운 도형의 Y축 위치 // 노드와 링크 정보를 만든다. angular.forEach($scope.vm.issueStatusVos, function (issueStatusVo, index) { $scope.vm.nodes[issueStatusVo.name] = $scope.fn.makeNode(issueStatusVo, startX, startY); angular.forEach(issueStatusVo.workflowTransitionVos, function (workflowTransition) { $scope.vm.links.push($scope.fn.makeLink(workflowTransition)); }); // 기본 좌표가 없을 경우 계속 60씩 증가 startX += 60; if (index % 2 == 0) { startY += 30; } else { startY += 30; } }); angular.forEach($scope.vm.links, function (link) { if (link.source == $scope.vm.nodes[link.source].name) { link.source = $scope.vm.nodes[link.source]; } if (link.target == $scope.vm.nodes[link.target].name) { link.target = $scope.vm.nodes[link.target]; } }); } // 기초 레이아웃을 설정한다. function initializeLayout() { $scope.vm.svg = d3.select("#svgContent") .append("svg") .attr("width", $scope.vm.width) .attr("height", $scope.vm.height); $scope.vm.rect = $scope.vm.svg.append("rect") .attr("width", $scope.vm.width) .attr("height", $scope.vm.height) .attr("fill", "white") .on("click", function (d) { // 원 색상 초기화 $scope.fn.initCircleColor(null, true); // 링크 색상 초기화 $scope.fn.initLinkColor(null, true); // 현재 선택한 대상 정보 $scope.vm.activeTarget = null; if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') { $scope.$apply(); } }); } // 표시되는 선을 생성한다. function makeGridLine() { // 기본 표시되는 선 $scope.vm.svg.append("marker") .attr("orient", "auto") .attr("markerHeight", 4) .attr("markerWidth", 4) .attr("refX", 18) .attr("viewBox", "0 -5 10 10") .attr("id", "licensing") .append("path") .attr("d", function () { return "M0,-5 L10,0 L0,5"; }); // 선을 눌러서 활성화되었을때 $scope.vm.svg.append("marker") .attr("orient", "auto") .attr("markerHeight", 5) .attr("markerWidth", 5) .attr("refX", 16) .attr("viewBox", "0 -5 10 10") .attr("id", "greenLicensing") .append("path") .attr("d", function () { return "M0,-5 L10,0 L0,5"; }); } // 포스 레이아웃 실행 function executeForce() { $scope.vm.force = d3.layout.force() .nodes(d3.values($scope.vm.nodes)) .links($scope.vm.links) .size([$scope.vm.width, $scope.vm.height]) .on("tick", $scope.fn.tick) .start(); } // 링크를 셋팅한다. function setLink() { $scope.vm.link = $scope.vm.svg.append("svg:g") .selectAll(".link") .data($scope.vm.force.links()) .enter() .append("path") .attr("class", function (d) { return "link " + d.colorClass; }) .attr("id", function (d, i) { return "link_" + i; }) .attr("marker-end", function (d) { return "url(#licensing)"; }) .attr("cursor", "pointer") .on("click", function () { $scope.fn.targetClick(d3.select(this)); }); } // 노드를 셋팅한다. function setNode() { $scope.vm.node = $scope.vm.svg.append("svg:g") .selectAll("circle") .data($scope.vm.force.nodes()) .enter() .append("circle") .attr("class", "node") .style("fill", function (d) { return d.color; }) .style("cursor", "pointer") .attr("r", 15) .on("click", function () { $scope.fn.targetClick(d3.select(this)); }); } // 노드 텍스트를 셋팅한다. function setNodeTexts() { $scope.vm.nodeTexts = $scope.vm.svg.append("svg:g") .selectAll("text") .data($scope.vm.force.nodes()) .enter() .append("text") .attr("class", "label") .attr("fill", "black") .attr("dy", 30) .style("font-size", "12px") .style("text-anchor", "middle") .text(function (d) { var name = ""; if (d.name != null) { if (d.name.length > 10) { name = d.name.substring(0, 10) + "..."; } else { name = d.name; } } return name; }); } // 워크플로우 다이어그램을 그린다. function forceLayout() { $scope.fn.initializeLayout($scope.vm.width, $scope.vm.height); $scope.fn.makeNodeAndLink(); $scope.fn.makeGridLine(); $scope.fn.executeForce($scope.vm.width, $scope.vm.height); $scope.fn.setLink(); $scope.fn.setNode(); $scope.fn.setNodeTexts(); } }, link : function (scope, element, attrs) { } } }]) });