*** 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 뚜와띠엔
,