Tableau SDKを利用してCSVから地図を表示する(その2)

# テキストファイルやDBの緯度経度情報を空間ファイルに変換せずにTableauで開く方法 - その2
<img width="750" alt="スクリーンショット 2017-10-16 0.30.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/47064/a0bab2b0-b882-192f-02d4-f4d8c2c9c5cb.png">

## Tableauと緯度経度情報の関係
Tableau 10.2からShape fileやKMLと言った空間ファイルをサポートしました
空間ファイルを利用することで、お手軽・高速にTableau上で扱うことができるようになりました

緯度経度情報が含まれているデータが空間ファイルであれば良いのですが、テキストファイルやDBの場合にライン・ポリゴン形状の緯度経度情報をお手軽・高速にTableau上で扱うには空間ファイルに変換するか、Tableau Extract Data(tde)形式の空間ファイルを必要があります

## 記事の内容
[Tableau SDKを利用してCSVから地図を表示する(その１)](https://qiita.com/nishiokya/items/212b40b7d88bc84fd5eb)では、空港の緯度経度をTableau SDKでポイント形式に変換しました
ポイント形式であればTableau SDKを利用しなくても、Tableau上でlat,lonフィールドの型を指定するだけで地図表示することができます
つまり、Tableau SDKを利用するメリットはあまりありません

そこで、今回はTableau SDKを利用するメリットがある、複雑なLineStringに挑戦します
表示する内容は、飛行機の空路のデータとなります
具体的には下記のサイトの一番上にあるような2点間の経路をアーチ上に描画します

<div class='tableauPlaceholder' id='viz1508078119809' style='position: relative'><noscript><a href='http:&#47;&#47;sciolisticramblings.wordpress.com&#47;2013&#47;11&#47;15&#47;flights-of-fancy&#47;'><img alt='Flight Explorer ' src='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;Op&#47;OpenFlightsDataExplorer&#47;FlightExplorer&#47;1_rss.png' style='border: none' /></a></noscript><object class='tableauViz'  style='display:none;'><param name='host_url' value='https%3A%2F%2Fpublic.tableau.com%2F' /> <param name='embed_code_version' value='2' /> <param name='site_root' value='' /><param name='name' value='OpenFlightsDataExplorer&#47;FlightExplorer' /><param name='tabs' value='no' /><param name='toolbar' value='yes' /><param name='static_image' value='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;Op&#47;OpenFlightsDataExplorer&#47;FlightExplorer&#47;1.png' /> <param name='animate_transition' value='yes' /><param name='display_static_image' value='yes' /><param name='display_spinner' value='yes' /><param name='display_overlay' value='yes' /><param name='display_count' value='yes' /></object></div>                <script type='text/javascript'>                    var divElement = document.getElementById('viz1508078119809');                    var vizElement = divElement.getElementsByTagName('object')[0];                    vizElement.style.width='650px';vizElement.style.height='710px';                    var scriptElement = document.createElement('script');                    scriptElement.src = 'https://public.tableau.com/javascripts/api/viz_v1.js';                    vizElement.parentNode.insertBefore(scriptElement, vizElement);                </script>

## 前提
PythonからTableau Extract Data(tde)形式の空間ファイルを作成するには以下の環境が必要になります

- TableauSDK for python
- python 2.7

# この記事の実行環境

この記事は以下の環境で実行しています
- Mac OS X
- Python 2.7.12 (Anaconda 4.2.0 (x86_64))
- geographiclib
- Tableau 10.4
- TableauSDK-10300.17.0915.2101

## Tableau SDKのインストール

[Tableau SDKを利用してCSVから地図を表示する(その１)](https://qiita.com/nishiokya/items/212b40b7d88bc84fd5eb)参照

## 2点間の経路を表示するpythonのライブラリのインストール
 2点間の経路を表示するために、[geographiclib](https://geographiclib.sourceforge.io)というライブラリを利用します
```
pip install  geographiclib
```


# Tableau SDKを用いて空路テキストファイルをTDEに変換する
## 作業フォルダ
作業フォルダは"work/tableau_sdk_test"とします
Mac OS Xの場合はターミナルを開いて以下のコマンドを実行してください
```
mkdir ~/work/tableau_sdk_test/
cd ~/work/tableau_sdk_test
```

## 空港と空路の緯度経度情報を入手する

1. [OpenFlights Data](https://openflights.org/data.html)から[airports.dat](https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat)ダウンロードをして、作業フォルダにおきます
2. [OpenFlights Data](https://openflights.org/data.html)から[routes.dat](https://raw.githubusercontent.com/jpatokal/openflights/master/data/routes.dat)ダウンロードをして、作業フォルダにおきます

# pythonコード
airports.datとroutes.datを保存したフォルダと同じ場所にairport2greatCirecleTde.pyを作成します
内容は以下です

```py:airport2greatCirecleTde.py
#!/usr/bin/env python 
# -*- coding: utf-8 -*- 

###
# @nishiokya 
#
# OpenFlights Data(https://openflights.org/data.html)のairports.dat (CSV)をTableau上で表示できるようになる
#
# Python = 2.7
# pip install  geographiclib

###

from tableausdk import Type
from tableausdk.Extract import *
from geographiclib.geodesic import Geodesic
import csv,codecs


# Where the origin data comes from
#csvLocation = 'https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat'
csvLocation = 'airports.dat'

# Where the TDE will be written
extractLocation = 'greate_circle_airport.tde'

# csv column number for origin/destination coordinates (start count at 0 for the first col in your csv)

#####################################################################
## Process the data and write the TDE
#####################################################################

# 1. initialize a new extract
ExtractAPI.initialize()

# 2. Create a table definition
new_extract = Extract(extractLocation)

# 3. Add column definitions to the table definition
table_definition = TableDefinition()
table_definition.addColumn('Airline', Type.UNICODE_STRING)  # column 0
table_definition.addColumn('Airline ID', Type.INTEGER)#1
table_definition.addColumn('Source airport', Type.UNICODE_STRING)#2
table_definition.addColumn('Source airport ID', Type.INTEGER)#3
table_definition.addColumn('Destination airport', Type.UNICODE_STRING)#4
table_definition.addColumn('Destination airport ID', Type.INTEGER)#5
table_definition.addColumn('Codeshare', Type.UNICODE_STRING)#6
table_definition.addColumn('Stops', Type.INTEGER)#7
table_definition.addColumn('Equipment', Type.UNICODE_STRING)#8
table_definition.addColumn('geometry', Type.SPATIAL)#9
table_definition.addColumn('Source airport Country', Type.UNICODE_STRING)#10
table_definition.addColumn('Source airport Name', Type.UNICODE_STRING)#11
table_definition.addColumn('Destination airport Country', Type.UNICODE_STRING)#12
table_definition.addColumn('Destination airport Name', Type.UNICODE_STRING)#13


# 4. Initialize a new table in the extract
if (new_extract.hasTable('Extract') == False):
  new_table = new_extract.addTable('Extract', table_definition)
else:
  new_table = new_extract.openTable('Extract')

# 5. Create a new row
new_row = Row(table_definition)  # Pass the table definition to the constructor

# 6. walk through the origin/destination data from CSV, write each path to TDE
#f = codecs.open(csvLocation,'r','utf-8')
#f =codecs.open(csvLocation, 'r', 'utf8')
airport  = {}
with codecs.open(csvLocation, 'r') as csvfile:
  csvreader = csv.reader(csvfile, delimiter=',')

  
  for row in (csvreader):
    """WKT POINT(LON LAT)"
    https://ja.wikipedia.org/wiki/Well-known_text
    """
   
    airport[(row[0])] = [row[7],row[6],
      row[1].decode('utf-8'),
      row[2].decode('utf-8'),
      row[3].decode('utf-8')]
    
   

with codecs.open("routes.dat", 'r') as csvfile:
  csvreader = csv.reader(csvfile, delimiter=',')

  
  for row in (csvreader):
    """WKT POINT(LON LAT)"
    https://ja.wikipedia.org/wiki/Well-known_text
    """
    o = []
    d = []#init list
    if row[3] in airport:
      #print(airport[row[3]])
      o = airport[row[3]]
    if row[5] in airport:
      #print(airport[row[5]])
      d = airport[row[5]]
    
    #空港の緯度経度とAirplan IDが取れている場合に出力
    if len(o) >0 and len(d)>0 and row[1].isdigit() == True :
        olat = float(o[1])
        olon = float(o[0])
        oname = o[2]
        ocountry = o[4]

        dlat = float(d[1])
        dlon = float(d[0])
        dname = d[2]
        dcountry = d[4]
      
        p = Geodesic.WGS84.Inverse(olat, olon, dlat, dlon)
        l = Geodesic.WGS84.Line(p['lat1'], p['lon1'], p['azi1'])
        if (p['s12'] >= 1000000):
          num = int(p['s12'] / 100000)
        else:
          num = 10
        output = ''
        linestring = 'LINESTRING('
        for i in range(num + 1):
          b = l.Position(i * p['s12'] / num, Geodesic.STANDARD | Geodesic.LONG_UNROLL)
          output += repr(b['lon2']) + ',' + repr(b['lat2']) + ', '
          linestring += str(b['lon2']) + ' ' + str(b['lat2']) + ', '
        # remove the ',' after the last coordinate and close the WKT string with a ')'
        linestring = linestring[:-2] + ')'
        #print(linestring)

        new_row.setString(0, row[0])
        new_row.setInteger(1, int(row[1]))
        new_row.setString(2, row[2])
        #print(row[3])
        new_row.setInteger(3, int(row[3]))
        new_row.setString(4, row[4])
        #print(row[5])
        #print(d)
        new_row.setInteger(5, int(row[5]))
        new_row.setString(6, row[6])
        new_row.setInteger(7, int(row[7]))
        new_row.setString(8, row[8])
        new_row.setSpatial(9, linestring)
        new_row.setString(10, ocountry)
        new_row.setString(11, oname)
        new_row.setString(12, dcountry)
        new_row.setString(13, dname)


        new_table.insert(new_row)

# 7. Save the table and extract
new_extract.close()

# 8. Release the extract API
ExtractAPI.cleanup()
```


## pythonの実行

Mac OS Xの場合はターミナルを開いて以下のコマンドを実行してください
成功すれば、all_airport.tdeというファイルができるはずです

```
python all_airport.py
```


## Tableauでの確認
Tableau Desktopでそのまま表示
<img width="1437" alt="スクリーンショット 2017-10-16 0.23.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/47064/75437bf6-657f-093b-845c-7c574fe9ec96.png">

中部国際空港のみ
<img width="750" alt="スクリーンショット 2017-10-16 0.30.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/47064/e35b2900-1f71-8633-2e49-f413210aeff9.png">

関西国際空港のみ
<img width="830" alt="スクリーンショット 2017-10-16 0.31.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/47064/14b7a1ae-89d0-5078-9f3a-cc6a12ede69b.png">

やっぱ中部国際空港は関西国際空港よりも少ない。。。


# お願い

今回の記事は緯度経度情報を、空間ファイルではなくTDEに変換する手法の使い方を説明しました
ただしTableau SDKによるプログラム作成が必要なので面倒な作業です
CSVファイルやDB内部の緯度経度情報を変換処理を介さずにTableauで表示できたほうがよいですよね？

CSVファイルやDB内部の緯度経度情報をTableauで表示のことを実現したい人が以下のTableau IDEASに要望を出しています　
[Plot WKT fields directly/Tableau Community](https://community.tableau.com/ideas/5494)

Tableauでは、新機能をコミュニティーからの要望が多いものから実現するそうです、もし賛同いただけるようでしたら、１票いれてください

<img width="903" alt="Vote" src="https://qiita-image-store.s3.amazonaws.com/0/47064/5e232b87-4e4d-b203-792e-672d7a325e20.png">

Tableau Communityへのログインが必要ですが、投票よろしく:bow_tone3:お願いします

# 関連情報
- [github](https://github.com/nishiokya/tableau_sdk_spatial_sample.git)
- [Tableau Public](https://public.tableau.com/profile/nishioka7038#!/vizhome/openflights_visualize/sheet0?publish=yes)

<div class='tableauPlaceholder' id='viz1508082593403' style='position: relative'><noscript><a href='#'><img alt='大圏データ ' src='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;op&#47;openflights_visualize&#47;sheet0&#47;1_rss.png' style='border: none' /></a></noscript><object class='tableauViz'  style='display:none;'><param name='host_url' value='https%3A%2F%2Fpublic.tableau.com%2F' /> <param name='embed_code_version' value='2' /> <param name='path' value='views&#47;openflights_visualize&#47;sheet0?:embed=y&amp;:display_count=y&amp;publish=yes' /> <param name='toolbar' value='yes' /><param name='static_image' value='https:&#47;&#47;public.tableau.com&#47;static&#47;images&#47;op&#47;openflights_visualize&#47;sheet0&#47;1.png' /> <param name='animate_transition' value='yes' /><param name='display_static_image' value='yes' /><param name='display_spinner' value='yes' /><param name='display_overlay' value='yes' /><param name='display_count' value='yes' /><param name='filter' value='publish=yes' /></object></div>                <script type='text/javascript'>                    var divElement = document.getElementById('viz1508082593403');                    var vizElement = divElement.getElementsByTagName('object')[0];                    vizElement.style.width='100%';vizElement.style.height=(divElement.offsetWidth*0.75)+'px';                    var scriptElement = document.createElement('script');                    scriptElement.src = 'https://public.tableau.com/javascripts/api/viz_v1.js';                    vizElement.parentNode.insertBefore(scriptElement, vizElement);                </script>

# 参考情報
- https://onlinehelp.tableau.com/current/api/sdk/en-us/help.htm
- https://github.com/sarahbat/Examples-using-TableauSDK/tree/master/postgisToTde
- https://community.tableau.com/ideas/5494