이 글에서는 GeoJSON 포맷으로 작성된 지리 정보를 레일즈 데이타베이스에 저장한 후 이 정보를 맵박스를 통해서 렌더링하는 기법을 소개한다.
레일즈 모델 및 테이블 생성
rails g model geolocation name geo:jsonb
rake db:migrate
여기서 주목할 점은 GeoJSON 포맷의 정보를 저장할 geo 칼럼의 타입을 jsonb로 지정하는 것이다. jsonb 타입의 칼럼을 사용하기 위해서는 PostgreSQL 9.4 이상이 설치되어 있어야 한다.
샘플 데이터 입력
샘플로 사용할 GeoJSON 데이타를 확보한다. 커뮤니티에서 작성한 데이타를 사용해도 되고 geojson.io를 통해서 직접 만들어도 된다. 예를 들어 아래 파일은 뉴욕 엠파이어 스테이트 빌딩 위치와 센트럴 파크 경계를 표현한 GeoJSON 데이타이다.
vi empire_state_building.geojson
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [-73.9856644, 40.7484405]
      },
      "properties": {
        "title": "Empire State Building",
        "description": "350 5th Ave New York, NY 10118 USA",
        "marker-color": "#fc4353",
        "marker-size": "small",
        "marker-symbol": "commercial"
      }
    }
  ]
}
vi central_park.geojson
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "title": "Central Park",
        "stroke": "#008f00",
        "stroke-width": 1,
        "stroke-opacity": 1,
        "fill": "#4f8f00",
        "fill-opacity": 0.3
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -73.95858764648438,
              40.80029619806279
            ],
            [
              -73.98210525512695,
              40.76845173617708
            ],
            [
              -73.9735221862793,
              40.76481139691839
            ],
            [
              -73.95034790039062,
              40.7970474627213
            ],
            [
              -73.95858764648438,
              40.80029619806279
            ]
          ]
        ]
      }
    }
  ]
}
GeoJSON 파일이 확보되었으므로 아래와 같이 geolocations 테이블에 레코드를 생성한다.

Geolocation.create!(name: 'Empire State Building', geo: JSON.parse(File.read('empire_state_building.geojson'))
Geolocation.create!(name: 'Central Park', geo: JSON.parse(File.read('central_park.geojson'))
레일즈 콘트롤러
rails g controller mapcontroller index --skip-assets --skip-template-engine --skip-helper
vi mapbox_controller.rb

# app/controllers/mapbox_controller.rb
def index
  geolocations = Geolocation.all.select(:id, :name, :geo)
  render json: geolocations, root: false
end
vi routes.rb

# config/routes.rb
get '/mapbox', to: 'mapbox#index'
이제 curl 명령으로 JSON 응답이 동작하는지 시험한다.
curl http://localhost:3000/mapbox
앵귤러 마크업
<geo-map></geo-map>
앵귤러 컴포넌트 - 디렉티브
vi geojson.coffee

# app/assets/angulars/mapboxes/geojson.coffee
app.directive 'geoMap', [ () ->
  restrict: 'E'
  replace: 'true'
  templateUrl: 'mapboxes/geo-map.html'
  controller: [
    ...
  ]
  controllerAs: 'mapBoxCtrl'
앵귤러 컴포넌트 - 템플릿
vi geo-map.html

<!-- app/assets/angulars/mapboxes/geo-map.html -->
<div class="row">
  <div class='col-md-9'>
    <div id="map" style="height: 480px"></div>
  </div>
  <div class='col-md-3'>
    <div class="list-group">
      <a class="list-group-item" ng-repeat="place in mapBoxCtrl.places" ng-click="mapBoxCtrl.panTo(place)">
        {{mapBoxCtrl.getTitle(place)}}
      </a>
    </div>
  </div>
</div>
앵귤러 컴포넌트 - 팩토리
vi geojson.coffee

# app/assets/angulars/mapboxes/geojson.coffee
app.factory 'GeoLocation', [ '$http', ($http) ->
  @all = () ->
    $http.get('/mapbox.json')
  @
]
Geolocation 팩토리는 잠시 후에 설명할 MapBoxCtrl 콘트롤러에 인젝트되며, Geolocation.all() 형태로 사용된다. 여기서 all()은 레일즈의 Mapbox 콘트롤러 index 액션에 대한 비동기 요청을 수행하고 프라미스(Promise)를 리턴하게 된다.
앵귤러 컴포넌트 - 디렉티브 콘트롤러
vi geojson.coffee

# app/assets/angulars/mapboxes/geojson.coffee
app.directive 'geoMap', [ () ->
  ...
  controller: [ '$scope', '$http', 'GeoLocation', ($scope, $http, GeoLocation) ->
    L.mapbox.accessToken = '[YOUR MAPBOX TOKEN]'

    map = L.mapbox.map('map', '[YOUR MAPBOX MAP ID]',
      scrollWheelZoom: false
      legendControl:
        position: 'topright'
    ).setView([40.75, -73.98], 13)

    map.legendControl.addLegend('POI in New York')

    @places = []
    GeoLocation.all().then (response) =>
      _.forEach response.data, (geolocation) =>
        layer = L.mapbox.featureLayer().addTo(map)
        layer.setGeoJSON(geolocation.geo)
        layer.eachLayer (locale) =>
          @places.push(locale)
          locale.on 'click', ->
            if locale.feature.geometry.type == 'Point'
              map.panTo(locale.getLatLng())
            else # locale.feature.geometry.type == 'LineString' or 'Polygon'
              map.panTo(locale.getBounds().getCenter())

    @getTitle = (locale) ->
      locale.feature.properties.title

    @panTo = (locale) ->
      if locale.feature.geometry.type == 'Point'
        map.panTo(locale.getLatLng())
        locale.openPopup(locale.getLatLng())
      else # locale.feature.geometry.type == 'LineString' or 'Polygon'
        map.panTo(locale.getBounds().getCenter())
        locale.openPopup(locale.getBounds().getCenter())
    @
  ]
  controllerAs: 'mapBoxCtrl'
콘트롤러에서는 Geolocation.all()의 프라미스(Promise)를 리졸브(Resolve) 하여 MapController에서 보낸 JSON 응답을 얻는 것으로 시작한다. JSON 응답의 매 레코드마다 featureLayer를 생성하고, 이 레이어에 수신한 GeoJSON 데이터를 연결하면 map에 벡터 그래픽이 렌더링된다. 현재 우리 예제에서는 Geolocations 테이블의 geo 칼럼에 오직 한 곳에 대한 GeoJSON 데이타 만을 저장하고 있으나, 복수 위치에 대한 GeoJSON 데이타를 한번에 만들고 같은 레이어에서 여러곳의 위치를 동시에 렌더링할 수도 있다. 이런 경우 특정 레이어에 대한 eachLayer()를 사용하면 레이어내 개별 위치에 대한 정보를 얻을 수 있다. 이 개별 위치 정보를 인스턴스 변수 @places에 저장하여 사이드바 리스트 구현에 이용한다.