이전 프로젝트에서, 화면의 객체(최상위 node)를 선택하고 검색 조건을 입력한 후 검색을 하면,
그 하위 엘리먼트로 검색 결과의 정보를 보여주는 다중 계층 엘리멘트를 구성하는 동적 화면을 구성한적이 있다.
그런데 이번 프로젝트에도 비슷한 기능을 구현해야하는게 있다.
여긴 FLEX로 구현 하면 되는데, 기술자를 더 연장해야하는 비용의 문제로 이전 기억을 더듬어(프로젝트 소스는 가져오지 않기에 내겐 기억으로 밖에 존재하지 않는다.) 구현하기로 했다.
이번엔 BLOGGER에 정리를 해두고자 한다.
나도 다음에 사용할 수 있겠지만 다른 개발자들도 사용할 수 있으리라...
(나의 희소성이 떨어질려나? ㅋㅋ)
예전부터 생각한거지만 개발은 노가다가 아니라 창조라는 말을 하고싶다.
그리고 창조는 즐겁다.
1. 구현 목표
a. 저장매체에 있는 데이터를 불러 와서 아래와 같은 화면을 구성한다.
b. 주위에 있는 객체를 선택하면 그 객체와 관련된 데이터를 불러와 아래와 같은 화면을 구성한다.
2. 설계시 고려사항
문서를 보고 개발 설계를 할때 고려해야할 사항들은 어떤게 있을까?
a화면을 구성할때
- 모든 구성원들의 좌표 계산
기준이 되는 parent node의 좌표
기준을 중심으로 주위 구성원들의 좌표 자동계산
- 구성원들의 연결(vml이용 기준과 자식 구성원의 좌표 계산)
a에서 b화면으로 재구성 될때
문서상의 요구사항은 기존 구성은 비활성화 되어야한다. 명시는 되어있지 않지만 두개의 구성체를 한 화면에 넣으려면 기존 구성체의 크기도 작아져야할 것이다.
위 그림에서 보자면 선택한 자식노드가 중앙으로 배치 되며 기존 구성체는 좌측 아래로 밀려야한다. 좌표 계산이 힘들지 않을까? 위 그림은 기존 구성체의 우측 상단 자식 구성원을 선택했으므로 기존 구성체는 좌측 아래로 밀리는것이지만 만약 좌측 아래 자식 구성원을 선택한다면 기존 구성체는 반대로 우측 상단으로 밀려야하나?
차라리 밀리는 방향은 하나로 정해두고 자식 구성원을 선택한 구성원을 기준으로 재배치 하는게 더 쉽지 않을까? 그런데 두가지 다 재밌을거 같다.
또한 두번째 구성체는 기존 구성체의 경우 처럼 360도 모두를 사용하는게 아니라 기존 구성체를 차지하는 범위를 제외하고 구성해야해서 사용범위를 달리 정해야한다....
처리해야할 이벤트는 하나다. 자식 노드를 선택하면 그 자식 구성원을 기준으로 하나의 구성체만 더 만들면 되니까. 그리고 depth도 최대 2단계라고 했으니. 위 정도의 고려면 충분할듯.
자 이제 대충 고려사항은 나왔으니 개발 설계를 어떻게 할까?
설계에 대한 내용은 개발이 완료 되고 난 후 정리
(다른 개발자들이 느낄때 이 기능구현 난이도는 어떨까?)
기대하시라... ^^
4. 우선 기반 기술이 될 만한 소스를 수집하자.
이 업을 시작하기 초반에는 개발에 대한 기반 지식이 없어 강좌를 비롯 많은 책을 읽으면서 지식을 쌓았고 개발을 했다.
하지만 어느 정도 체계가 잡히고 나선 책보다는 구글링이라는 인터넷에서 많은 기술을 습득하게 되었다.
그리고 새로운 기술이 나오거나 다른 언어 또는 플러그인을 접하게 되면 근간이 되는 흐름을 파악하고 대부분 api를 통해서 필요한 것들을 습득한다.
엄청 많은 기술과 세부적인 사항을 모두 강좌나 책으로 습득한다면 공부하기도 바쁠것이기에...
우선 좌표 구하기...
jquery 절대 좌표
var p = $("p:last");
var offset = p.offset();
p.html( "left: " + offset.left + ", top: " + offset.top );
jquery 상대좌표
var p = $("p:first");
var position = p.position();
$("p:last").text( "left: " + position.left + ", top: " + position.top );
그리고 이번 프로젝트의 요구사항은 절대 좌표를 이용해서 구성원들을 배열해야할것이다.
<div id="maddog" class="aaaa" style='position: absolute; left: 2; top: 195; width: 230;
height: 150; background-color: 666666; font-size: 11px; color: #5BF9F5; overflow: auto;
padding-top: 5; padding-left: 5; padding-bottom: 5; filter: Alpha(Opacity=50);
clip: rect(0 230 150 0); border: 1px none #000000; visibility: visible; z-index: 1'>
여기에 구성원들을 넣기만 하면 된다.
</div>
위에서 내가 원하는건 위 빨간 색의 attribute이다. 절대 좌표로 위치 시키기...
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>jQuery 1.3.2 VMl Test Stuff</title>
<style type="text/css" media="screen">
#roundme {
width: 100px;
height: 100px;
position: relative;
background: red;
}
</style>
<script src="http://code.jquery.com/jquery-1.3.2.js" type="text/javascript"></script>
<script type="text/javascript" charset="utf-8">
$(window).load(function() {
if ($.browser.msie) {
if (!document.namespaces.v) {
document.namespaces.add("v", "urn:schemas-microsoft-com:vml");
var ss = document.createStyleSheet().owningElement;
ss.styleSheet.cssText = "v\\:*{behavior:url(#default#VML);}";
}
var roundrect = $('<v:roundrect arcsize="0.1" style="width:100px;height:100px; position: absolute; top: 0; left: 0;"></v:roundrect>');
var roundme = $("#roundme");
roundme.append(roundrect);
}
});
</script>
</head>
<body>
<div id="roundme">
<span class="goo">Foo</span>
</div>
</body>
</html>
개발 준비도 대충은 된듯하다.
이제 어떻게 조합하고 어떻게 구현하느냐....
한번 해봤다고 구현 시간이 줄었다.
근데 이렇게 했는데 팀장은 놀라지도 않네. ㅋ
당연히 할수 있는거 아니냐는 듯한 반응...
설명은 다음에...
개인적인 생각은 소스가 좀더 체계적(javascript class 개념이 부족해서)이었으면 좋겠다.
-----------------------------------------------------------------
vml.js
-----------------------------------------------------------------
var tm;
$(document).ready(function() {
$('#addCircle').click(function() {
var item = new Item('Paris', 'key', '', '', true);
var map = new Map();
map.put('key01', new Item('France', 'key01', 'A', false, false));
map.put('key02', new Item('Soccer', 'key02', 'A', false, false));
map.put('key03', new Item('Provence', 'key03', 'A', false, false));
map.put('key04', new Item('Pont Neuf', 'key04', 'A', false, false));
map.put('key05', new Item('Nice', 'key05', 'A', true, false));
map.put('key06', new Item('Eiffel Tower', 'key06', 'A', false, false));
map.put('key07', new Item('Cannes', 'key07', 'A', false, false));
map.put('key08', new Item('Louvre', 'key08', 'A', true, false));
map.put('key09', new Item('파리', 'key09', 'S', false, false));
map.put('key10', new Item('France', 'key10', 'A', false, false));
map.put('key11', new Item('Montmartre', 'key11', 'A', true, false));
map.put('key12', new Item('Fly', 'key12', 'A', false, false));
tm = new ThesaurusMap(item, map);
tm.draw(true, true);
});
});
function viewMore(strkey)
{
var newMap = new Map();
var isStart = false;
var arrKeys = tm.map.keys();
for(var i=0; i<arrKeys.length; i++)
{
if(arrKeys[i] == strkey) isStart = true;
if(isStart) newMap.put(arrKeys[i], tm.map.get(arrKeys[i]));
}
for(i=0; i<arrKeys.length; i++)
{
if(arrKeys[i] == strkey) break;
newMap.put(arrKeys[i], tm.map.get(arrKeys[i]));
}
tm.erase();
tm.map = newMap;
tm.draw(false, false);
var newName = tm.map.get(arrKeys[i]).name;
var item = new Item(newName, 'key', '', '', true);
var map = new Map();
map.put('key01', new Item(newName+'1', 'key01', 'S', false, false));
map.put('key02', new Item(newName+'2', 'key02', 'S', false, false));
map.put('key03', new Item(newName+'3', 'key03', 'S', false, false));
map.put('key04', new Item(newName+'4', 'key04', 'S', false, false));
map.put('key05', new Item(newName+'5', 'key05', 'A', true, false));
map.put('key06', new Item(newName+'6', 'key06', 'A', false, false));
map.put('key07', new Item(newName+'7', 'key07', 'S', false, false));
map.put('key08', new Item(newName+'8', 'key08', 'A', true, false));
map.put('key09', new Item(newName+'9', 'key09', 'S', false, false));
var newTm = new ThesaurusMap(item, map);
newTm.draw(false, true);
}
ThesaurusMap = function(cItem, itemMap){
this.map = new Object();
this.cItem = cItem;
this.map = itemMap;
};
ThesaurusMap.prototype = {
draw : function(isSingle, isMain){
var obj = document.getElementById('roundme');
var posi = getAbsolutePos(obj);
//가운데 Item
this.cItem.setPosition(getIPos(posi, obj, isMain));
var roundme = $("#roundme");
roundme.append($(getCircle(this.cItem.position, this.cItem, isMain)));
var circleRound = isSingle || !isMain ? 360 : 300;
var radian = circleRound/this.map.size()*Math.PI/180;
var arrKey = this.map.keys();
for(var i=0; i<arrKey.length; i++)
{
if(isMain || i!=0)
{
var cnt = isSingle || !isMain ? i : radian*(i) > 2.4 ? i + this.map.size()*2/9: i;
var sidP = getIPos(this.cItem.position, '', isMain, radian*(cnt));
this.map.get(arrKey[i]).setPosition(sidP);
roundme.append($(getDiv(sidP, this.map.get(arrKey[i]), isSingle, isMain)));
roundme.append($(getLine(getLinePos(this.cItem, this.map.get(arrKey[i]), radian*(cnt)), isMain)));
}
if(!isSingle && isMain && i==0) //map 과 map사이의 선
{
roundme.append($(getLine(getLinePos(this.cItem, tm.cItem, Math.PI), isMain)));
}
}
},
erase : function () {
var obj = document.getElementById('roundme');
var cn = obj.childNodes;
for(var i=cn.length-1; i>=0; i--)
obj.removeChild(cn[i]);
}
};
Item = function(name, key, type, isNew, isCenter)
{
this.name = name;
this.key = key;
this.type = type; //S, A
this.isNew = isNew; //true, false;
this.isCenter = isCenter; //true, false
};
Item.prototype = {
setPosition : function(position){
this.position = position;
}
};
function getAbsolutePos(obj) {
var position = new Object;
position.x = position.y = 0;
if( obj ) {
position.x = obj.offsetLeft;
position.y = obj.offsetTop;
if( obj.offsetParent ) {
var parentpos = getAbsolutePos(obj.offsetParent);
position.x += parentpos.x;
position.y += parentpos.y;
}
}
return position;
}
//Item 좌표
function getIPos(posi, obj, isMain, radian)
{
var position = new Object;
var width = isMain ? 114 : 57;
if(radian == null)
{
position.x = posi.x + (obj.offsetWidth - width)/ (isMain ? 2 : 6);
position.y = posi.y + (obj.offsetHeight - width)/2;
}
else
{
width = isMain ? 70 : 50;
var wRadius = isMain ? 200 : 100;
var hRadius = isMain ? 200 : 100;
position.x = (posi.x + posi.w/2) + wRadius * Math.cos(radian) - width/2;
position.y = (posi.y + posi.h/2) + hRadius * Math.sin(radian) - width/2;
}
position.w = width;
position.h = width;
position.cx = position.x + width/2;
position.cy = position.y + width/2;
return position;
}
//line 좌표
function getLinePos(cItem, sItem, radian)
{
var position = new Object;
//over pi
sRadian = radian > Math.PI ? radian - Math.PI : radian + Math.PI;
var tampXa = cItem.position.cx + cItem.position.w/2 * Math.cos(radian); //center circle x
var tampYa = cItem.position.cy + cItem.position.h/2 * Math.sin(radian); //cneter circle y
var tampXb = sItem.position.cx + sItem.position.w/2 * Math.cos(sRadian); //side circle x
var tampYb = sItem.position.cy + sItem.position.h/2 * Math.sin(sRadian); //side circle y
if(radian >= Math.PI)
{
position.sx = tampXb;
position.sy = tampYb;
position.ex = tampXa - tampXb;
position.ey = tampYa - position.sy;
}
else
{
position.sx = tampXa;
position.sy = tampYa;
position.ex = tampXb - tampXa;
position.ey = tampYb - tampYa;
}
return position;
}
//원을 구성 string
function getCircle(posi, item, isMain)
{
var strObj = '<v:oval style="position:absolute;text-align:center;cursor:hand'
+ ';left:' + posi.x + ';top:' + posi.y
+ ';width:' + posi.w + ';height:' + posi.h
+ ';color:' + (isMain ? 'black' : 'gray') + ';padding-top:' + (posi.h/2-8)+'px;"'
+ ' strokecolor="' + (isMain ? 'gray' : '#cccccc')
+ '" strokeweight="' + (isMain ? '1.5pt' : '1pt') + '">'+item.name+'</v:oval>';
return strObj;
}
//div 객체 구성 string
function getDiv(posi, item, isSingle, isMain)
{
if(isMain) tColor = item.type == 'S' ? 'darkblue' : 'CC0099';
else tColor = item.type == 'S' ? '#E9CFEC' : '#FCDFFF';
var clickEvent = !isSingle && isMain ? '' : ' onClick="viewMore(\''+item.key+'\');"';
var strObj = '<div ' + clickEvent + ' style="position:absolute;text-align:center;cursor:hand'
+ ';left:' + posi.x + ';top:' + posi.y
+ ';width:' + posi.w + ';height:' + posi.h
+ ';color:' + tColor
+ ';padding-top:'+(posi.h/2-8)+'px;">'+item.name+'</div>';
return strObj;
}
//lin 객체 구성 string
function getLine(posi, isMain)
{
var strObj = '<div style="position:absolute;left:'+posi.sx
+ ';top:' + posi.sy + ';width:0;height:0;background-color:yellow;">'
+ '<v:line from="0,0" to="' + posi.ex + ',' + posi.ey + '"'
+ ' strokecolor="'+(isMain ? 'gray' : '#cccccc')
+ '" strokeweight="' + (isMain ? '1.5pt' : '1pt') + '"><v:stroke dashstyle="dot"/></v:line>';
return strObj;
}
-----------------------------------------------------------------
vml.html
-----------------------------------------------------------------
<html xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Cp Tag Thesaurus Map</title>
<style>
v\:* { behavior: url(#default#VML); }
</style>
<script src="http://code.jquery.com/jquery-1.3.2.js" type="text/javascript"></script>
<script src="./map.js" type="text/javascript"></script>
<script src="./vml.js" type="text/javascript"></script>
</head>
<body>
<div>
<h2>VML test</h2>
<p>
<span id='addCircle' style="position:absolute;width:500;right:420px; top:28px;">
<a href="#">
<span>Draw</span>
</a>
</span>
</p>
<div id='roundme' style='background-color:#fff;width:100%;height:500px;'></div>
</div>
</body>
</html>
-----------------------------------------------------------------
vml.html
-----------------------------------------------------------------
Map = function(){
this.map = new Object();
};
Map.prototype = {
put : function(key, value){
this.map[key] = value;
},
get : function(key){
return this.map[key];
},
containsKey : function(key){
return key in this.map;
},
containsValue : function(value){
for(var prop in this.map){
if(this.map[prop] == value) return true;
}
return false;
},
isEmpty : function(key){
return (this.size() == 0);
},
clear : function(){
for(var prop in this.map){
delete this.map[prop];
}
},
remove : function(key){
delete this.map[key];
},
keys : function(){
var keys = new Array();
for(var prop in this.map){
keys.push(prop);
}
return keys;
},
values : function(){
var values = new Array();
for(var prop in this.map){
values.push(this.map[prop]);
}
return values;
},
size : function(){
var count = 0;
for (var prop in this.map) {
count++;
}
return count;
}
};