[Android] GPS 기능 관련 API 예제

모바일이 이미 충분히 대중화되었음으로 해서.. GIS 분야 중 하나인 LBS(Location Based System; 위치 기반 시스템)을 활용할 수 다양한 앱이 꽃을 피울 기회를 맞이 한지 이미 꽤 오래되었습니다.

이에 안드로이드가 탑재된 모바일 기기의 GPS를 통해 현재 자신의 위치를 얻을 수 있는 안드로이드 API를 활용하는 샘플 코드를 공유해 봅니다. 아래는 샘플 코드에 대한 스크린 샷입니다.

사용자 삽입 이미지
위치(WGS84 타원체에 대한 경위도)는 물론이고 현재 이동 속도과 위치 정확도(휴대용 GPS의 경우 최고의 정확도 오차는 10m로 제한됨) 등을 얻어와 화면에 표시하고 있습니다. 또한 GPS의 원체 데이터 형식인 NMEA0183을 표시하고 있습니다.

GPS에게 위치데이터 힌트를 제공하는 인공위성의 수 역시 제공하고 있는데요. 이 인공위성의 수는 NMEA0183 데이터로부터 얻어올 수 있습니다. 긴 설명보다는 실제 실행 가능한 예제 코드 샘플을 공유합니다.

인터넷 상에서 공유되고 있는 다양한 소스를 취합하여 이 하나의 예제 샘플을 제작했습니다. 안드로이드에서 GPS로부터 위치 데이터 등을 취득하고자 하시는 개발자 분들에게 조금이라도 도움이 되시길 바랍니다.

GPS로부터 수신받은 좌표(WGS84 타원체의 경위도 좌표)를 다른 좌표계로 변환하기 위한 방법은 다음 URL을 통해 살펴보시기 바랍니다.

[Android] 사용자 정의 이벤트 추가

자바에서 사용자 정의 이벤트를 추가하는 방법에 대한 글(자바에서 사용자정의 이벤트 추가하기)을 남긴 적이 있습니다. 그 글에서 소개한 사용자 정의 이벤트 추가 방식은 안드로이드에서는 추천하지 않는다는 글로 정리를 했습니다.

안드로이드에서는 어떤 식으로 사용자 정의 이벤트를 남기면 좋을까에 대한 것이 이 글의 주제입니다. 일반적인 자바에서 사용자 정의 이벤트는 이벤트 리스트에 여러개의 이벤트를 등록하게 됩니다. 예를 들어서 특정한 뷰에 대한 마우스 클릭에 대한 이벤트라면.. 여러개의 이벤트를 등록하게 하는 방식입니다. 하지만 안드로이드라는 환경은 한정된 리소스를 가지고 있음으로 해서.. 예로 든 특정 뷰에 대한 마우스 클릭에 대한 이벤트에 대해 ‘하나’만을 등록하는 방식이 더 적당하다고 할 수 있습니다. 물론… 필요하다면 자바에서 일반적인 사용자 정의 이벤트를 추가하는 방식을 안드로이드에서 사용해도 됩니다. 그럼.. 안드로이드에서 적합한 사용자 정의 이벤트를 추가하는 방식에 대해 정리해 보겠습니다.

먼저 새롭게 추가할 이벤트가 무엇인지 확인해 봅니다. 예를 들어 맵엔진 개발에 있어서 맵의 축척이 변경되었을때 발생하는 이벤트를 추가하는 예를 통해 살펴보겠습니다. 1개의 클래스와 또 다른 한개의 인터페이스를 추가해야 하는데.. 새롭게 추가할 클래스는 이벤트 객체에 대한 클래스이며 새롭게 추가할 인터페이스는 이벤트 리스너입니다. 이 둘은 일반적인 자바에서의 사용자 정의 이벤트에 대해서 동일한 내용입니다. 다른게 없습니다.

아래는 축척 변경 이벤트에 대한 이벤트 객체 클래스 소스입니다.

package geoservice.blackpoint.events;

import geoservice.blackpoint.XrMap;
import java.util.EventObject;

public class MapEvent extends EventObject {
    public MapEvent(Object source) {   
        super(source);   
    }   
 
    public XrMap getMap() {
        XrMap map = (XrMap)getSource(); 
        return map;
    }
}

이벤트 객체는 이벤트를 발생시킨 주체(getSource 매서드를 통해 반환됨)를 기본적인 내용으로 하며 그외 더 필요한 정보를 담을 수 있습니다. 다음으로 이벤트 리스너에 대한 인터페이스입니다.

package geoservice.blackpoint.events;

import java.util.EventListener;

public interface OnMapScaleChangedEventListener extends EventListener {
    void onMapScaleChanged(MapEvent event);
}

이벤트 리스너 인터페이스에는 여러개의 이벤트를 넣을 수 있는데.. 이 경우 맵축척 변경에 대한 이벤트만이 있습니다. 이제 축척 변경에 대한 이벤트에 필요한 클래스와 인터페이스가 준비되었습니다.

그럼 맵엔진 단에서 맵 축척이 변경되었을때 이벤트를 발생시키는 것에 대해 코드를 통해 살펴보겠습니다.

class XrMap {
    // 이벤트 코드에만 집중하자

    public OnTapUpEventListener onTapUpEventListener = null;    
    public void setOnTapUpListener(OnTapUpEventListener listener) {
        onTapUpEventListener = listener;
    }

    private void __맵축척을변경시키는부분__ {
        if(onMapScaleChangedEventListener != null) {
            MapEvent event = new MapEvent(this);
            onMapScaleChangedEventListener.onMapScaleChanged(event);
        }
    }

    // 이벤트 코드에만 집중하자
}

보시는 것처럼.. 이벤트 리스너에 대한 변수와 이 이벤트 리스너를 할당시키기 위한 매서드 끝으로 실제 이벤트가 발생될때 이벤트 리스너를 실행해 주는 코드로 구성되어 있습니다. 여기까지가 일반적으로 안드로이드에서 사용자 정의 이벤트를 만들어 주는 방식입니다. 그럼.. 이 맵축척이 변경되었을때 발생하는 이벤트에서 원하는 코드를 작성해보는 샘플 코드는 어떻게 될까.. 아래 코드를 통해 살펴보시기 바랍니다.

map.setOnMapScaleChangedListener(
    new OnMapScaleChangedEventListener() {
        public void onMapScaleChanged(MapEvent event) {
            Toast.makeText(
                XrMapTest.this, 
                "SCALE CHANGED", 
                Toast.LENGTH_SHORT).show();
       }
    }
);

map은 앞서 XrMap 클래스에 대한 변수입니다. 축척이 변경될때마다 화면에 SCALE CHANGED라는 메세지를 토스트를 통해 띠웁니다.. 끝으로 안드로이드에서 버튼을 클릭했을 때 이벤트를 할당하는 코드를 살펴보고.. 위의 맵축척 변경 이벤트 할당 코드와 비교해 보시기 바랍니다.

Button btnro= (Button)findViewById(R.id.ro);
btnro.setOnClickListener(
    new Button.OnClickListener() {
        public void onClick(View v) {
            CoordMapper cm = map.getRendererManager().getCoordMapper();
            cm.rotate(15);
            map.update();
        }
    }
);

보시는 것처럼.. 이벤트를 할당하는 두개의 구조가 모두 동일함을 알 수 있습니다. 이상으로 안드로이드에서 사용자 정의 이벤트를 할당하는 방법에 대한 정리를 마치도록 하겠습니다.

[Android] 3D API, OpenGL ES – 2 : 폴리곤 렌더링

이전 글을 통해 안드로이드에서 OpenGL ES에 대한 초기화에 대해 살펴보았습니다. 이 글은 간단한 폴리곤을 화면상에 렌더링해 보는 API에 대해 살펴보겠습니다. 간단한 폴리곤에 대한 렌더링을 위해 먼저 폴리곤을 구성하는 데이터에 대한 개념을 살펴보도록 하겠습니다. (OpenGL ES를 보시기 전에 먼저 OpenGL을 선행 학습하시면 훨씬 쉽게 이글을 이해할 수 있습니다.)

정점(Vertex) : 정점은 3D 모델을 구성하는 최소 단위입니다. 정점은 2개 이상의 모서리(Edge)가 만나는 점입니다. 3D 모델에서 정점은 모든 연결된 모서리나 면(Face) 그리고 폴리곤 사이에 공유됩니다. 또한 정점은 카메라나 광원의 위치를 나타내는데도 사용됩니다.

모서리(Edge) : 모서리는 두개의 정점을 잇는 선분입니다. 모서리는 면이나 폴리곤의 외곽선입니다. 3D 모델에서 모서리는 2개의 인접한 면이나 폴리곤 사이에 공유됩니다. OpenGL ES에서는 선분을 정의한다라는 개념보다는 정점을 이용해 면을 정의한다고 합니다. 면은 최소 3개의 모서리로 구성됩니다.

면(Face) : 면은 삼각형입니다. 면은 3개의 정점으로 구성되며 3개의 모서리로 둘러 싸여졌습니다. 면의 구성을 변환하면 모든 연결된 정점과 모서리 그리고 폴리곤에 영향을 받습니다. 면을 구성하는 정점의 지정 순서에 따라 면의 앞면과 뒷면에 대한 정의가 달라집니다. 앞면과 뒷면이 중요한 이유는 퍼포먼스에 있습니다. 뒷면은 눈에 들어나지 않으므로 렌더링에서 제외될 수 있기 때문입니다. 정점의 지정 순서에 대해 앞면이냐 뒷면이냐를 정의할 수 있는데 glFrontFace 매서드를 통해 가능합니다. 정점의 지정순서는 삼각형에 대해 반시계 방향 순서(CCW)냐 시계 방향 순서(CW)냐입니다. OpenGL ES는 기본적으로 반시계 방향이 앞면을 의미합니다.

폴리곤(Polygon) : 폴리곤은 하나의 3D 모델이라고 생각하면 됩니다. 폴리곤을 구성하기 위해서는 정점들의 데이터와 이 정점들 중 3개를 이용해 면을 구성하기 위한 정점 인덱스 정보를 통해 이뤄집니다. 폴리곤을 화면에 렌더링하는 매서드는 glDrawArrays와 glDrawElements입니다. 이 두 함수의 첫번째 인자는 정점과 인덱스를 통해 구성할 모델의 형태입니다. 다음과 같이 총 7가지 모드가 존재합니다.

1. GL_POINTS : 정점을 개별적인 포인트로 렌더링한다.

사용자 삽입 이미지
2. GL_LINE_STRIP : 정점을 지정된 순서대로 이어 라인을 구성해 렌더링한다.사용자 삽입 이미지
3. GL_LINE_LOOP : GL_LINE_STRIP와 동일하고 첫번째와 마지막 정점을 잇는다.사용자 삽입 이미지
4. GL_LINES : 정점 2개씩 연속적으로 묶어 개별적인 선분 하나씩 구성해 렌더링한다.사용자 삽입 이미지
5. GL_TRIANGLES : 정점 3개씩 연속적으로 묶어 개별적인 삼각형을 구성해 렌더링한다.사용자 삽입 이미지
6. GL_TRIANGLE_STRIP : 처음은 제공되는 3개의 정점으로 삼각형을 구성하고 다음 정점 하나를 통해 또 다른 삼각형을 구성한다. 구성되는 삼각형들은 모두 동일한 면 방향을 갖도록 구성하게 된다.사용자 삽입 이미지
7. GL_TRIANGLE_FAN : GL_TRIANGLES_STRIP와 비슷하지만 처음 제공되는 정점을 중심으로 팬(Fan) 형태로 삼각형을 구성해 렌더링한다.사용자 삽입 이미지
이제 이러한 기본 지식을 토대로 OpenGL ES를 통해 안드로이드에서 간단한 사각형 폴리곤 모델을 렌더링 해보는 코드를 작성해 보겠습니다. 기반이 되는 코드는 이전 글의 초기화에서 작성했던 프로젝트를 기반으로 합니다. 변경될 부분은 MyRenderer 클래스와 Square라는 새로운 클래스 추가입니다. 먼저 Square이라는 새로운 클래스를 추가합니다.

public class Square { }

이 클래스는 앞에서 언급했던 사각형 폴리곤 모델에 대한 정점과 정점 인덱스 정보를 담고 있으며 렌더링하는 코드가 존재합니다. 구성할 사각형 폴리곤의 정점 좌표와 인덱스 정보는 다음과 같습니다.

사용자 삽입 이미지
즉.. 총 4개의 정점으로 존재하며 총 2개의 삼각형 면으로 구성되어 있습니다. 2개의 삼각형면을 구성하기 위해 사용할 정점에 대한 인덱스가 필요할텐데.. 이에 대한 정보를 클래스에 코드로 추가합니다.

public class Square {
    private float vertices[] = {
        -1.0f,  1.0f, 0.0f,
        -1.0f, -1.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        1.0f,  1.0f, 0.0f,
    };
 
    private short[] indices = { 0, 1, 2, 0, 2, 3 };
}

vertices가 정점에 대한 정보를 담고 있으며 indices가 삼각형 면을 구성할 정점에 대한 인덱스 정보를 담고 있습니다. 이제 Square의 생성자에서 이 정보를 OpenGL ES에서 사용할 수 있도록 Buffer로 저장하는 코드를 작성합니다.

 private FloatBuffer vertexBuffer;
 private ShortBuffer indexBuffer;

public Square() {
    ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
    vbb.order(ByteOrder.nativeOrder());
    vertexBuffer = vbb.asFloatBuffer();
    vertexBuffer.put(vertices);
    vertexBuffer.position(0);

    ByteBuffer ibb = ByteBuffer.allocateDirect(indices.length * 2);
    ibb.order(ByteOrder.nativeOrder());
    indexBuffer = ibb.asShortBuffer();
    indexBuffer.put(indices);
    indexBuffer.position(0);
}

자바의 NIO를 이용해 배열을 ByteBuffer에 담아 정점에 대한 버퍼는 FloatBuffer 타입의 vertexBuffer로 정점 인덱스에 대한 버퍼는 ShortBuffer 타입의 indexBuffer에 담아 놓습니다. 이제 3D 모델에 대한 정의는 끝났고.. 이 모델을 렌더링해주는 코드를 Square에 추가해 보면 다음과 같습니다.

public void draw(GL10 gl) {
    gl.glFrontFace(GL10.GL_CCW);
    gl.glEnable(GL10.GL_CULL_FACE);
    gl.glCullFace(GL10.GL_BACK);
  
    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, 
        GL10.GL_UNSIGNED_SHORT, indexBuffer);
    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisable(GL10.GL_CULL_FACE);
 }

먼저 2번 코드는 앞면은 정점 지정 순서가 반시계 방향으로 지정한 것이고 3번 코드와  4번 코드를 통해 뒷면은 렌더링되지 않도록 합니다. 6번 코드는 정점 배열을 통해 모델을 렌더링하겠다고 지정한 코드이며 7번 코드는 모델의 정점 배열을 지정해 주는 코드입니다. 8번은 실제로 렌더링 시키라는 코드로 이 함수에 정점 인덱스 배열이 인자로 들어갑니다. 9번 코드는 정점 배열과 정점 인덱스 배열의 지정이 끝났음을 알립니다. 이제 이렇게 만들어진 Square 클래스를 사용하는 일이 남았습니다.

MyRenderer 클래스에 Square 클래스에 대한 필드 변수를 추가하고 onSurfaceCreated 매서드에서 생성합니다.

public class MyRenderer implements Renderer {
    private Square square;

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        square = new Square();

        gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        gl.glShadeModel(GL10.GL_SMOOTH);
        gl.glClearDepthf(1.0f);
        gl.glEnable(GL10.GL_DEPTH_TEST);
        gl.glDepthFunc(GL10.GL_LEQUAL);
        gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
    }    
}

onSurfaceCreated 매서드를 보면 Square 변수를 생성하는 코드 이외에도 OpenGL ES의 다양한 값들을 지정해 주는 코드가 존재합니다. 다음으로 MyRenderer 클래스의 onDrawFrame 매서드에서 square를 그리는 코드를 작성합니다.

@Override
public void onDrawFrame(GL10 gl) {
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();
    gl.glTranslatef(0, 0, -10);
    square.draw(gl);
}

화면상에 사각형 모델이 보이도록 5번 코드에서 Z축으로 -10만큼 이동하였습니다. 이에 앞서 투영 행렬과 모델뷰 행렬을 지정해 줘야 하는데.. 이 코드는 MyRenderer의 onSurfaceChanged 매서드에서 실행됩니다.

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    gl.glViewport(0, 0, width, height);
  
    gl.glMatrixMode(GL10.GL_PROJECTION);
    gl.glLoadIdentity();
  
    GLU.gluPerspective(gl, 45.0f, (float)width/(float)height, 0.1f, 100.0f);
    gl.glMatrixMode(GL10.GL_MODELVIEW);
}

이제 실행해 보면 다음과 같은 결과를 얻을 수 있습니다.

사용자 삽입 이미지

[Android] 텍스트에 외곽선 효과주기

안드로이는 2D Drawing API가 상당히 뛰어납니다. 일반 PC에서 제공하는 API 수준.. 그 이상인데요. 안드로이에서 제공하는 2D Drawing API 중에서 텍스트에 외곽선 효과를 주는 코드에 대해 간단히 소개해 드리겠습니다. 결과를 먼저 보여드리면 다음과 같습니다.

사용자 삽입 이미지
위의 시스템은 지오서비스에서 현재 개발중인 지적도 현장 검색 시스템으로.. 항공사진과 지적도를 표현하고 있습니다. 지적도의 특성상 라벨로.. 지번을 텍스트로 표현해야 합니다. 텍스트를 그냥 표현하게 되면 지도와 텍스트가 섞여 라벨이 눈에 잘 들어오지 않게 됩니다. 해서.. 라벨에 외곽선을 그려주게 되면 라벨의 가독성이 향상됩니다. 위의 그림에서처럼 글자는 하얀색으로 하고 외곽선은 검정색으로 하는 코드는 다음과 같습니다.

textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(textSize);
  
strokePaint = new Paint();
strokePaint.setAntiAlias(true);       
strokePaint.setTextSize(textSize);       
strokePaint.setColor(Color.BLACK);       
strokePaint.setStyle(Paint.Style.STROKE);       
strokePaint.setStrokeWidth(4);

위의 코드에서 보시는 것처럼.. 하얀색의 글자와 외곽선의 검정색에 대한 Paint 객체를 2개 만듭니다. 즉 글자 자체의 하얀색에 대한 textPaint와 검정색 외곽선에 대한 strokePaint 변수가 바로 그 것입니다. 이렇게 만들어진 Paint 객체를 다음의 텍스트 그리기 함수에서 사용합니다.

String text = "label";

canvas.drawText(text, x, y, strokePaint);
canvas.drawText(text, x, y, textPaint);

텍스트 그리기 한번 그리고 외곽선 그리기 한번.. 이렇게 총 2번을 그려서 텍스트의 외곽선을 표현해 줄 수 있습니다. 외곽선의 굵기를 원하는 만큼 지정할 수 있는데요. 방법은 strokePaint의 setStrokeWidth 매서드에 원하는 굵기만큼 값을 주면 쉽게 원하는 바를 얻을 수 있습니다.