Proj4js

이 글은 proj4js.org 사이트에서 제공되는 내용을 파악하기 위해 정리한 포스트이며, 좀 더 상세한 내용을 추가적으로 담고자 노력하였습니다.

Proj4js는 좌표계 간의 상호 변환하기 위한 자바스크립트 라이브러리이며 서로 다른 타원체 간의 Datum 변환 기능을 포함하고 있습니다. 이 라이브러리는 원래 C언어로 개발된 PROJ.4와 MetaCRS 그룹의 프로젝트 중의 하나인 GCTCP C를 JavaScript로 포팅한 것입니다.

설치

개발자의 개발환경에 따라 다르지만, 아래의 4가지 방식 중 한가지 방식으로 설치가 가능합니다.

npm install proj4
bower install proj4
jam install proj4
component install proj4js/proj4js

또는 최신 배포의 dist/ 폴더에서 proj4.js 파일을 직접 사용할 수 있습니다. 아무것도 다운로드 받고 싶지 않다면 CDN을 통한 URL로 라이브러리를 사용해도 됩니다.

사용 방법

기본적인 사용 구문은 다음과 같습니다.

proj4(fromProjection[, toProjection, coordinates])

인자 fromProjection와 toProjection는 proj이거나 WTK 문자열일 수 있습니다. coordinates 인자는 {x:x,y:y}와 같은 객체 형태이거나 [x,y]와 같은 배열 형태일 수 있습니다.

fromProjection, toProjection, coordinates 인자가 모두 지정되면, 지정된 좌표가 fromProjection 좌표 체계에서 toProjection 좌표 체계로 변환된 결과를 반환합니다. 변환된 결과는 지정된 좌표 인자의 형태와 같습니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var secondProjection = "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

var result = proj4(firstProjection, secondProjection, [2, 5]);
// [-2690666.2977344505, 3662659.885459918]

만약 1개의 projection 만을 사용한다면 해당 인자는 fromProjection을 의미하며, firstProjection은 WGS84 경위도 좌표계가 됩니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var result = proj4(firstProjection, [-71, 41]);
// [242075.00535055372, 750123.32090043]

또한 coordinates 인자 없이 projection 인자만을 사용한다면, forward와 inverse 매서드를 갖는 객체가 반환되는데, forward는 fromProjection 좌표계에서 toProjection로의 좌표 변환을, inverse는 toProjection 좌표계에서 fromProjection로의 좌표 변환을 수행하는 매서드입니다.

var firstProjection = 'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]';

var secondProjection = "+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs";

var resultA = proj4(firstProjection, secondProjection).forward([2, 5]);
// [-2690666.2977344505, 3662659.885459918]

var resultB = proj4(secondProjection,firstProjection).inverse([2,5]);
// [-2690666.2977344505, 3662659.885459918]

projection 인자가 하나만 지정되면, 지정된 인자는 toProjection에 해당되며 fromProjection은 WGS84 경위도 좌표계가 됩니다.

이름을 가지는 투영변환

만약 문자열로 투영변환에 이름을 부여하고, 이 이름으로 좌표 변환을 수행하고자 한다면 proj4.defs 매서드를 사용할 수 있습니다.

proj4.defs('WGS84', "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees");

또는 아래처럼 배열로 여러개의 투영변환을 정의할 수 있습니다.

proj4.defs([
  [
    'EPSG:4326',
    'PROJCS["NAD83 / Massachusetts Mainland",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",42.68333333333333],PARAMETER["standard_parallel_2",41.71666666666667],PARAMETER["latitude_of_origin",41],PARAMETER["central_meridian",-71.5],PARAMETER["false_easting",200000],PARAMETER["false_northing",750000],AUTHORITY["EPSG","26986"],AXIS["X",EAST],AXIS["Y",NORTH]]]'
  ],
  [
    'EPSG:4269',
    '+proj=gnom +lat_0=90 +lon_0=0 +x_0=6300000 +y_0=6300000 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'
  ]
]);

그러면, 언제라도 다음처럼 사용할 수 있습니다.

var p = proj4('EPSG:4326', 'EPSG:4269');
var result = p.forward([-71, 41]);
// [-2690599.9886444192, 3662814.7663661353]

사실 위 코드의 첫번째 라인은 다음의 축약형입니다.

var p = proj4(proj4.defs('EPSG:4326'), proj4.defs('EPSG:4269'));

미리 정의된 이름을 가지는 투영변환은 EPSG:4326, EPSG:4269, EPSG:3857입니다. 아울러 EPSG:4326은 WGS84라는 이름으로도 정의되어 있으며, EPSG:3857은 EPSG:3758, GOOGLE, EPSG:900913, EPSG:102113이라는 다양한 이름으로도 정의되어 있습니다. 이에 대한 proj4js 라이브러리의 코드를 살펴보면 다음과 같습니다.

defs('EPSG:4326', "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees");
defs('EPSG:4269', "+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees");
defs('EPSG:3857', "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs");

defs.WGS84 = defs['EPSG:4326'];
defs['EPSG:3785'] = defs['EPSG:3857']; // maintain backward compat, official code is 3857
defs.GOOGLE = defs['EPSG:3857'];
defs['EPSG:900913'] = defs['EPSG:3857'];
defs['EPSG:102113'] = defs['EPSG:3857'];

아래는 한국의 좌표계를 예로 들어 proj4js의 설명을 담은 글입니다. 2013년도 글이라 현재 버전의 API와 맞지 않을 수 있습니다.

[GIS] 오픈소스, 자바스크립트 좌표계 변환 라이브러리, proj4js

끝으로 EPSG 코드를 통한 proj 및 wkt를 얻을 수 있는 사이트에 대한 글은 아래와 같습니다.

EPSG.io를 통한 proj4 문자열 얻기

[OpenLayers] 지오메트리(Geometry)에 대한 공간 연산(JSTS.js 사용)

ol에서 지오메트리에 대해 buffer나 union 등과 같은 공간 연산 기능에 대한 API를 정리합니다. 공간 연산 기능은 JSTS.js라는 별도의 라이브러리를 통해 수행합니다. JSTS.js는 Java의 JTS 라이브러리를 Javascript 언어로 포팅한 라이브러리입니다. 이글은 공간 연산을 위한 지오메트리를 생성하기 위해 “마우스로 Vector 생성하기(그리기)”라는 글의 코드를 약간 변형하여 사용합니다. ol에서 마우스로 도형을 그리는 API의 설명은 이 글을 참고 하기 바랍니다.

먼저 실행 결과를 아래의 동영상을 통해 살펴볼 수 있습니다.

사용자가 Buffer 연산을 적용할 지오메트리를 그리면 바로 200m 만큼 Buffer 연산이 적용되어 화면에 표시됩니다. 이를 위해 먼저 index.html 파일을 아래처럼 구성합니다.



    
        
        OpenLayers
        

        
    
    
        

20번 라인에 jsts.js 라이브러리 스크립트를 추가하고 있습니다. index.js 파일에 대해 설명하면 다음과 같습니다.

먼저 필요한 모듈을 추가합니다.

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

그리고 레이어와 지도를 구성합니다.

var raster = new TileLayer({
    source: new OSM()
});

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

var map = new Map({
    layers: [raster, vector],
    target: 'map',
    view: new View({
        center: [-11000000, 4600000],
        zoom: 15
    })
});

다음으로 그리고자 하는 도형의 종류를 선택하는 UI의 이벤트 처리와 도형을 그리는 Draw 클래스 타입을 위한 draw 변수를 정의합니다.

var typeSelect = document.getElementById('type');
 
typeSelect.onchange = function() {
    map.removeInteraction(draw);
    addInteraction();
};
 
addInteraction();

var draw;

지금까지는 “마우스로 Vector 생성하기(그리기)”의 내용과 큰 차이가 없습니다. 이제 변경된 가장 중요한 부분인 addInteraction 함수는 아래와 같습니다.

function addInteraction() {
    var value = typeSelect.value;
 
    if (value !== 'None') {
        draw = new Draw({
            source: source,
            type: typeSelect.value,
            freehand: true
        });

        draw.on('drawend', function(e) {
            var format = new WKT();
            var feature = e.feature;
            var wkt = format.writeFeature(feature);
            var reader = new jsts.io.WKTReader()
            var jstsGeom = reader.read(wkt)
            var buffered = jstsGeom.buffer(200);
            var writer = new jsts.io.WKTWriter()
            var bufferedWKT = writer.write(buffered);
            var bufferedFeature = format.readFeature(bufferedWKT);

            feature.setGeometry(bufferedFeature.getGeometry());
        });

        map.addInteraction(draw);
    }
}

마우스를 통한 도형 그리기가 완료되면 11번 코드에 등록된 drawend 이벤트 함수가 호출됩니다. 이 이벤트 함수에서 그려진 도형을 WKT로 변환하고 JSTS.js에서 해석할 수 있는 지오메트로로 변환한 후 Buffer 연산을 수행합니다. 그리고 이를 다시 WKT로 변환하고 ol의 지오메트리로 변환하는 과정을 거치게 됩니다. 실행하면 앞서 살펴본 영상과 동일한 결과를 볼 수 있습니다.

GeoServer의 Cross Domain 허용 (CORS)

GeoServer에서 Cross Domain을 허용하기 위한 설정 방법을 기록해 둡니다. 먼저 아래처럼 GeoServer의 환경설정 파일을 편집하기 위해 vi를 실행합니다. 환경 설정 파일은 설치되는 GeoServer의 디렉토리에 따라 달라집니다.

vi /usr/geoserver-2.14.1/webapps/geoserver/WEB-INF/web.xml

내용 중 CORS에 해당되는 2곳에 대한 주석을 아래 그림처럼 해제합니다.

만약 이미 GeoServer가 실행 중이라면 종료하고 재기동해야 합니다.

mybatis를 통한 Select, Insert, Delete

과거에 mybatis 라이브러리 사용하기라는 제목으로 mybatis를 이용하기 위한 프로젝트 설정과 간단한 SELECT 문을 호출해 그 결과를 살펴보았습니다. 이번에 실제 프로젝트에 mybatis를 사용하기로 결정하여 좀더 구체적인 SQL 문을 호출하는 API를 파악하여 정리해 봅니다.

먼저 사용하는 테이블의 구조는 다음과 같습니다. (헐! 그림 대따 크게 표시되네.. -_-;)

앞서 언급해 드린 mybatis 라이브러리 사용하기를 통해 프로젝트를 구성하고, SQL 문을 정의하는 sql.xml 파일을 아래처럼 구성합니다.

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="test">
    <select id="getFirstPlayerName" resultType="string">
        SELECT name FROM players LIMIT 1
    </select>
    
    <select id="getFirstPlayer" resultType="map">
        SELECT name, age FROM players LIMIT 1
    </select>
    
    <select id="getPlayers" resultType="map">
        SELECT name, age FROM players
    </select>
    
    <select id="getPlayersWhere" resultType="map">
        SELECT name, age FROM players WHERE age > #{age}
    </select>
    
    <select id="getPlayersWhere2" resultType="map">
        SELECT name, age FROM players WHERE age > #{age} AND name LIKE #{name}
    </select>
    
    <insert id="insertPlayer"> 
    	INSERT INTO players (name, age) VALUES (#{name}, #{age}) 
    </insert>
    
    <delete id="deletePlayerWhere"> 
    	DELETE FROM players WHERE name = #{name} 
    </insert>    
</mapper>

보시는 것처럼 SELECT 문에 대한 getFirstPlayerName, getFirstPlayer, getPlayers, getPlayersWhere, getPlayersWhere2과 INSERT 문에 대한 insertPlayer, DELETE 문에 대한 deletePlayerWhere가 정의되어 있습니다. 이 7개에 대한 SQL문을 실행하는 API의 설명을 통해 mybatis에서 SELECT, INSERT, DELETE 문의 호출을 정리해 보겠습니다.

먼저 getFirstPlayerName은 테이블의 첫번째 Row에 대한 name 필드값을 가져오는 것인데, 관련된 API는 다음과 같습니다. 반환값이 string이고 반환결과는 1건 이하이므로 String과 SqlSession 객체의 selectOne 함수를 사용합니다.

String result = session.selectOne("test.getFirstPlayerName");
// 홍길동

다음은 getFirstPlayer인데, 반환결과는 역시 1건 이하인데 반환 필드가 name과 age로 2개입니다. API는 다음과 같습니다.

Map result = session.selectOne("test.getFirstPlayer");
 // {name=홍길동, age=18}

다음은 getPlayers로 반환 결과는 0건 이상이며 API는 다음과 같습니다.

List<Map<String, Object>> result = session.selectList("test.getPlayers");
// [{name=홍길동, age=18}, {name=임꺽정, age=32}, {name=일지매, age=21}]

getPlayersWhere는 조건 결과가 지정된 것으로 사용하는 API는 다음과 같습니다.

List<Map<String, Object>> result = session.selectList("test.getPlayersWhere", 20);
// [{name=임꺽정, age=32}, {name=일지매, age=21}]

getPlayersWhere2는 getPlayersWhere처럼 조건이 지정되어 있지만 비교 조건으로 사용되는 변수가 2개입니다. 사용하는 API는 다음과 같습니다.

Map param = new HashMap<>(); 
param.put("age", 20); 
param.put("name", "일%");

List<Map<String, Object>> result = session.selectList("test.getPlayersWhere2", param);
// [{name=일지매, age=21}]

insertPlayer는 데이터를 추가하는 것으로 API는 다음과 같습니다.

Map<String, Object> param = new HashMap<>(); 
param.put("age", 41); 
param.put("name", "황장군");

int result = session.insert("test.insertPlayer", param); // 1
session.commit();

deletePlayerWhere는 지정된 조건과 매칭되는 데이터를 삭제하는 것으로 사용하는 API는 다음과 같습니다.

Map<String, Object> param = new HashMap<>(); 
param.put("name", "황장군");

int result = session.delete("test.deletePlayerWhere", param);  // 1
session.commit();

mybatis는 별도의 xml 파일을 통해 SQL 문의 정의 부분을 독립적으로 유지할 수 있으며, SQL문을 보다 유연하게 처리할 수 있다는 문법 구문을 제공합니다. 또한 mybatis는 데이터베이스 연결시 발생하는 부하를 줄이기 위한 Connection Pool을 자체적으로 제공하고 있습니다. 이외에 JDBC를 직접 사용함에 따라 발생하는 복잡한 try .. catch .. finally 코드 부분이 제거되어 코드의 가독성이 향상됩니다.