*** Draw에 대해 ***


안녕하세요.


이번에는 지난 번에 이어서 실제로 화면에 shp를 뿌리는 Draw에 대해 알아보도록 합시다.


3. Draw에 대해..


3.1 화면좌표와 지도좌표


SHP가 되었든지, 아니면 DWG/DXF 혹은 DGN, TAB이 되든지 화면에 GIS 자료를 뿌리기 위해서는 MBR(Minimum Bounding Rectangle)이 필수적으로 요구됩니다.


이 MBR을 이용하여 실제 사용자의 화면과의 비례계수를 얻어서 그 비율에 맞게 화면에 GIS 자료를 표시하는 것입니다.


그래서 모든 GIS나 CAD 자료들은 반드시 전체 자료의 MBR과 각 자료의 MBR을 가지고 있습니다.(MBR이 없을 경우는 수동으로 직접 자료를 뒤져가며 MBR을 만들어줘야 합니다.)


사실 GIS 자료를 모니터 상에 표시한다는 것은 일조으이 2차원 변환으로 정의할 수 있습니다.


즉, (X , Y)의 값을 갖는 한 점이 (x , y)의 점으로 이동하도록 하는 것이죠.


이 과정에서 발생하는 변환은 크게 스케일 변환과 원점이동의 두 가지입니다. 그냥 화면에 표시하기 위해서는 회전은 의미가 없겠죠.


그래서 이를 정리하면,

 (X , Y) = (x , y)*Scale + (dx, dy)
 
뭐 이런 식으로 될 것입니다.


간단히 이야기해서 화면 사이즈는 한 1,024 되는데, GIS 자료의 폭이 10,240이면 GIS 자료의 점들을 일단 1/10로 확 줄이고, 그 다음에 GIS 자료의 좌상단점이 화면의 좌상단에 가도록 이동시키는 것입니다.


반대로 화면의 점을 GIS 자료의 점으로 환산하기 위한 방법도 마찬가지입니다.


앞 프로그램의 WorldToDevice가 GIS 자료의 점을 화면의 점으로 이동시키는 함수이구요..


DeviceToWorld가 화면의 점을 GIS 자료의 점의 값으로 이동시키는 함수가 되겠습니다.


CPoint CShape::WorldToDevice(const GeoPoint& _geoPoint, double _lfRatio)

{

CPoint tmpPoint;

tmpPoint.x = long(_lfRatio * (_geoPoint.x - m_geoCenterPoint.x) * m_lfZoomFactor + m_scrCenterPoint.x);

tmpPoint.y = long(_lfRatio * (m_geoCenterPoint.y - _geoPoint.y) * m_lfZoomFactor + m_scrCenterPoint.y);

return tmpPoint;

}


GeoPoint CShape::DeviceToWorld(const CPoint& _point, double _lfRatio)

{

 GeoPoint tmpGeoPoint;

 tmpGeoPoint.x = m_geoCenterPoint.x + (_point.x - m_scrCenterPoint.x) / (m_lfZoomFactor * _lfRatio);

 tmpGeoPoint.y = m_geoCenterPoint.y - (_point.y - m_scrCenterPoint.y) / (m_lfZoomFactor * _lfRatio);

 return tmpGeoPoint;

}


위에서 _lfRatio란 GIS 자료의 MBR과 모니터 화면 사이의 비율을 의미합니다. 그럴 때 m_lfZoomFactor가 1이 되겠지요.


m_lfZoomFactor는 사용자가 확대를 하면, 그 확대비율만큼 곱해지는 것입니다. 그렇게 되면, GIS 자료의 전체 내용 중 확대된 지역만이 화면에 표시될 것입니다.


그런데, GIS자료의 MBR의 폭과 높이의 비율이 항상 모니터 화면의 가로세로의 비율과 일치하라는 법이 없습니다.


이에 따라 양쪽간의 폭의 비율이든지 높이의 비율 중 하나를 선택하여야만 한답니다. 결론은 어느 쪽을 선택하든 GIS 자료가 화면에 다 뿌려지는 형태야겠지요.


그래서...


 GetClientRect(&rect);

 // 종횡비 계산..
 double lfRatio_X = fabs((rect.Width()) / (m_MBR.xmax - m_MBR.xmin));
 double lfRatio_Y = fabs((rect.Height()) / (m_MBR.ymax - m_MBR.ymin));

 // 비율 계산...
 if(lfRatio_X < lfRatio_Y)
  m_lfRatio = lfRatio_X;
 else
  m_lfRatio = lfRatio_Y;
 
위와 같이 처리를 하는 것입니다.


여기에서는 GIS 자료를 화면에 표시하는 핵심이 바로  좌표의 변환에 있음만 이해하시면 됩니다.


위 DeviceToWorld나 WorldToDevice를 보시면 Y축에 대해서는 뒤집혀 있음을 발견하실 수 있을 것입니다.


이는 현재 제가 MM_TEXT라는 Mapping Mode를 사용해서 그리고 있기 때문입니다.


즉, MM_TEXT Mapping Mode에서는 화면 좌상단이 (0,0)이 되어 우측으로 갈 수록 X좌표가 증가하고, 아래쪽으로 갈 수록 Y 좌표가 증가하기 때문입니다.


일반적인 지도좌표계는 좌하단이 (0,0)이 좌표계입니다. 그래서 Y축을 뒤집은 것입니다.


이 부분이 귀찮으면, Mapping Mode를 지도좌표계와 동일하게 하실 수도 있습니다. Mapping Mode에 대해서는 MSDN을 참조하시기 바랍니다. Mapping Mode를 잘 이용하면 매우 유연하고도 편리하게 코딩을 할 수 있습니다.


3.2 Point Draw

 // 포인트의 개수만큼 돌면서 화면에 그린다.
 for(int i = 0; i < m_nRecords; ++i)
 {
  tmpPoint = WorldToDevice(m_pSHPPoints[i], m_lfMainRatio);
  _pDC->Ellipse(tmpPoint.x-2, tmpPoint.y-2, tmpPoint.x+2, tmpPoint.y+2);
 }
 
점의 표시는 위와 같은 루틴에 의해 깔끔하게 됩니다.

즉, GIS 점을 담고 있는 배열에서 하나씩 끄집어 내어 이 놈들을 하나씩 화면좌표계로 바꾼 뒤 그대로 화면에 그리는 것입니다.

쉽죠? 저는 여기에서 원을 그리도록 했습니다. 다른 타입의 SHP도 결국은 레코드만큼 돌면서 하나씩 그리는 것입니다.


3.3 MultiPoint Draw
 
 for(int i = 0; i < m_nRecords; ++i)
 {
  for(int j = 0; j < m_pSHPPolyObjects[i].m_nNumPoints; ++j)
  {
   point = WorldToDevice(m_pSHPPolyObjects[i].m_pPoints[j], m_lfMainRatio);
   _pDC->Ellipse(point.x - 2, point.y - 2, point.x + 2, point.y + 2);
  }
 }
 
다중점(MultiPoint)는 하나의 개체가 여러 점을 가질 수 있는 구조라고 저번에 설명을 드렸습니다.

그래서 하나의 개체에서 점의 개수만큼 회전하면서 지도좌표계의 점을 화면좌표계로 바꾸어 화면에 뿌리는 것입니다.


3.4 Arc Draw

 // 폴리라인의 개수만큼 돌면서 그린다.
 for(int i = 0; i < m_nRecords; ++i)
 {
  // 화면에 그릴 포인트를 할당하고..
  CPoint* pScrPoints = new CPoint[m_pSHPPolyObjects[i].m_nNumPoints];
  for(int j = 0; j < m_pSHPPolyObjects[i].m_nNumPoints; ++j)
   pScrPoints[j] = WorldToDevice(m_pSHPPolyObjects[i].m_pPoints[j], m_lfMainRatio);

  // 파트에 관한 메모리를 할당하고..
  int* pParts = new int[m_pSHPPolyObjects[i].m_nNumParts];
  for(j = 0; j < m_pSHPPolyObjects[i].m_nNumParts; ++j)
  {
   if(j==m_pSHPPolyObjects[i].m_nNumParts-1)
    pParts[j] = m_pSHPPolyObjects[i].m_nNumPoints - m_pSHPPolyObjects[i].m_pParts[j];
   else
    pParts[j] = m_pSHPPolyObjects[i].m_pParts[j+1] - m_pSHPPolyObjects[i].m_pParts[j];
  }
  // 그린다..
  _pDC->PolyPolyline(pScrPoints, (DWORD*)pParts, m_pSHPPolyObjects[i].m_nNumParts);

  delete [] pScrPoints;
  delete [] pParts;
 }
 
아크는 라인의 연속체입니다. 아크를 그리기위해서는 다양한 방법을 사용할 수 있습니다.


DC의 MoveTo, LineTo를 반복적으로 사용하거나, 아니면 Polyline을 반복적으로 사용할 수도 있습니다.


Windows는 PolyPolyline이라는 좋은 함수를 제공하고 있습니다.


앞에서 언급한 함수를 반복적으로 사용하는 것보다 이 함수를 이용하는 것이 더 효율적이라고 피터 페졸드라는 사람이 그러더군요. 그 사람 말을 믿어야죠.. 뭐.. ^^;


PolyPolyline은


BOOL PolyPolyline( const POINT* lpPoints, const DWORD* lpPolyPoints, int nCount );


와 같이 정의되어 있습니다.


여기에 맞춰 주느라 약간 처리를 하는 것입니다.


lpPoints : 화면포인트의 배열
lpPolyPoints : 파트별로 몇개의 점인지를 나타내는 배열
nCount : 파트의 개수


3.5 Polygon Draw

 for(int i=0;i<m_nRecords;++i)
 {
  // 스크린 포인트를 할당하고..
  CPoint* pScrPoints = new CPoint[m_pSHPPolyObjects[i].m_nNumPoints];
  for(int j=0;j<m_pSHPPolyObjects[i].m_nNumPoints;++j)
   pScrPoints[j] = WorldToDevice(m_pSHPPolyObjects[i].m_pPoints[j], m_lfMainRatio);

  // 파트 정보를 정리하고..
  int* pParts = new int[m_pSHPPolyObjects[i].m_nNumParts];
  for(j=0;j<m_pSHPPolyObjects[i].m_nNumParts;++j)
  {
   if(j==m_pSHPPolyObjects[i].m_nNumParts-1)
    pParts[j] = m_pSHPPolyObjects[i].m_nNumPoints - m_pSHPPolyObjects[i].m_pParts[j];
   else
    pParts[j] = m_pSHPPolyObjects[i].m_pParts[j+1] - m_pSHPPolyObjects[i].m_pParts[j];
  }
  // 그린다..
  _pDC->PolyPolygon(pScrPoints, pParts, m_pSHPPolyObjects[i].m_nNumParts);

  delete [] pScrPoints;
  delete [] pParts;
 }


폴리곤의 구조는 사실 아크의 구조와 같다고 보시면 됩니다. 그래서 거의 똑같은 코드로 이루어져 있습니다.


차이는 아크가 PolyPolyline을 사용함에 반해 Polygon은 PolyPolygon이라는 함수를 사용하는 것입니다.


2001년 12월 7일

Posted by 뚜와띠엔
TAG ,

댓글을 달아 주세요