// 포트 삼각형 도형의 가로길이
const PORT_ARROW_WIDTH = 10;
// 포트 삼각형 도형의 세로길이
const PORT_ARROW_HEIGHT = 20;
// 모델 타입들
const MODEL_TYPE = {
UNKNOWN : 0,
DISCRETE : 1,
ARRAY_ATOMIC : 10,
CONTINUOUS : 2,
FUNCTION : 3,
ATOMIC : 11,
COUPLED : 99,
EMBEDED_FUNC : 10000,
EMBEDED_FUNC_PRINT : 10001,
EMBEDED_FUNC_GRAPH_2D : 10002,
EMBEDED_FUNC_PLUS : 10003,
EMBEDED_FUNC_MINUS : 10004,
EMBEDED_FUNC_MUL : 10005,
EMBEDED_FUNC_DIV : 10006,
EMBEDED_FUNC_MATPLOTLIBPY : 10007,
};
const PORT_TYPE = {
IN_PORT : 1,
OUT_PORT : 2,
EX_IN_PORT : 3,
EX_OUT_PORT : 4,
};
// 라인 그리기시 방향
const DRAW_DIR = {
NONE : 0,
UP : 1,
RIGHT : 2,
DOWN : 3,
LEFT : 4
};
// 기본 모델 클래스(커플, 아토믹, 연속, 함수)
class base_model{
constructor(o){
this.is_root_model = o.is_root_model || false;//최상위 모델여부
this.parent = o.parent;
this.ctx = o.ctx;//canvas context
this.in_port = [];//입력포트
this.out_port = [];//출력포트
this.idx = o.idx || -1;//모델 index
this.ext_window = null;//그래프, 콘솔창 같이 특별히 연결된 창
this.models = [];//하위 모델들(base_model)
this.URL = o.URL;
this.load(o);
}
destructor(){
let d = document.getElementById(this.iid + '_div');
if(d !== undefined && d != null){//특별한 창이 있다면
console.log(d);
remove_resize_observer(d);//index.html에 있는 ResizeObserver 객체 삭제
d.remove();
}
this.models.forEach((v) => {
v.destructor();
});
this.in_port.forEach(v=>{
v.destructor();
});
this.out_port.forEach(v=>{
v.destructor();
});
}
load(o){
console.log(o);
this.iid = o.iid || get_random_string();//instance id
this.uid = o.uid || get_random_string();//unique id
this.font_title = 'bold 11pt arial';//모델명 출력 font
this.font_port = '10pt arial';//포트명 출력 font
this.file_name = o.file_name || 'untitled.json';
let ext = this.file_name.substring(this.file_name.lastIndexOf('.')+1).toLowerCase();//모델 파일 확장자
if(o.model_type === undefined && ext == 'json'){
o.model_type = MODEL_TYPE.COUPLED;
}
else if(o.model_type === undefined){
if(ext == 'dll' || ext == 'py' || ext == 'm'){
o.model_type = MODEL_TYPE.DISCRETE;
}
}
this.model_type = o.model_type || MODEL_TYPE.COUPLED;
if(this.model_type == 4)
this.model_type = MODEL_TYPE.EMBEDED_FUNC_PRINT;
if(this.model_type == 10)
{
this.model_type = MODEL_TYPE.ARRAY_ATOMIC;
this.InstanceCount = o.InstanceCount;
}
if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY){//콘솔출력창 모델일 경우
this.model_type = MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY;
}
if(this.model_type == 2)
{
this.model_type = MODEL_TYPE.CONTINUOUS;
this.model_subtype = o.model_subtype;
}
this.file_path = o.file_path || project_name;
if(o.ip !== undefined){
o.ip_port = o.ip + ':' + o.port;
}
this.ip_port = o.ip_port || 'localhost:8865';//모델 실행을 위한 접속 주소
this.model_name = o.model_name || 'untitled';//모델 명
this.ctx.font = this.font_title;
this.fontmetrics = this.ctx.measureText(this.model_name);
this.fontHeight = this.fontmetrics.fontBoundingBoxAscent + this.fontmetrics.fontBoundingBoxDescent;//폰트 높이
if(o.position !== undefined){
if(o.position.x < 150)
o.position.x = 150;
if(o.position.y < 50)
o.position.y = 50;
this.position = new vector2({x:o.position.x, y:o.position.y});//모델 위치
}
else
this.position = new vector2({x:150, y:50});
this.color = o.color || get_random_color();//포트 컬러
let port_name_max_length = 0;//최대 모델 가로 크기를 알기 위해 포트명 최대 길이를 구한다
let port_name_max_length_str = '';
if(o.models !== undefined){
o.models.forEach((v)=>{
this.add_model(v);
});
}
if(o.out_port !== undefined){
o.out_port.forEach((v, idx) => {
if(this.is_root_model && (this.model_type == MODEL_TYPE.COUPLED || this.model_type == MODEL_TYPE.ARRAY_ATOMIC))
this.add_eout_port(v);
else
this.add_out_port(v, false);
let port_name = v.name || v;
if(port_name_max_length < port_name.length){
port_name_max_length = port_name.length;
port_name_max_length_str = port_name;
}
});
}
else{
this.out_port = [];
}
if(o.in_port !== undefined){
o.in_port.forEach((v, idx) => {
if(this.is_root_model && (this.model_type == MODEL_TYPE.COUPLED || this.model_type == MODEL_TYPE.ARRAY_ATOMIC))
{
this.add_ein_port(v);
}
else
{
if(v.iid=="")
{
v.iid = get_random_string();
}
this.add_in_port(v, false);
}
let port_name = v.name || v;
if(port_name_max_length < port_name.length){
port_name_max_length = port_name.length;
port_name_max_length_str = port_name;
}
});
}
else{
this.in_port = [];
}
if(o.in_param !== undefined){
this.in_param = o.in_param;//모델 파라미터
}
else{
this.in_param = [];
}
this.selected = false;//모델 선택여부
this.selected_pos = new vector2({});
this.ctx.font = this.font_port;
var fm = this.ctx.measureText(port_name_max_length_str);
this.width = fm.width + PORT_ARROW_WIDTH * 4;
if((this.fontmetrics.width + PORT_ARROW_WIDTH * 2) > this.width)
this.width = this.fontmetrics.width + PORT_ARROW_WIDTH * 2;
//this.model_name_lines = wrap_text(this.model_name, this.width - 10, this.ctx);
let min_height = this.fontHeight * 1.5;//(this.model_name_lines.length + 0.5);
if(this.in_port.length > 0 || this.out_port.length > 0)
min_height += ((this.in_port.length > this.out_port.length? this.in_port.length * (PORT_ARROW_HEIGHT * 2):this.out_port.length * (PORT_ARROW_HEIGHT*2) + PORT_ARROW_HEIGHT));
this.height = min_height;
if(this.width < 100)
this.width = 100;
if(this.height < 100)
this.height = 100;
if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_PRINT){//콘솔출력창 모델일 경우
add_print_window(this.iid);
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_GRAPH_2D){//2D 그래프 출력창 모델일경우
add_graph2d_window(this.iid);
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY){//2D 그래프 출력창 모델일경우
add_graph2d_window(this.iid);
}
console.log(this.model_type, o);
// 모든 모델의 포트가 생성된 후 커플링
// 출발포트에 라인정보(커플링)가 있으나 모델에서 라인정보를 가지고 있는것으로 수정필요
if (o.in_port !== undefined && (this.model_type == MODEL_TYPE.COUPLED || this.model_type == MODEL_TYPE.ARRAY_ATOMIC))
{//커플모델일경우 입력포트에서 라인 출력
o.in_port.forEach((v, idx) => {
console.log(v.line);
if (v.line !== undefined) {
v.line.forEach(l => {
this.set_coupling(l);
});
}
});
if (o.models !== undefined) {
o.models.forEach((m) => {//하위모델들
if (m.out_port !== undefined) {//출력포트
m.out_port.forEach((v, idx) => {
if (v.line !== undefined) {
v.line.forEach(l => {
this.set_coupling(l);
});
}
});
}
});
}
}
/*if(o.couples !== undefined && Array.isArray(o.couples)){
o.couples.forEach((v) =>{
this.set_coupling(v);
});
}*/
}
// project view창의 트리뷰를 위한 정보 생성
get_tree(ret, parentid){
let d = new Object();
d.id = this.iid;
d.parent = parentid || "#";
d.text = this.model_name;
if(this.model_type == MODEL_TYPE.COUPLED){
d.icon = "images/cpd.png";
}
else if(this.model_type >= MODEL_TYPE.EMBEDED_FUNC){
d.icon = "images/function.png";
}
else{
d.icon = "images/atomic.png";
d.text = this.file_name;
}
d.state = {};
d.state.opened = true;
ret.push(d);
this.models.forEach(v=>{
v.get_tree(ret, this.iid);
});
return ret;
}
// external in port
add_ein_port(o){
this.in_port.push(new base_port(Object.assign(o, {parent:this, ctx:this.ctx,
port_type:PORT_TYPE.EX_IN_PORT, color:'rgba(128,128,128,0.5)', idx:this.in_port.length})));
return this.in_port[this.in_port.length-1];
}
// external out port
add_eout_port(o){
this.out_port.push(new base_port(Object.assign(o, {parent:this, ctx:this.ctx,
port_type:PORT_TYPE.EX_OUT_PORT, color:'rgba(128,128,128,0.5)', idx:this.out_port.length})));
return this.out_port[this.out_port.length-1];
}
add_in_port(o, update_size = true){
let p = new base_port(Object.assign(o, {parent:this,
ctx:this.ctx,
port_type:PORT_TYPE.IN_PORT,
color:this.color, idx:this.in_port.length}));
this.in_port.push(p);
if(update_size){//포트가 추가되어 기존 모델 크기보다 더 커져야 되는지 확인
let port_name = o.name || o;
this.ctx.font = this.font_port;
var fm = this.ctx.measureText(port_name);
if(this.width < fm.width + PORT_ARROW_WIDTH * 4)
this.width = fm.width + PORT_ARROW_WIDTH * 4;
let min_height = this.fontHeight * 1.5;//(this.model_name_lines.length + 0.5);
if(this.in_port.length > 0 || this.out_port.length > 0)
min_height += ((this.in_port.length > this.out_port.length? this.in_port.length * (PORT_ARROW_HEIGHT * 2):this.out_port.length * (PORT_ARROW_HEIGHT*2) + PORT_ARROW_HEIGHT));
this.height = min_height;
}
return p;
}
add_out_port(o, update_size = true){
let p = new base_port(Object.assign(o, {parent:this,
ctx:this.ctx,
port_type:PORT_TYPE.OUT_PORT,
color:this.color, idx:this.out_port.length}));
this.out_port.push(p);
if(update_size){//포트가 추가되어 기존 모델 크기보다 더 커져야 되는지 확인
let port_name = o.name || o;
this.ctx.font = this.font_port;
var fm = this.ctx.measureText(port_name);
if(this.width < fm.width + PORT_ARROW_WIDTH * 4)
this.width = fm.width + PORT_ARROW_WIDTH * 4;
let min_height = this.fontHeight * 1.5;//(this.model_name_lines.length + 0.5);
if(this.in_port.length > 0 || this.out_port.length > 0)
min_height += ((this.in_port.length > this.out_port.length? this.in_port.length * (PORT_ARROW_HEIGHT * 2):this.out_port.length * (PORT_ARROW_HEIGHT*2) + PORT_ARROW_HEIGHT));
this.height = min_height;
}
return p;
}
// 자식모델 추가
add_model(o){
//console.log(JSON.stringify(o));
if(Array.isArray(o.model_name)){
let m = o.model_name;
m.forEach(v =>{
this.models.push(new atomic(Object.assign(o, {parent:this, ctx:this.ctx, width:100, height:100, idx:this.models.length, model_name:v})));
});
}
else{
if(o.model_type != 0)
{
this.models.push(new atomic(Object.assign(o, {parent:this, ctx:this.ctx, width:100, height:100, idx:this.models.length})));
}
else
{
return;
}
}
return this.models[this.models.length-1];
}
// iid로 모델 찾기
// is_search_sub_models : child 모델 하위로 계속 찾을것인지 여부
get_model_by_id(model_id, is_search_sub_models = false){
//console.log(model_id, is_search_sub_models, this.iid);
if(this.iid == model_id){
return this;
}
let ret = null;
this.models.forEach(v=>{
if(v.iid == model_id){
ret = v;
return;
}
});
if(ret == null && is_search_sub_models){
this.models.forEach(v=>{
let ret2 = v.get_model_by_id(model_id, is_search_sub_models);
if(ret2 != null){
ret = ret2;
return;
}
});
}
return ret;
}
// 모델명으로 모델 찾기
// get_model_by_id()와 달리 하위 모델 1 depth에서만 찾는다
get_model_by_name(model_name){
let ret = null;
this.models.forEach(v=>{
if(v.model_name == model_name){
ret = v;
return;
}
});
return ret;
}
// 모델 커플링
set_coupling(o){
console.log(o);
let from_obj = null;
if (o.from_obj_info.iid == 'this' || o.from_obj_info.iid == this.iid) {
this.in_port.forEach((v, idx) => {
if (v.name == o.from_obj_info.port_name) {
from_obj = v;
return;
}
});
}
else {
let src = this.get_model_by_id(o.from_obj_info.iid, true);
if (src != null) {
console.log(src);
src.out_port.forEach((v, idx) => {
if (v.name == o.from_obj_info.port_name) {
from_obj = v;
return;
}
});
}
}
if(from_obj == null){
console.log('error : set_coupling::from_obj is null', o, this.iid);
return;
}
let to_obj = null;
if(o.to_obj_info.iid == 'this' || o.to_obj_info.iid == this.iid){
this.out_port.forEach((v, idx) =>{
if(v.name == o.to_obj_info.port_name){
to_obj = v;
return;
}
});
}
else{
let to_obj_model = this.get_model_by_id(o.to_obj_info.iid, true);
if(to_obj_model == null){
console.log('error : set_coupling::to_obj_model is null', o, this.iid);
return;
}
to_obj_model.in_port.forEach((v, idx) =>{
if(v.name == o.to_obj_info.port_name){
to_obj = v;
return;
}
});
}
if(to_obj != null){
from_obj.load(Object.assign(o, {to_obj:to_obj}));
}
}
set_coupling2(o){
console.log(o);
let from_obj = null;
if(o.from_obj_info.iid == 'this' || o.from_obj_info.iid == this.iid){
this.in_port.forEach((v, idx) =>{
if(v.name == o.from_obj_info.port_name){
from_obj = v;
return;
}
});
}
else{
let src = this.get_model_by_id(o.from_obj_info.iid, true);
if(src != null){
console.log(src);
src.out_port.forEach((v, idx) =>{
if(v.name == o.from_obj_info.port_name){
from_obj = v;
return;
}
});
}
}
if(from_obj == null){
console.log('error : set_coupling::from_obj is null', o, this.iid);
return;
}
let to_obj = null;
if(o.to_obj_info.iid == 'this' || o.to_obj_info.iid == this.iid){
this.out_port.forEach((v, idx) =>{
if(v.name == o.to_obj_info.port_name){
to_obj = v;
return;
}
});
}
else{
let to_obj_model = this.get_model_by_id(o.to_obj_info.iid, true);
if(to_obj_model == null){
console.log('error : set_coupling::to_obj_model is null', o, this.iid);
return;
}
to_obj_model.in_port.forEach((v, idx) =>{
if(v.name == o.to_obj_info.port_name){
to_obj = v;
return;
}
});
}
if(to_obj != null){
from_obj.load(Object.assign(o, {to_obj:to_obj}));
}
}
save(){
let d = new Object();
d.model_name = this.model_name;
d.iid = this.iid;
d.uid = this.uid;
d.idx = this.idx;
d.model_type = this.model_type;
d.file_name = this.file_name;
d.file_path = this.file_path;
d.ip_port = this.ip_port;
d.position = this.position;
d.color = this.color;
d.URL = this.URL;
d.InstanceCount = this.InstanceCount;
if(this.model_type == 2)
{
d.model_subtype = this.model_subtype;
}
d.models = [];
this.models.forEach((v) => {
d.models.push(v.save());
});
d.in_port=[];
this.in_port.forEach(v=>{
d.in_port.push(v.save());
});
d.out_port=[];
this.out_port.forEach(v=>{
d.out_port.push(v.save());
});
d.in_param=this.in_param;
console.log(d);
return d;
}
// 시뮬레이션 서버에서 init을 요청한 경우(함수모델, console, graph같은...)
on_init_from_sim_server(){
if(this.model_type < MODEL_TYPE.EMBEDED_FUNC){
return;
}
if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_PRINT){
if(this.ext_window == null){
this.ext_window = document.getElementById(this.iid + '_div_content');
}
this.ext_window.innerHTML = '';
this.ext_window.scrollTop = this.ext_window.scrollHeight;
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_GRAPH_2D){
graph_clear(this.iid);
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY){
graph_clear(this.iid);
}
}
// 시뮬레이션 서버에서 msg를 보낸경우
on_message_from_sim_server(json_msg){
if(this.model_type < MODEL_TYPE.EMBEDED_FUNC){
return;
}
if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_PRINT){
if(this.ext_window == null){
this.ext_window = document.getElementById(this.iid + '_div_content');
}
// this.ext_window.innerHTML += json_msg.sim_time.toFixed(2) + '][' + json_msg.src_port_name
// + '] ' + json_msg.value + '
';
this.ext_window.innerHTML += json_msg + '
';
this.ext_window.scrollTop = this.ext_window.scrollHeight;
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_GRAPH_2D){
graph_add_data(this.iid, json_msg.src_port_name,
Math.round(json_msg.sim_time*1e3)/1e3, json_msg.value);
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY){
graph_add_data(this.iid, json_msg.src_port_name,
Math.round(json_msg.sim_time*1e3)/1e3, json_msg.value);
}
}
// 시뮬레이션 서버에서 msg를 보낸경우
on_message_from_sim_serve2(src_port_name, json_msg, time){
graph_add_data(this.iid, src_port_name,
Math.round(time*1e3)/1e3, json_msg);
}
// 입력 좌표가 모델 범위 안에 있는지
chk_mousepos(x, y){
if ((x >= (this.position.x)) &&
(x <= (this.position.x + this.width)) &&
(y >= (this.position.y)) &&
(y <= (this.position.y + this.height))) {
return true;
}
return false;
}
// 마우스 더블클릭시
on_mousedbclick(x, y){
if(this.chk_mousepos(x, y)){
if(this.model_type == MODEL_TYPE.COUPLED){//커플모델일경우 모델 자세히 보기
console.log('cpd', this.model_name, this.file_name);
//save_to_stack();
//setTimeout(load_from_api_server, 100, project_name, this.file_name, this.uid);
cpd_designer.set_model(this);
}
else{
console.log(this.model_name, this.file_name);
if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_PRINT){//콘솔창 모델일 경우 창 활성화
let d = document.getElementById(this.iid + '_div');
d.style.display='';
d.style.left = event.clientX;
d.style.top = event.clientY;
if(this.ext_window == null){
this.ext_window = document.getElementById(this.iid + '_div_content');
}
this.ext_window.scrollTop = this.ext_window.scrollHeight;
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_GRAPH_2D){//그래프 모델일 경우 창 활성화
let d = document.getElementById(this.iid + '_div');
d.style.display='';
d.style.left = event.clientX;
d.style.top = event.clientY;
line_graph[this.iid+'_div_graph'].update();
}
else if(this.model_type == MODEL_TYPE.EMBEDED_FUNC_MATPLOTLIBPY){//그래프 모델일 경우 창 활성화
let d = document.getElementById(this.iid + '_div');
d.style.display='';
d.style.left = event.clientX;
d.style.top = event.clientY;
line_graph[this.iid+'_div_graph'].update();
}
}
return 1;
}
return 0;
}
// 마우스 클릭시
on_mousedown(x, y){
for(var i=0;i= 150)
this.position.x = new_x;
if(new_y >= 50)
this.position.y = new_y;
cpd_designer.on_model_changed(this);// 모델이 변경되었기에 SAVE버튼 활성화
}
else{
this.in_port.forEach(v => v.on_mousemove(x, y, dx, dy));
this.out_port.forEach(v => v.on_mousemove(x, y, dx, dy));
}
}
on_mouseup(x, y){
if(this.selected){
this.selected = false;
cpd_designer.adjust_pane();//모델 위치가 변경되었수도 있어 창크기 조절
}
else{
this.in_port.forEach(v => v.on_mouseup(x, y));
this.out_port.forEach(v => v.on_mouseup(x, y));
}
}
// 모델 삭제시
delete_obj(){
// 하위모델은?
this.in_port.forEach(v=>{
v.delete_obj();
});
this.out_port.forEach(v=>{
v.delete_obj();
});
}
// 백그라운드 그리기
draw_bg(){
this.ctx.save();
this.ctx.font
this.ctx.font = this.font_title;
this.ctx.fillStyle = 'rgba(255,255,255,0.8)';
this.ctx.strokeStyle = 'black';
this.ctx.beginPath();
this.ctx.roundRect(this.position.x, this.position.y, this.width, this.height, [5, 5, 0, 0]);//잴 외곽 상단 두곳만 동그랗게
this.ctx.fill();//패스 채우기
this.ctx.stroke();//라인그리기
this.ctx.beginPath();
// 위쪽 타이틀 부분 그리기
this.ctx.roundRect(this.position.x, this.position.y, this.width, this.fontHeight * 1.5, [5, 5, 0, 0]);//(this.model_name_lines.length + 1));//제목 외곽
if(this.model_type >= MODEL_TYPE.EMBEDED_FUNC){//모델에 따라 배경색 다르게, 이것도 case별로 지정하는걸로 바꿔야...
this.ctx.fillStyle = 'rgba(0,0,255,0.2)';
}
else if(this.model_type == MODEL_TYPE.ARRAY_ATOMIC){
this.ctx.fillStyle = 'rgba(255,0,0,0.8)';
}
else if(this.model_type == MODEL_TYPE.DISCRETE){
this.ctx.fillStyle = 'rgba(217,65,197,0.2)';
}
else if(this.model_type == MODEL_TYPE.CONTINUOUS){
this.ctx.fillStyle = 'rgba(0,216,255,0.2)';
}
else{
this.ctx.fillStyle = 'rgba(0,255,0,0.2)';
}
this.ctx.fill();
this.ctx.textBaseline = 'top';
this.ctx.textAlign = 'center';
this.ctx.fillStyle = 'rgba(0,0,0,1)';
this.ctx.fillText(this.model_name, this.position.x + this.width * 0.5, this.position.y + this.fontHeight * 0.25);
this.ctx.restore();
return this.fontHeight * 1.5;//(this.model_name_lines.length + 1);
}
// 모델 그리기
draw(){
this.ctx.save();
var port_y = this.draw_bg();//배경 그리기
this.ctx.font = this.font_port;
this.ctx.textAlign='left';
this.ctx.textBaseline = 'middle';
for(var i=0;i