GeoService-Xr과 FingerEyes-Xr의 On-The-Fly-Projection 적용하기

다양한 좌표계가 존재하므로, 사용자마다 자신이 가지고 있는 공간 데이터마다 서로 다른 좌표계를 적용하고 있습니다. 이러한 서로 다른 좌표계의 공간 데이터를 DBMS에 Import할때, 좌표계를 명시해 주고, 이를 사용하는 클라이언트 단에서는 서로 다른 좌표계를 하나의 기준 좌표계로 변환하여 사용하게 됩니다. 그래야 레이어로써의 여러개의 지도가 하나의 지도로 중첩됩니다.

GeoService-Xr과 FingerEyes-Xr에서도 서로 다른 좌표계에 대해 동적으로 좌표계를 변환해 단일 좌표계로 변환해 레이어를 중첩해 주는 On-The-Fly-Projection 기능을 제공합니다. 이를 위해서는 서버인 GeoService-Xr 과 클라이언트인 FingerEyes-Xr에서 간단한 설정이 필요한데요. 이 내용을 정리해 봅니다.

먼저 서버인 GeoService-Xr에서는 DBMS에 저장된 공간 데이터를 GeoData라는 단위로 관리하는데, 이 GeoData를 정의하는 xml 파일의 내용에 EPSG를 설정할 수 있습니다. 아래의 예와 같습니다.


    parcel
    muan_db://public."parcel"
    5186



    link
    network://public."link"
    5179

위의 내용은 parcel이라는 GeoData의 좌표계는 EPSG:5186이고, link라는 GeoData의 좌표계는 EPSG:5179라는 것을 명확히 하고 있습니다.

다음은 클라이언트 단의 코드인데요. 클라이언트 단에서 공통 좌표로 사용하고자 하는 좌표계를 아래처럼 지정하면 됩니다.

map.EPSG(5186);

위의 코드에 의해 클라이언트 단에서 사용하는 단일 좌표계는 EPSG:5186으로써, 만약 서버 단에서 이와 다른 좌표계의 데이터가 오면 이를 EPSG:5186으로 변환하도록 합니다. (명확히는 서버단에서 좌표계 변환을 수행함)

아래의 그림은 서로 다른 좌표계를 가지는 parcel(지적도로 EPSG:5186)와 link(네트워크 링크로 EPSG:5179)가 하나의 단일 좌표계(EPSG:5186)으로 변환되어 정확히 그 위치가 일치되는 것을 보여주고 있습니다.

회전 제한 및 양방향 성질을 가진 네트워크 DB를 활용한 A* 알고리즘

실제 도로망은 U턴 제한이나 우회전 제한 등과 같은 회전 제한에 대한 성질과 좌측 및 우측 차로에 대한 방향에 대한 성질을 가집니다. 이러한 성질에 대한 속성값을 가지는 네트워크 데이터는 지능형 교통체계 표준 노드링크 관리시스템(http://nodelink.its.go.kr)에서 제공하고 있습니다.

이 글은 지능형 교통체계 표준 노드링크 관리시스템에서 제공하는 네트워크 DB에 대해 A* 알고리즘을 적용하는 각 단계별 과정에 대한 상태정보를 기록한 자료에 대한 글입니다. A*에 대한 자세한 설명은 기존의 최단 경로 탐색 – A* 알고리즘이라는 글을 참고하시기 바랍니다. 본 글은 A* 알고리즘을 확장하고 응용한 글이므로 반드시 A* 알고리즘을 완전하게 이해하고 있는 상태에서만 의미있게 이해될 수 있는 글이라는 점을 알려드립니다.

해결하고자 하는 네트워크 DB에서의 최적경로에 대한 문제는 아래 그림과 같습니다.

위의 문제에 대해 확장된 A* 알고리즘을 통해 최종적으로 도출된 그 결과는 아래의 그림과 같습니다.

해결하고자 하는 문제와 그 결과 도출을 위한 알고리즘의 각 단계를 정리한 내용은 아래의 pdf 파일에 담겨 있습니다.

Java에서 HashMap을 위한 Custom Key 정의하기

Java의 HashMap 자료구조는 고유한 값인 Key와 1:1로 연관되는 값(Value)을 하나의 쌍(Pair)로 하여 여러 개의 쌍을 저장하는 자료구조입니다. 흔히 Key 값으로 문자열이나 정수값을 사용하는 것으로도 충분합니다. 그러나 기능에 따라서 개발자가 특별한 Key에 대한 타입을 정의해야할 필요가 있습니다.

만약 개발자가 Key 타입으로 아래와 같은 클래스를 정의했다고 합시다.

public class Identifier {
    private long A;
    private long B;
    private long C;
	
    public Identifier(long A, long B, long C) {
        this.A = A;
        this.B = B;
        this.C = C;
    }
}

그리고 위의 Identifier 클래스를 Key로 하는 HashMap을 사용하는 코드를 아래와 같이 작성했다고 합시다.

HashMap<Identifier, String> map = new HashMap<Identifier, String>();
		
Identifier id1 = new Identifier(1,2,3);
map.put(id1, "White");
		
Identifier id2 = new Identifier(2,1,3);
map.put(id2, "Yellow");
		
Identifier id3 = new Identifier(2,1,3);
map.put(id3, "Red");
		
System.out.println(map.get(id2));

Key로써의 Identifier 클래스의 동일성을 Identifier가 가지는 3개의 필드인 A, B, C의 동일함으로 정의한다고 할때, 두개의 Identifier 객체 o1, o2가 있다고 한다면 o1과 o2가 같을 조건은 o1.A == o2.A && o1.B == o2.B && o1.C == o2.C라고 할 수 있습니다. 이러한 정의대로라면 위의 코드의 결과는 new Identifier(2,1,3)으로 생성된 객체 중 가장 마지막으로 저장된 “Red” 문자열을 출력해야 하는데, “Yellow”를 출력하게 됩니다. 그렇다면 원하는 결과를 얻기 위해서 무엇을 해야 할까요?

HashMap의 Key 타입으로써 기능을 충족하기 위해서는 Object 클래스의 equals와 hashCode 함수를 재정의해야 합니다. 아래가 Identifier 클래스에 이 2개의 함수를 재정의한 전체 코드입니다.

import java.util.Objects;

public class Identifier {
    private long A;
    private long B;
    private long C;
	
    public Identifier(long A, long B, long C) {
        this.A = A;
        this.B = B;
        this.C = C;
    }
	
    @Override
    public boolean equals(Object o) {
        if(this == o) return true;	
        if(o == null || getClass() != o.getClass()) return false;
		
        Identifier other = (Identifier)o;
		
        return other.A == A && other.B == B && other.C == C;
    }
	
    @Override
    public int hashCode() {
        int result = (int)(A ^ (A >>> 32));
        
        result = 31 * result + (int)(B ^ (B >>> 32));
        result = 31 * result + (int)(C ^ (C >>> 32));
		
        return result;
    }
}

위의 equals 함수는 앞서 설명한 동일성에 대한 정의를 코드로 작성한 것입니다. hashCode 함수는 최대한 중복되지 않고 다양한 값을 반환할 수 있어야 함과 동시에 같은 값이라고 한다면 같은 hashCode 값을 반환해야 합니다. 즉, new Identifier(2,1,3)를 2번 호출하여 2개의 객체를 생성했다면 이 2개의 객체에 대한 hashCode 함수의 반환값은 같아야 합니다. 이러한 조건을 최대한 수용할 수 있도록 작성했다고 작성한게 위의 hashCode 함수의 구현입니다만… 이게 애매한지라 hashCode 함수에 대해서만큼은 아래의 코드로 대체할 수 있습니다.

@Override
public int hashCode() {
    return Objects.hash(A, B, C); // import java.util.Objects; 가 필요함
}

이제 실행해 보면, 우리가 원하는 결과를 얻을 수 있습니다. 이상으로 HashMap에 대한 Custom Key 타입을 정의하는 방식에 대한 정리였습니다.

jetty를 이용한 WebSocket 서버 구현하기

어플리케이션에 Servlet Container 이외도 여러가지 서버 기능을 내장할 수 있는 jetty 라이브러리를 이용해 WebSocket 서버를 구현하는 코드에 대한 뼈대를 정리한다. WebSocket는 HTML5에서 지원하는 기능으로 서버와 클라이언트는 연결되어 있는 상태에서 상호간에 데이터를 주고니 받거니 할 수 있는 기술이다. WebSocket은 그 막강한 통신 기능에 비해 서버와 클라이언트의 코드가 매우 작다. 그래서인지 뭔가 부족한 느낌이 드는데, 일단 기본적인거 정리하고 부족한 느낌이 드는 이유는 나중에 파악해 보자.

먼저 서버 단의 코드이다. Server를 구동하는 클래스와 WebSocket를 통해 접속하는 클라이언트를 나타내는 Session 범위의 클래스인데, 먼저 Server를 구동하는 클래스는 아래와 같다.

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

public class test_webSocket {
    public static void main(String[] args) {
        Server server = new Server(8080);
		
        WebSocketHandler wsHandler = new WebSocketHandler() {
            @Override
            public void configure(WebSocketServletFactory factory) {
                factory.register(MyEchoSocket.class);
            };
        };

        ContextHandler context = new ContextHandler();
        context.setContextPath("/echoServer");
        context.setHandler(wsHandler);
		
        server.setHandler(context);

        try {
            server.start();
            server.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

그리고 다음은 서버에 접속하는 클라이언트(Session 객체로 구분) 마다 생성되는 Echo 기능을 갖는 MyEchoSocket 클래스이다.

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;

public class MyEchoSocket implements WebSocketListener {
    private Session outbound;
	
    public MyEchoSocket() {
        System.out.println("class loaded " + this.getClass());
    }

    @Override
    public void onWebSocketConnect(Session session) {
        this.outbound = session;		
    }

    @Override
    public void onWebSocketError(Throwable cause)
    {
        cause.printStackTrace(System.err);
    }

    @Override
    public void onWebSocketText(String message)
    {
        if ((outbound != null) && (outbound.isOpen()))
        {
            String echoMessage = outbound.hashCode() + " : " + message;
            outbound.getRemote().sendString(echoMessage, null);
        }
    }

    @Override
    public void onWebSocketBinary(byte[] payload, int offset, int len)
    {
        //.
    }

    @Override
    public void onWebSocketClose(int statusCode, String reason)
    {    	
    	System.out.println("Close, statusCode = " + statusCode + ", reasone = " + reason);
        this.outbound = null;
    }	
}

요즘 웹이 그렇듯이 WebSocket 기술은 Binary 데이터도 주고 받을 수 있다는 것을 알 수 있다. 중요한 것은 클라이언트 하나가 접속을 하면 위의 클래스가 매번 생성된다는 것이고, 클라이언트는 Session 객체를 통해 나타내지며, 이 객체를 통해 데이터를 주고 받을 수 있다.

서버를 살펴봤으니, 이제 클라이언트 코드를 살펴보면..




    
    


    

위의 13번 부분에 들어가는 js 코드는 다음과 같다.

        var webSocket = new WebSocket("ws://localhost:8080/echoServer/");
        var msgField = document.getElementById("messageField");
        var divMsg = document.getElementById("msg-box");

        function sendMsg() {
            var msgToSend = msgField.value;

            webSocket.send(msgToSend);

            divMsg.innerHTML += "
Client> " + msgToSend + "
"; msgField.value = ""; } webSocket.onmessage = function (message) { divMsg.innerHTML += "Server> : " + message.data; } webSocket.onopen = function () { alert("connection opened"); } webSocket.onclose = function () { alert("connection closed"); } webSocket.onerror = function (message) { alert("error: " + message); }

서버를 기동한 뒤, 클라이언트를 실행하고 메세지를 날리면, 해당 메세지가 메아리로 돌아오는 것을 볼 수 있다.