[C++] Stream을 이용한 Typed Binary 데이터 읽고 쓰기

C++은 stream을 통해 반복자(iterator)를 이용해 데이터를  매우 유연하게 처리 할 수 있습니다. 이 스트림을 통해 데이터의 입력을 키보드나 파일 등을 통해 매우 유연하고 효과적으로 읽고 쓸 수 있는데요. 문제는 일반화된 문자열에 대한 읽고 쓰기에 중심을 두고 있다는 점입니다.  즉,  C++의 스트림을 이용해 실수 타입의 값으로 3.14159265라는 값을 파일에 저장하고자 한다면, 이 값을 메모리에 저장된 그대로의 실수형 바이너리 형식으로의 저장이 아닌 “3.14159265”라는 문자열로 저장되는 것이 C++ 개발자에게 흔히 노출된 방식이라는 것입니다.

다 이유가 있겠으나, 필자는 이 실수형 값을 메모리에 저장된 그대로의 실수형 바이너리 형식으로 파일에 저장하고, 이렇게 저장된 값을 다시 파일을 열어 꺼내와 화면에 표시하고자 합니다.

먼저 3.14159265 값을 d 드라이브의 data.bin에 저장하는 코드입니다.

#include <iostream>
#include <fstream>

int main()
{
	std::ofstream os("d:\\data.bin", std::ios::binary);

	double v = 3.141592625;
	os.write(reinterpret_cast<const char*>(&v), sizeof(double));

	os.close();
}

실행해 보면 data.bin 파일이 생성되어졌고, 파일 크기는 double의 바이트 크기와 동일한 8바이트인 것을 확인할 수 있습니다.

이제 이렇게 생성된 파일에서 다시 실수형 값을 읽어 보는 코드는 다음과 같습니다.

#include <iostream>
#include <fstream>

int main()
{
	std::ifstream is("d:\\data.bin", std::ios::binary);

	double v2;
	is.read(reinterpret_cast<char*>(&v2), sizeof(double));
	
	is.close();

	std::cout << v2;
}

여기서 C++의 template을 이용해 개선을 해보도록 하겠습니다. 즉, 다음과 같은 2개의 템플릿 함수를 추가합니다.

template<typename T>
std::ostream& write_typed_data(std::ostream& stream, const T& value) {
	return stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
}

template<typename T>
std::istream & read_typed_data(std::istream& stream, T& value) {
	return stream.read(reinterpret_cast<char*>(&value), sizeof(T));
}

네, write_typed_data와 read_typed_data는 각각 임이의 데이터 타입에 대해서 지정된 스트림에 메모리에 저장된 그대로의 구조인 바이너리 형식으로 쓰고 읽는 범용 함수입니다.

이 두 함수를 활용해 3.141592625 값을 쓰고 읽는 전체 코드를 다시 작성해 보면 다음과 같습니다.

#include <iostream>
#include <fstream>

template<typename T>
std::ostream& write_typed_data(std::ostream& stream, const T& value) {
	return stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
}

template<typename T>
std::istream & read_typed_data(std::istream& stream, T& value) {
	return stream.read(reinterpret_cast<char*>(&value), sizeof(T));
}

int main()
{
	// Write 
	std::ofstream os("d:\\data.bin", std::ios::binary);

	double v = 3.141592625;
	//os.write(reinterpret_cast<const char*>(&v), sizeof(double));
	write_typed_data(os, v);

	os.close();
	// <- test unit end

	// Read
	std::ifstream is("d:\\data.bin", std::ios::binary);

	double v2;
	read_typed_data(is, v2);
	//is.read(reinterpret_cast<char*>(&v2), sizeof(double));
	
	is.close();

	std::cout << v2;
	// <- test unit end
}

C++에서 스트림을 활용해 타입을 가지는 변수를 바이너리 형식으로 저장하는 위의 방식이 정석인지는 모르겠습니다. (이제와서 이게 무슨 소리? -_-;) reinterpret_cast를 사용했다는 점에서 상당히 의구심이 들기 때문인데요. 더욱 나은 방식이 있다면 제안해 주시기 바랍니다. reinterpret_cast는 포인터가 다른 포인터 형식으로 변환될 수 있도록 하거나 정수 계열 형식이 포인터 형식으로 변환될 수 있도록 하고 그 반대로도 변환될 수 있도록 하는 cast 연산자입니다.

[OpenLayers3] 레이어 스파이(Layer Spy)

아래의 지도 위에 마우스를 움직여 보기 바랍니다.


네! 바로 이 기능이 ‘레이어 스파이(Layer Spy)’입니다. 이 기능을 OpenLayer3에서 구현해 보겠습니다.

먼저 필요한 외부 css 및 js를 페이지에 추가합니다.

<script src="https://openlayers.org/en/v3.19.1/build/ol.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>

UI를 구성할 텐데요. 위의 지도 페이지에는 지도에 대한 DIV 요소만이 있습니다. 아래의 코드처럼 말입니다.

이제 필요한 css 및 외부 js 라이브러리와 함께 UI까지 준비가 되었으니, 스크립트 코드를 작성할 차례입니다. jQuery의 이벤트 중 UI 등과 같은 요소의 준비가 완료되면 호출되는 ready 이벤트를 아래처럼 작성해 둡니다.

$(function() {
     ....
});

위의 코드에서 …에 해당하는 부분에 스크립트 코드를 추가할 것입니다.

먼저 지도를 구성하는 레이어로 2개를 준비할텐데요. Bing Map에서 제공하는 타일맵을 통해 레이어를 구성합니다. Bing Map의 지도 서비스를 사용하기 위해서는 key가 필요한데, 만약 아래의 코드에서 제공하는 key로 지도가 표시되지 않는다면 직접 발급을 받아 적용하시기 바라니다. 코드는 아래와 같습니다.

var key = 'AgRcrlx0phSk0kDN6HFX9HkMLbG3dBIqtTm-2no4igx0xJPXeDTffQAQfQP1e-Xv';

var roads = new ol.layer.Tile({
    source: new ol.source.BingMaps({ key: key, imagerySet: 'Road' })
});

var imagery = new ol.layer.Tile({
    source: new ol.source.BingMaps({ key: key, imagerySet: 'Aerial' })
});

이제 이 레이어를 지도에 추가하고 지도를 표시할 DIV 요소에 적용할 차례입니다. 아래의 코드와 같습니다.

var container = document.getElementById('map');

var map = new ol.Map({
    layers: [roads, imagery],
    target: container,
    view: new ol.View({
        center: ol.proj.fromLonLat([-109, 46.5]),
        zoom: 6
    })
});

먼저 1번 코드는 지도를 표시할 DIV에 대한 DOM 요소를 id값을 통해 가져와 container이라는 변수에 할당합니다. 그리고 3번은 지도 객체를 생성하는데요. 생성 시 옵션값으로 layers, target, view를 설정합니다. layers 옵션값은 앞서 생성해 둔 레이어를 추가하고, target은 지도가 표시될 DIV에 대한 DIM 요소가 지정되며, view 옵션에는 처음 지도의 위치와 줌 레벨을 지정합니다. layers 옵션에 대해 추가적으로 설명하면.. roads 레이어를 먼저 추가하고 다음에 imagery 레이어를 추가하고 있다는 점이 중요한데요. 이렇게 하면 roads 레이어 위에 imagery 레이어가 표시되는데, 이때 위체 표시되는 imagery 레이어에 대한 렌더링 컨텍스트에 대해 자르기(Clipping)을 적용하면 레이어 스파이 기능이 완성됩니다.

imagery 레이어에 대한 자르기(Clipping)의 형태는 원 모양입니다. 이 원을 위한 반경값으로 사용할 radius 변수와 키보드를 통해 이 radius 변수를 증가하고 감소하는 키 입력 이벤트를 지정합니다. 코드는 아래와 같습니다.

var radius = 100;

document.addEventListener('keydown', function (evt) {
    if (evt.which === 38) { // Up Key
        radius = Math.min(radius + 5, 200);
        map.render();
        evt.preventDefault();
    } else if (evt.which === 40) { // Down Key
        radius = Math.max(radius - 5, 25);
        map.render();
        evt.preventDefault();
    }
});

위의 코드에서, radius 변수값을 증가시키고 감소시키는 키를 입력할때마다 map 객체에 대한 render함수가 호출되고 있습니다. 이 render 함수는 다음과 같은 실행 흐름을 갖습니다.

먼저 precompose 이벤트가 발생하는데, 이 이벤트는 레이어를 그리기(Drawing) 직전에 발생합니다. 그 다음은 실제로 레이어 그리기(Layer Drawing)가 실행됩니다. 레이어 그리기가 완료되면 postcompose 이벤트가 호출됩니다. 이러한 3 단계는 각 레이어마다 반복됩니다. 또 여기서 중요한점은 이 3 단계가 실행되기에 앞서 지도 데이터를 서버측에 요청하여 받게 되는 것이지 이 3 단계 안에서 서버측으로부터 지도 데이터를 받는 것이 아닙니다. 이 3단계에서 imagery 레이어를 원 모양으로 자르기 위해서 주목해야할 이벤트는 레이어가 그려지기 전인 precompose 이벤트입니다. 아래는 imagery 레이어에(지도가 아닌) precompose 이벤트를 지정하는 코드입니다.

imagery.on('precompose', function (event) {
    var ctx = event.context;
    var pixelRatio = event.frameState.pixelRatio;
    ctx.save();
    ctx.beginPath();
    
    if (mousePosition) {
        ctx.arc(mousePosition[0] * pixelRatio, mousePosition[1] * pixelRatio,
            radius * pixelRatio, 0, 2 * Math.PI);
        ctx.lineWidth = 5 * pixelRatio;
        ctx.strokeStyle = 'rgba(255,255,0,1)';
        ctx.stroke();
    }
    
    ctx.clip();
});

위의 코드를 살펴보면, 2번에서 레이어를 그릴 컨텍스트를 얻어와 ctx 변수에 저장합니다. 그리고 3번에서는 현재 지도에 대한 픽셀 비율값을 가져와 pixelRatio 변수에 저장하고 있습니다. 4번ㅡ15번의 코드가 원 모양으로 자르기(Clipping) 하는 코드입니다. 코드를 부면 mousePosition 변수가 보이는데요. 이 변수는 마우스를 지도 위에서 움직일때 현재 마우스 커서의 좌표를 담게 됩니다. 이를 위해서 마우스 이벤트 지정이 필요한데요. 아래의 코드와 같습니다. 잘라내기(Clipping)의 모양을 지정하기 위해 컨텍스의 arc 함수를 호출하고 있는데요. 중심 좌표 및 반경에 대한 인자값 모두에 pixelRatio 값을 곱해주고 있습니다. 이 pixelRatio는 모니터 화면에 대한 물리 픽셀(physical Pixels)와 장치독립적인 픽셀(device-independent pixels)의 비(ratio) 값입니다.

var mousePosition = null;

container.addEventListener('mousemove', function (event) {
    mousePosition = map.getEventPixel(event);
    map.render();
});

container.addEventListener('mouseout', function () {
    mousePosition = null;
    map.render();
});

또한 postcompose 이벤트의 지정도 필요합니다. 이 이벤트는 레이어가 그려지고 난 뒤에 호출되는 이벤트인데요. 앞서 precompose 이벤트에서 수행한 잘라내기(Clipping)를 무효화해줍니다. 코드는 아래와 같습니다.

imagery.on('postcompose', function (event) {
    var ctx = event.context;
    ctx.restore();
});

이제 실행해보면, 레이어 스파이 기능이 작동되는 것을 확인할 수 있습니다. 아래는 지금까지에 대한 전체 코드입니다.