Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

용어 정리

지구를 중심으로 하는 공간 데이터를 다루려고 하니, 생소한 용어가 한둘이 아니다. 정리를 해야 겠다.

WGS84

한 지점의 좌표 값은 어떤 측지계를 기준으로 하느냐에 따라 달라진다. 과거 우리나라에서 사용하던 측지계는 Tokyo를 중심으로 사용했다. 최근에는 WGS84(1984년에 만든 최신 Word Geodetic System 이다. WGS 1984 혹은 EPSG:4326이라고 부르기도 한다)라고 부르는 세계측지계를 사용한다.

WGS84의 좌표 원점은 지구 질량 중심에 있다. 위치는 2cm 보다 작다고 한다.

투영

지도를 제작하기 위해서는 지구를 일정한 기준에서 봐야 하는데, 지구는 완벽한 원형 혹은 타원형이 아니다. 실제 울퉁불퉁한 표면을 가지고 있기 때문에 수학적으로 구현하기에 무리가 따른다. 그래서 수학적으로 쉽게 계산하기 위한 유사타원체(Geoid)를 만들었다. 지도의 좌표체계의 차이는 지오이드를 정하는 방법의 차이다.
  • Bessel 타원체 : 동경좌표계(Tokyo Datum)를 사용.
  • WGS84 : 미국방성이 1984년 채택한 GPS용 타원체
이 타원체는 3차원 곡면좌표를 가지는데, 이를 우리가 사용하는 2차원 평면좌표에 표현하기 위해서 투영이라는 과정을 거친다.

투영법(도법)은 나라와 지역별로 다양하다. 베셀타원체의 투영법으로는 TM 도법을 사용하고, WGS84 타원체의 경우 UTM 도법을 사용한다. 우리나라는 TM과 UTM을 모두 사용한다.

Spatial 데이터 저장

Mysql, ElasticSearch 혹은 Solr, MongoDB등 많은 RDBMS혹은 NoSQL 애플리케이션들이 spatial 쿼리를 지원한다.

나는 Postgresql의 spatial 기능 확장인 PostGIS로 공간 데이터를 저장하기로 했다. 이유는 기능과 안정성 모든 측면에서 독보적이라는 평가가 많아서이다. 상용 DBMS인 Oracle과 비교해서도 그렇다는 많은 글들을 읽을 수 있었다.

PostGIS 설치 및 간단한 사용

역시 우분투 이미 패키징 돼 있다.
# apt-cache search postgis
...
postgis - Geographic objects support for PostgreSQL
postgis-doc - Geographic objects support for PostgreSQL -- documentation
postgis-gui - Geographic objects support for PostgreSQL -- GUI programs
postgresql-9.6-pgrouting - Routing functionality support for PostgreSQL/PostGIS
postgresql-9.6-pgrouting-doc - Routing functionality support for PostgreSQL/PostGIS (Documentation)
postgresql-9.6-pgrouting-scripts - Routing functionality support for PostgreSQL/PostGIS - scripts
postgresql-9.6-postgis-2.3 - Geographic objects support for PostgreSQL 9.6
postgresql-9.6-postgis-2.3-scripts - Geographic objects support for PostgreSQL 9.6 -- scripts
....
GUI 프로그램도 제공하는 것 같은데, 일단은 "postgresql-9.6-postgis-2.3"만 설치했다.
$ apt-get install postgresql-9.6-postgis-2.3
postgres 사용 문서는 아니다. 유저를 만들거나 하는 것들을 여기에서 하고 싶지 않다. 그냥 슈퍼유저인 "postgres" 계정으로 su(switch user) 한 다음에, 작업을 하자.

postgres로 su 한다음 테스트를 위한 데이터 베이스를 만든다.
$ sudo su - postgres
$ createdb mygis
$ psql mygis
mygis=# \conninfo
You are connected to database "mygis" as user "postgres" via socket in "/var/run/postgresql" at port "5432".
PostGIS는 postgresql의 확장기능이다. spatial function을 이용하기 위해서는 postgis 확장을 로딩해야 한다.
mygis=# create extension postgis;
CREATE EXTENSION
이제 mygis 데이터베이스에서 spatial functions를 사용할 수 있게 됐다. 서울 시청(126.9778923,37.5654808)에서 광주광역시청(126.8491083,35.1600765)까지의 직선거리를 구해보자. "ST_DistanceSphere" 함수를 이용하면 두 지점(경도,위도)의 거리를 구 할 수 있다. st_makepoint함수는 입력한 두개의 float값을 경도,위도로 변환한다.
mygis=# select ST_DistanceSphere(st_makepoint(126.9778923,37.5654808),st_makepoint(126.8491083,35.1600765));
 st_distancesphere 
-------------------
    267717.5019854
267.717 km로 나왔다. 실제 거리와 일치한다.

Geometric Types

PostGresql에서 제공하는 Geometric Types 들이다. MSSQL, MySQL들도 비슷한 타입유형을 제공한다.

point 16 bytes 평면에서 점의 위치 (x,y)
line 32 bytes 무한 선 (A, B, C)
lseg 32 bytes 유한 선 ((x1,y1), (x2,y2))
box 32 bytes 4각형 ((x1,y1), (x2,y2))
path 16+16n bytes 닫힌 경로(폴리곤과 유사) ((x1,y1),...)
path 16+16n bytes 열린 경로 [(x1,y1),...]
polygon 40+17n bytes 폴리곤 ((x1,y1),...)
circle 24 bytes <(x,y), r>, 점과 반지름

Geometry 함수들

  • sum(expression) : aggregate to return a sum for a set of records
  • count(expression) : aggregate to return the size of a set of records
  • ST_GeometryType(geometry) : returns the type of the geometry
  • ST_NDims(geometry): return the number of dimensions of the geometry
  • ST_SRID(geometry) : returns the spatial reference identifier number of the geometry
  • ST_X(point) : return the X ordinate
  • ST_Y(point) : return the Y ordinate
  • ST_Length(linestring) : returns the length of the linestring
  • ST_StartPoint(geometry) : returns the first coordinate as a point
  • ST_EndPoint(geometry) : returns the last coordinate as a point
  • ST_NPoints(geometry) : returns the number of coordinates in the linestring
  • ST_Area(geometry) : return the area of the polygons
  • ST_NRings(geometry) : return the number of rings
  • ST_ExteriorRing(polygon) : return the outer ring as a linestring
  • ST_InteriorRingN(polygon, integer) : return a specified interior ring as a linestring
  • ST_Perimeter(geometry) : return the length of all the rings
  • ST_NumGeometries(multi/geomcollection) : return the number of parts in the collection
  • ST_GeometryN(geometry, integer) : returns the spcified part of the collection
  • ST_GeomFromText(text) : returns geometry
  • ST_AsText(geometry) : returns WKT text
  • ST_AsEWKT(geometry) : returns EWKT text
  • ST_GeomFromWKB(bytea) : returns geometry
  • ST_GeomFromGML(Text) : returns geometry
  • ST_AsGML(geometry) : returns GML text
  • ST_GeomFromKML(text) : returns geometry
  • ST_AsKML(geometry) : return KML text
  • ST_AsGeoJSON(geometry) : return JSON text
  • ST_AsSVG(geometry) : return SVG text

예제 테이블

PostGIS의 첫번째(그리고 가장 중요한) 기능은 지리 데이터를 다루는 것이다. 실제 작동하는지 테스트하기 위해서 맛집 정보를 담고 있는 테이블을 만들기로 했다. 테이블의 이름은 restaurant로 스키마는 아래와 같다.
name varchar(80) 식당 이름
category varchar(20) 한식,양식,중식,이탈리안,기타
latitude float 위도
longitude float 경도
point int 점수 1-5
CREATE TABLE restaurant (
  name      VARCHAR(80),
  category  VARCHAR(20),
  latitude  FLOAT,
  longitude FLOAT,
  point     INT
);

INSERT INTO restaurant VALUES('푸켓쌀국수', '기타', 37.499851, 127.026272, 3 );
INSERT INTO restaurant VALUES('메드포갈릭', '이탈리안', 37.497774, 127.026615, 4 );
INSERT INTO restaurant VALUES('아리랑', '한식', 37.499587, 127.027098, 5 );
INSERT INTO restaurant VALUES('참치공방', '한식', 37.497868, 127.028428, 5 );
INSERT INTO restaurant VALUES('베이징', '중식', 37.496395, 127.029297, 3 );

위의 데이터를 google map에 나타냈다. 가운데 있는 비셔스는 "내 위치"다. 빨간원은 나를 중심으로 150m 범위를 나타낸다. 나는 내 위치를 중심으로 150m 범위에 있는 맛집을 검색하고 싶다.

Geometry

PostGIS는 spatial data 저장을 위해서 geomtry 타입을 지원한다.

restaurant 테이블에 "geometry" 필드를 추가했다.
mygis=# ALTER TABLE restaurant ADD COLUMN geom geometry(POINT,4326);
geometry 타입은 POINT와 SRID 두개의 값을 가진다.
  • POINT 는 postgresql이 제공하는 Geometry Types 중 하나로 평면에서 점을 표현할 때 사용한다.
  • SRID는 spatial reference identifier는 평면 지구, 혹운 둥근지구 매핑에 사용되는 특정 타원면을 기준으로 하는 공간 참조 시스템이다. rastaurant 테이블은 SRID 4326이라는 투영체를 사용하는데, 이는 WGS84를 의미한다.
latitude, longitude 컬럼 데이터를 이용해서 gemoetry 필드를 업데이트 했다.
mygis=# UPDATE restaurant SET geom=ST_SetSRID(ST_MakePoint(longitude,latitude),4326);
UPDATE 5

SELECT를 해보자.
mygis=# select * from restaurant ;
    name    | category | latitude  | longitude  | point |                        geom                        
------------+----------+-----------+------------+-------+----------------------------------------------------
 푸켓쌀국수 | 기타     | 37.499851 | 127.026272 |     3 | 0101000020E61000003C33C170AEC15F40BBEF181EFBBF4240
 메드포갈릭 | 이탈리안 | 37.497774 | 127.026615 |     4 | 0101000020E610000051A5660FB4C15F404966F50EB7BF4240
 아리랑     | 한식     | 37.499587 | 127.027098 |     5 | 0101000020E61000005EF23FF9BBC15F40DD408177F2BF4240
 참치공방   | 한식     | 37.497868 | 127.028428 |     5 | 0101000020E61000009B92ACC3D1C15F40CADC7C23BABF4240
 베이징     | 중식     | 37.496395 | 127.029297 |     3 | 0101000020E6100000BD378600E0C15F40EF7211DF89BF4240
(5 rows)
geometry는 좌표정보를 16진수 코드로 제공한다. 인간이 읽기 쉬운 값으로 변환해 보자.
mygis=# SELECT name, latitude, longitude, ST_AsText(geom), ST_AsEwkt(geom), ST_X(geom), ST_Y(geom) FROM restaurant ;
    name    | latitude  | longitude  |          st_astext          |               st_asewkt               |    st_x    |   st_y    
------------+-----------+------------+-----------------------------+---------------------------------------+------------+-----------
 푸켓쌀국수 | 37.499851 | 127.026272 | POINT(127.026272 37.499851) | SRID=4326;POINT(127.026272 37.499851) | 127.026272 | 37.499851
 메드포갈릭 | 37.497774 | 127.026615 | POINT(127.026615 37.497774) | SRID=4326;POINT(127.026615 37.497774) | 127.026615 | 37.497774
 아리랑     | 37.499587 | 127.027098 | POINT(127.027098 37.499587) | SRID=4326;POINT(127.027098 37.499587) | 127.027098 | 37.499587
 참치공방   | 37.497868 | 127.028428 | POINT(127.028428 37.497868) | SRID=4326;POINT(127.028428 37.497868) | 127.028428 | 37.497868
 베이징     | 37.496395 | 127.029297 | POINT(127.029297 37.496395) | SRID=4326;POINT(127.029297 37.496395) | 127.029297 | 37.496395

내가 있는 위치로 부터 150m 이내에 있는 음식점을 찾아보자.
mygis=# SELECT * FROM restaurant WHERE ST_DistanceSphere(geom, ST_MakePoint(127.026993,37.497933)) <= 150;
    name    | category | latitude  | longitude  | point |                        geom                        
------------+----------+-----------+------------+-------+----------------------------------------------------
 메드포갈릭 | 이탈리안 | 37.497774 | 127.026615 |     4 | 0101000020E610000051A5660FB4C15F404966F50EB7BF4240
 참치공방   | 한식     | 37.497868 | 127.028428 |     5 | 0101000020E61000009B92ACC3D1C15F40CADC7C23BABF4240
(2 rows)
성공이다.

내가 있는 위치에서 음식점까지의 거리를 출력해보고 싶다.
SELECT name, latitude, longitude, ST_DistanceSphere(geom, ST_MakePoint(127.026993,37.497933)) FROM restaurant WHERE ST_DistanceSphere(geom, ST_MakePoint(127.026993,37.497933)) <= 150;
    name    | latitude  | longitude  | st_distancesphere 
------------+-----------+------------+-------------------
 메드포갈릭 | 37.497774 | 127.026615 |       37.74392799
 참치공방   | 37.497868 | 127.028428 |      126.80109399
(2 rows)

기타 참고할 만한 Spatial 데이터베이스

  • Redis Geospatial Queries : Redis 3.2.0 버전 부터 지원한다. Redis는 메모리기반으로 데이터에 대한 고속 접근을 지원한다. PostGIS와 함께 사용 해서, 성능을 높일 수 있을 것 같다.

위치 데이터 저장 애플리케이션 예제

PostGIS와 goLang을 이용해서 위치 데이터를 저장하고 꺼내오는 웹 애플리케이션 서버를 만들어보기로 했다. 음식점 데이터를 입력하고 검색하는 애플리케이션이다.
  1. google map 상에서 Point를 찍고, 음식점 정보 몇 가지를 입력하고 submit 하면 데이터베이스에 저장한다.
  2. 자신의 위치에서, 가장 가까운 음식점을 검색한다. 위치는 HTML5 GEO API를 이용한다.
    CREATE TABLE restaurant (
      userid     VARCHAR(20),
      name       VARCHAR(80),
      category   VARCHAR(20),
      latitude   FLOAT,
      longitude  FLOAT,
      geom       GEOMETRY(POINT,4326),
      point      smallint,
      created_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    UPDATE restaurant SET geom=ST_SetSRID(ST_MakePoint(longitude,latitude),4326);
... 계속

참고