[Android] 이미지 회전 또는 Mirror(반전)

작디 작은 화면을 가지는 모바일 디바이스.. 요즘은 작다고 하면 한대 맞은 시대이긴 하지만 말입니다. 여튼 모바일 디바이스에서 이미지를 참 많이 가져다 사용합니다. 아래의 코드는 파일명으로 해서 코드단에서 쉽게 사용할 수 있는 Bitmap을 하나 만드는 코드입니다.

BitmapFactory.Options bo = new BitmapFactory.Options();
bo.inSampleSize = 1;
Bitmap bmp = BitmapFactory.decodeFile(imageFilename, bo);

...

bmp.recycle(); 

와우..  참 쉽죠?  사실 1번과 2번 코드는 옵션입니다. 이 코드가 필요없을때는 BitmapFactory의 decodeFile 함수의 두번째 인자를 null로 주면 됩니다. 생성한 비트맵을 잘썼다면 7번 코드를 호출해 줍니다.

이 글의 요지는 이렇게 비트맵을 생성하는게 아니고.. 생성된 비트맵을 회전하고 Mirror시키는 방법이니다. 먼저 회전입니다.

Matrix matrix = new Matrix();
matrix.postRotate(90);
Bitmap newBmp = Bitmap.createBitmap(bmp, 0, 0, 
    bmp.getWidth(), bmp.getHeight(), matrix, true); 

2번 코드에서 postRotate 함수의 첫번째 인자가 바로 회전하고자 하는 각도입니다. 다음은 Mirror 시키는 코드입니다.

float[] mirrorY = {
    -1, 0, 0,
    0, 1, 0,
    0, 0, 1
};

Matrix matrix = new Matrix();
matrix.setValues(mirrorY);
Bitmap newBmp = Bitmap.createBitmap(bmp, 0, 0,
     bmp.getWidth(), bmp.getHeight(), matrix, true); 

회전처럼 행렬을 이동하기는 하는데.. Mirror의 경우 직접 행렬요소를 직접 지정하고 있습니다. 행렬(Matrix)는 개발자에게 매우 유용한 수학적 도구중에 하나가 분명합니다.

안드로이드에서 비트맵에 대한 이야기가 나온 차에 하나 더…! 비트맵을 파일로 저장하는 방법입니다. 안드로이드는 PNG와 JPG 포맷을 지원합니다. 아래의 코드는 비트맵을 PNG 포맷에 대한 파일로 저장하는 코드입니다.

File file = new File(filename);
FileOutputStream filestream = null;
try {
    filestream = new FileOutputStream(file);
    newBmp.compress(CompressFormat.PNG, 0, filestream);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

[Android] 모바일 디바이스에 저장된 전체 이미지 파일 가져오기

안드로이드를 처음 학습할때… 첨으로 별나다.. 싶었던 기억이 이었습니다만.. 지금은 제법 잘만들어진 프레임워크라는 생각이 듭니다. 버전업이 많이 되어져.. 안정화가 되었다는 것이 가장 큰 이유인듯 하지만 말입니다. 아래의 코드는 디바이스에 저장된 이미지 파일(PNG, JPG)들 전체를 얻는 코드입니다.

String[] proj = { MediaStore.Images.Media.DATA };
Cursor imageCursor = managedQuery(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, proj, null, null, null);
   
if (imageCursor != null && imageCursor.moveToFirst()){
    String fileName;
    int dataCol = imageCursor.getColumnIndex(MediaStore.Images.Media.DATA);
    
    do {
        fileName = imageCursor.getString(dataCol);
        if (fileName != null){
            // fileName(이미지 파일명)
        }
    } while (imageCursor.moveToNext());

    imageCursor.close();
}

[Java] 특정 폴더에서 원하는 확장자를 가지는 파일 목록 구하기

원하는 폴더 안에.. 특정한 확장자를 가지는 파일 목록을 얻어야 할때가 있습니다. 예를 들어서 D:/TEMP라는 폴더안에 확장자가 SHP인 파일의 목록을 배열 형태로 반환하도록 하는 경우이지요. 이때 사용할만한 함수입니다.

private Vector getFileNames(String targetDirName, String fileExt) {
    Vector fileNames = new Vector();
    File dir = new File(targetDirName);
    fileExt = fileExt.toLowerCase();
  
    if(dir.isDirectory()) {
        String dirName = dir.getPath();
        String[] filenames = dir.list(null);
        int cntFiles = filenames.length;
       
        for(int iFile=0; iFile            String filename = filenames[iFile];
            String fullFileName = dirName + "/" + filename;
            File file = new File(fullFileName);
 
            boolean isDirectory = file.isDirectory();
            if(!isDirectory && filename.toLowerCase().endsWith(fileExt)) {
                fileNames.add(fullFileName);
            }
        }
    }

    return fileNames;
 }

제가 이 함수가 필요했던 이유는.. 특정 폴더에 존재하는 수백개의 항공영상이나 수백개의 SHP 파일을 한꺼번에 레이어로 추가하고자 하는 필요 때문이였습니다.

아래의 코드는 안드로이드 기반의 GIS 엔진인 블랙포인트에서 위의 함수를 사용해 25cm 해상도의 192개의 항공영상(GEOTIFF 기준으로 40GB 이상)과 일정한 격자로 나눈  SHP 파일 185개(전체 용량 85MB)를 올리는 코드예입니다.

LayerManager layerMan = map.getLayerManager();
  
String ess = Environment.getExternalStorageState();   
String sdCardPath = null;   
if(ess.equals(Environment.MEDIA_MOUNTED)) {   
    sdCardPath = Environment.getExternalStorageDirectory().getAbsolutePath();
    String rootDir = sdCardPath + "/mapdata/yp";
    
    // 항공사진 레이어 추가  
    Vector xrrFiles = getFileNames(rootDir +"/XrR", "xrr");
    for(int i=0; i        ILayer layer = new TileImageLayer("xrr_" + i, xrrFiles.get(i), false);
        layerMan.addLayer(layer);
    }

    // 수치지도 레이어 추가
    Vector cassFiles = getFileNames(rootDir + "/CBND", "shp");
    int cntCbndLyr = cassFiles.size();
    for(int i=0; i        ILayer layer = new ShapeLayer("cbnd_" + i, cassFiles.get(i));
        layerMan.addLayer(layer);
       
        ShapeLayerLabel roadLbl = (ShapeLayerLabel)shpLyr.getLabel();
        roadLbl.setFieldName("JIBUN");
        roadLbl.setEnable(true);
        roadLbl.getFontSymbol().setTextSize(11);
        SimpleDrawShapeTheme roadTheme = (SimpleDrawShapeTheme)shpLyr.getTheme();
        roadTheme.getFillSymbol().setHollow(true);
        roadLbl.getFontSymbol().setTextColor(Color.GREEN);
        roadTheme.getStrokeSymbol().setColor(Color.YELLOW);
}

아래는 위의 코드에 반영된 시스템에 대한 실행 화면입니다. 클릭하면 원본 크기로 볼 수 있습니다.

[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();
        }
    }
);

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