[OpenLayers] 특정 지점에 HotSpot 애니메이션 효과 주기

지도에 특정한 지점을 효과적으로 표현하기 애니메이션 효과를 사용하는 내용을 정리합니다. 먼저 그 결과는 아래와 같습니다.

화면은 지도에 대한 div 요소 하나입니다. 필요한 스타일과 함께 언급하면 아래와 같습니다.



이제 실제 코드인데요. 먼저 필요한 css와 모듈을 추가합니다.

import 'ol/ol.css';

import Feature from 'ol/Feature.js';
import Map from 'ol/Map.js';
import {unByKey} from 'ol/Observable.js';
import View from 'ol/View.js';
import {easeOut} from 'ol/easing.js';
import Point from 'ol/geom/Point.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {fromLonLat} from 'ol/proj.js';
import {OSM, Vector as VectorSource} from 'ol/source.js';
import {Circle as CircleStyle, Style, Fill} from 'ol/style.js';

이제 레이어와 지도를 구성합니다.

var map = new Map({
    layers: [
        new TileLayer({
            source: new OSM({
                wrapX: false
            })
        })
    ],
    target: 'map',
    view: new View({
        center: [0, 0],
        zoom: 2
    })
});

var source = new VectorSource({
    wrapX: false
});

var vector = new VectorLayer({
    source: source
});

map.addLayer(vector);

HotSpot 지점은 위의 20번 코드의 vector 객체의 데이터소스인 source 객체에 Feature로 추가됩니다. 위치는 난수로 지정되는데 아래의 함수 호출을 통해 추가됩니다.

function addRandomFeature() {
    var x = Math.random() * 360 - 180;
    var y = Math.random() * 180 - 90;
    var coord = fromLonLat([x, y]);
    var geom = new Point(coord);
    var feature = new Feature(geom);

    source.addFeature(feature);
}

위치를 난수로 정하고 Geometry를 구성한 뒤 Feature를 추가하는 형태입니다. Feature가 추가될 때 이벤트가 발생하게 할 수 있는데 아래와 같습니다.

source.on('addfeature', function(e) {
    flash(e.feature);
});

이벤트의 호출 함수에 전달되는 인자를 통해 어떤 Feature가 추가되었는지를 알 수 있고, 이 Feature을 인자로 flash 함수를 호출합니다. flash 함수는 다음과 같습니다.

var duration = 2000;
function flash(feature) {
    var start = new Date().getTime();
    var listenerKey = map.on('postcompose', animate);

    function animate(event) {
        var vectorContext = event.vectorContext;
        var frameState = event.frameState;
        var flashGeom = feature.getGeometry().clone();
        var elapsed = frameState.time - start;
        var elapsedRatio = elapsed / duration;

        var radius = easeOut(elapsedRatio) * 30;
        var opacity = easeOut(1 - elapsedRatio);

        var style = new Style({
            image: new CircleStyle({
                radius: radius,
                fill: new Fill({
                    color: 'rgba(255, 0, 0, ' + 0.5 * opacity + ')',        
                })
            })
        });

        vectorContext.setStyle(style);
        vectorContext.drawGeometry(flashGeom);

        if (elapsed > duration) {
            unByKey(listenerKey);
        } else {
            map.render();
        }
    }
}

짧다고 하기엔 조금 긴데요. 내부적으로 복잡한 처리가 실행됩니다. 이 flash 함수가 처음 호출되면 결국 31번의 map.render()가 호출됩니다. 이 map.render()가 호출되면 지도가 (데이터 요청없이) 다시 그려지고4번 코드에서 등록된 postcompose 이벤트 함수인 animate가 호출됩니다. animate 함수는 함수 내의 함수인데 6번 라인에 정의되어 있습니다. 해당 Feature의 위치를 기준으로 시간을 기반 투명도와 반지름의 크기를 변경하면서 빨간색 채움색으로 원을 그려주는 함수입니다. 이 flash 함수는 계속 자신의 호출하는 재귀함수로써 1번의 duration으로 지정된 시간이 지나면 28번의 조건이 True가 되어 앞서 등록한 postcompose 이벤트 등록을 제거합니다.

이제 마지막으로 1초마다 무작위로 Feature를 추가하도록 하는 코드는 아래와 같습니다.

window.setInterval(addRandomFeature, 1000);

[OpenLayers] 지도를 이미지로 저장하기

ol의 지도로 표현된 내용을 이미지 파일로 저장하는 내용에 대한 정리입니다. 먼저 지도를 위한 div와 이미지 저장을 위한 버튼에 대한 tag를 아래처럼 index.html에 작성합니다.

지도를 구성하는 코드는 아래와 같고..

import 'ol/ol.css';

import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer.js';
import {OSM, Vector as VectorSource} from 'ol/source.js';

var map = new Map({
    layers: [
        new TileLayer({
        source: new OSM()
        })
    ],
    target: 'map',
    view: new View({
        center: [0, 0],
        zoom: 2
    })
});

이미지 저장을 위한 버튼을 클릭할 경우와 관련된 코드는 아래와 같습니다.

document.querySelector('#saveAsImage').addEventListener('click', function() {
    map.once('rendercomplete', function(event) {
        var canvas = event.context.canvas;
        if (navigator.msSaveBlob) {
            navigator.msSaveBlob(canvas.msToBlob(), 'map.png');
        } else {
            canvas.toBlob(function(blob) {
                saveAs(blob, 'map.png');
            });
        }
    });
    map.renderSync();
});

saveAs라는 함수가 보이는데, 이 함수를 사용하기 위해서는 다음과 같은 js 라이브러리를 페이지에 포함해야 합니다.


Python과 OpenCV – 2 : 비디오를 가지고 시작하기

원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_gui/py_video_display/py_video_display.html 입니다. OpenCV에는 카메라를 통해서도 이미지를 받을 수 있습니다. 아래의 코드는 컴퓨터에 연결된 카메라를 통해 영상을 받아 표시하는 코드입니다.

import numpy as np
import cv2

cap = cv2.VideoCapture(0)

print(cap.get(3), cap.get(4))

ret = cap.set(3,320)
ret = cap.set(4,240)

while(True):
    ret, frame = cap.read() 

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    cv2.imshow('frame', gray)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

4번 코드에서 첫번째(0) 카메라를 VideoCapture 타입의 객체로 얻어옵니다. 6번은 이 카메라의 영상에 대한 가로, 세로에 대한 픽셀 길이를 출력합니다. 필자의 경우 640 480이 출력되었습니다. 8,9번 코드를 통해 가로와 세로의 크기를 각각 320×240으로 변경합니다. q 키를 누를때까지 11번의 반복문이 무한 반복됩니다. 12번에서 카메라로부터 현재의 영상 하나를 읽어오고, 14번에서 이 영상을 Gray로 변환합니다. 16번에서 이 Gray 영상을 Window에 표시합니다. q 키를 눌러 Window를 닫게 되면 프로그램이 종료되는데, 이때 사용한 카메라에 대한 리소스 해제에 대한 코드가 20번입니다.

카메라로부터 영상을 표시할 수도 있지만, 이를 파일로 기록할 수도 있습니다. 이래의 코드처럼요.

import numpy as np
import cv2

cap = cv2.VideoCapture(0)

fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter('d:/output.mp4', fourcc, 20.0, (640,480))

while(cap.isOpened()):
    ret, frame = cap.read()
    if ret == True:
        frame = cv2.flip(frame, 0)
        out.write(frame)

        cv2.imshow('frame', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    else:
        break

cap.release()
out.release()
cv2.destroyAllWindows()

7번의 코드에서 기록할 동영상 파일명과 프레임률(20), 영상의 크기(640, 480)을 지정합니다. 파일명의 확장자는 mp4나 avi 등이 가능한데, 확장자에 따라 만들어지는 영상의 포멧이 결정됩니다. 실제 영상의 저장은 13번 코드입니다. 테스트로 12번 코드에서 영상을 뒤집어 기록하도록 하였습니다. q 키를 눌러 영상을 기록하는 반복문이 종료되면 비디오 객체를 해제해야 하는데 해당 코드가 바로 21, 22번입니다.

카메라가 아닌 동영상 파일로부터도 영상을 얻어 표시할 수 있는데, 해당 코드는 아래와 같습니다.

import numpy as np
import cv2

cap = cv2.VideoCapture('./data/vtest.avi')

while(cap.isOpened()):
    ret, frame = cap.read()

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    cv2.imshow('frame',gray)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

카메라를 통한 영상 표시에 대한 코드와 모두 동일한데, 차이점은 단지 4번의 VideoCapture 함수의 인자가 카메라 인덱스가 아닌 파일명이라는 점입니다.

mustache.js를 이용한 HTML Tag에 Data를 Binding

이 글은 Include HTML이라는 글을 기초로 작성된 글입니다. Include HTML은 동적으로 외부의 HTML 리소스를 원하는 위치에 Include 해주는 방법을 설명합니다. 이제 이렇게 Include HTML을 통해 HTML 리소스를 원하는 위치에 포함할 때, 원하는 데이터를 Binding 해주는 방법을 설명합니다. 이를 위해 mustache.js 라이브러리를 이용합니다. mustache.js 라이브러리는 많은 프로그래밍 언어를 지원하는데, 여기서는 JS를 이용하였으며 관련 링크는 https://github.com/janl/mustache.js 입니다.

먼저 메인 페이지는 다음과 같습니다.

위의 id가 containerA인 DOM 요소에 A.html 리소스를 포함하고자 합니다. A.html의 내용은 다음과 같습니다.

조회결과

{{name}}
{{age}}
{{vision}}

위의 {{ .. }}로 묶인 3개의 항목이 보입니다. 이 {{ .. }} 위치에 원하는 데이터를 바인딩하는 것인데요. 바인딩하고자 하는 데이터는 아래와 같습니다.

let data = {
    name: "홍길동",
    age: function () {
        let birthYear = 1576;
        return new Date().getFullYear() - birthYear;
    },
    vision: "모든 세상 사람이 배 부르게 사는 그날까지.."
};

name, age, vision이 {{ .. }}에 지정된 단어와 동일합니다. 이렇게 각 데이터가 1:1로 매칭됩니다. 위의 data는 DBMS 조회를 통해 네트워크를 통해 가져올 수도 있겠죠. 이제 아래의 코드를 통해 외부의 HTML 리소스를 불러오면서 위의 데이터를 바인딩해 화면에 표시해 줍니다.

includeHTML(document.querySelector('#containerA'), 'A.html', data);

실행해보면 결과는 다음과 같습니다.

앞서 작성한 includeHTML 함수는 다음과 같습니다.

function includeHTML(divContainer, urlHTML, data) {
    let xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function () {
        if (this.readyState == 4) {
            if (this.status == 200) {
                var result = Mustache.render(this.responseText, data);
                divContainer.innerHTML = result;
            } else if (this.status == 404) { divContainer.innerHTML = "Page not found."; }
        }
    }

    xhttp.open("GET", urlHTML, true);
    xhttp.send();
}

위의 코드 중 Mustache 코드가 보이는데, 이 라이브러리를 사용하기 위해서는 아래의 코드가 필요합니다.