アプリケーションで地図を利用すると言って最初に思いつくのはGoogle Mapあたりだと思われるが、業務利用を考えた場合には一定以上の利用で料金が発生すること(
*1,
*2)やインターネットにアクセスしなければいけないことを考えると使いにくいと感じるユーザもいると思われる。ちなみに、非商用であればGoogleマップのスクリーンショットをブログに数枚利用することは可能とのこと。ただし権利帰属表示をする等の条件をクリアする必要はある(
*3)。
であれば、スタンド・アローン環境でも利用できてライセンスが緩く、かつJavaからも利用可能な地図はないかと探して見つけたのがGeo Toolsライブラリである。Geo Toolsライブラリはシェープファイル(拡張子:shp等)という地理情報ファイルを扱うライブラリであり、地図のシェープファイルを取り込むことにより地理情報の取得や地図の描画が行える。地図のシェープファイルは国土地理院をはじめとする様々なwebサイトでデータが公開されているため、必要に応じて逐次検索すればよい。
というわけで、今回は地形情報データライブラリであるGeo Toolsライブラリを使った地図の描画方法を確認する。
■ シェープファイルとは?
シェープファイルはEsri 社の提唱したベクトル形式の業界標準フォーマットで、仕様書はEsri社が公開されている(
*4)。シェープファイルではいくつかのファイルで一つの地理情報を構成し、主なファイル種類は以下の通りである(
*5)。dbfファイルのフォーマットはdBaseという古くてよく知られたフォーマットであるため、excelなどの表計算ソフトで中身を確認することができる。
拡張子 |
内容 |
*.shp |
図形情報 |
*.dbf |
属性情報(地名や国名、ランドマーク名など) |
*.shx |
図形(shp)に対する属性(dbf)の対応関係 |
*.prf |
投影規格(メルカトル図法など) |
シェープファイルは以下のような地理情報を公開しているwebサイトで公開されているため、必要な地図データをダウンロードして利用することができる。
- 国土交通省(*6)
- Natural Earth(*7)
■ Geo Toolsライブラリの利用方法
Javaでシェープファイルを利用するにはGeoToolsライブラリを利用する。ライセンスはLGPLなので商用利用も可能(
*8)。ライブラリは以下のサイトから最新版がダウンロードできる。
Geo Toolsライブラリをeclipseで利用するには、解凍したフォルダ内からlibフォルダをプロジェクトに複製し、以下のファイルにクラスパスを通す(クラスパスの通し方は
コチラを参照)。するとlibフォルダの他のjarファイルもeclipseに取り込まれ、利用できるようになる。
- gt-shapefile
- gt-swing
Geo Toolsライブラリでは地図上に表示されるオブジェクト1つ1つをfeatureと呼ぶ(
*10)。featureは例えば国を表したり、山を表したりランドマークを表したりする。feature内にはshpファイル内で定義されたシェイプ(ポリゴン)情報と、dbfファイルで定義された属性情報が格納される。JavaFXではこのシェイプと属性を地図上に表示させる。
■ サンプル・プログラム1(地図上で緯度・経度を指定)
以下に、Geo Toolsライブラリを利用して地図を描画するサンプル・プログラムを示す。サンプルでは、別ファイル上に記述された世界の主要都市の緯度経度情報を、シェープファイルから取得した地図上にプロットしている。また、プロットした点にマウスをあてると、都市名が拡大表示される。
◇リソース
- ne_50m_admin_0_countries.shp等(Natural Earth(*7)で公開されている地図ファイル)
- city-country-lat-long.txt(参照11で紹介されている世界の主要都市の緯度経度)
◇サンプルコード
package application;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.opengis.feature.simple.SimpleFeature;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
/**
* SHPファイル表示プログラム
* 「https://github.com/rafalrusin/geotools-fx-test/blob/master/src/geotools/fx/test/GeotoolsFxTest.java」
* で公開されているファイルについて、JavaFX 8 で動作するように修正
* @author karura
*
*/
public class TestGeoMap1 extends Application
{
public static void main(String[] args)
{
launch( args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
Group root = new Group();
Scene scene;
// シーンを作成
primaryStage.setTitle("世界の主要都市");
scene = new Scene(root, 1100, 500, Color.LIGHTBLUE);
// 地図を作成
Group map = createMap( "shape/ne_50m_admin_0_countries.shp" );
Group marks = createMarks( "resource/city-country-lat-long.txt" );
map.getChildren().add( marks );
root.getChildren().addAll( map );
// 初期表示位置とズーム
map.translateXProperty().set(520);
map.translateYProperty().set(300);
map.scaleXProperty().set(3);
map.scaleYProperty().set(-3);
// ウィンドウ表示
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* 地図を表示するノードを作成する
* @param shpFilePath
* @return
* @throws IOException
*/
protected Group createMap( String shpFilePath ) throws IOException
{
// 変数定義
Group map = new Group(); // 戻り値
Group mapShape = new Group(); // 地図の図形データ
// SHPファイルを読込
File file = new File( shpFilePath );
FileDataStore store = FileDataStoreFinder.getDataStore(file);
// ファイルからfeature(地図オブジェクト)を取得
SimpleFeatureSource featureSource = store.getFeatureSource();
SimpleFeatureCollection c = featureSource.getFeatures();
SimpleFeatureIterator featuresIterator = c.features();
// 各featureを画面出力可能なクラスに変更
while ( featuresIterator.hasNext() )
{
// 1つのfeatureの取得
SimpleFeature o = featuresIterator.next();
Object geometry = o.getDefaultGeometry(); // featureが表すポリゴンを取得
// 地理情報が複合ポリゴンの場合のみ処理実行。処理対象のポリゴンを取得
if ( !( geometry instanceof MultiPolygon ) ){ continue; }
MultiPolygon multiPolygon = (MultiPolygon) geometry;
// ポリゴンを画面出力可能なクラスに変換
for ( int geometryI=0 ; geometryI<multiPolygon.getNumGeometries() ; geometryI++ )
{
// 変数の初期化
Geometry polygon = multiPolygon.getGeometryN(geometryI); // ポリゴンの1つを取得
Coordinate[] coords = polygon.getCoordinates(); // ポリゴンの頂点配列を取得
// 頂点をつないでポリゴンを描く
Path path = new Path();
path.setStrokeWidth( 0.05 );
path.setFill( Color.WHITE );
path.getElements().add( new MoveTo(coords[0].x, coords[0].y) );
for (Coordinate p : coords){ path.getElements().add( new LineTo( p.x, p.y ) ); }
path.getElements().add( new LineTo(coords[0].x, coords[0].y) );
// マップに追加
mapShape.getChildren().add(path);
}
}
// 画面に地図を追加
map.getChildren().add( mapShape ); // 地形
// 地図を返す
return map;
}
/**
* マークを付ける緯度経度をファイルから取り込み、表示用ノードにマーカーを置く
* @param filePath
* @return
*/
protected Group createMarks( String filePath )
{
// ルートノードを作成
Group root = new Group();
// ファイルを読込(try-with-resources構文利用)
try( FileInputStream fr = new FileInputStream( filePath );
InputStreamReader ir = new InputStreamReader( fr , "UTF-8" );
BufferedReader br = new BufferedReader( ir ) )
{
// 1行ずつ読み込み処理
String line = null;
while( ( line = br.readLine() ) != null )
{
// ファイルフォーマットの解析
// タブで区切って緯度経度情報を取得
String[] elements = line.split("\t");
String city = elements[0];
String country = elements[1];
double latitude = Double.parseDouble( elements[2] );
double longitude = Double.parseDouble( elements[3] );
// 自分で緯度・経度を画面上の点に座標変換
GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory();
Point point = geometryFactory.createPoint(new Coordinate( longitude , latitude ));
// 点をプロット
Circle c = new Circle();
c.setCenterX( point.getX() );
c.setCenterY( point.getY() );
c.setRadius( 1.0 );
c.setFill( Color.RED );
root.getChildren().add( c );
// 文字
Text t = new Text( city );
t.getTransforms().add(new Translate( point.getX() + 1.0 , point.getY() ));
t.getTransforms().add(new Scale( 1.0 , -1.0 ));
t.setFont( new Font( 3.0 ) );
root.getChildren().add( t );
// 点にマウスオーバーで文字を大きく
c.addEventHandler( MouseEvent.MOUSE_ENTERED , e -> t.setFont( new Font( 6.0 ) ) );
c.addEventHandler( MouseEvent.MOUSE_EXITED , e -> t.setFont( new Font( 3.0 ) ) );
}
} catch (Exception e) {
e.printStackTrace();
}
return root;
}
}
◇実行結果
◇解説
Geo Toolsによるシェープファイルの読み込みはcreateMap関数(85行目~136行目)内で行っている。利用方法はファイルをFileDataStoreに読み込み(93行目)、FeatureSourceクラスの配列として取得(96行目~97行目)、Featureを1つ1つ処理していく(101行目~129行目)。Feature内には属性情報とシェイプが含まれているが、今回はシェイプだけを利用している(109行目~128行目)。
地図へのマーカー記入はcreateMarks関数(143行目~194行目)。地図とは別に緯度・経度を指定してマークを付けるには、JTSFactoryFinder::getFactory関数でfactoryを取得、Factory::createPoint関数にCoordinateクラス(引数は経度・緯度の順)を渡すことでJavaFX上の座標に変換される(166行目~167行目)。
注意する点としては地図がさかさまに表示されるため、地図を描いた後に全体を上下反転している点(71行目~72行目)と、それを見越して文字をあらかじめ逆さに描いている点(180行目)である。
■ サンプル・プログラム2(地図上で国を指定)
以下に、Geo Toolsライブラリを利用して国別の操作を行うサンプルプログラムを示す。サンプルでは、2015年度の日本の主輸出先国名と輸出額(
*12)を表示している。また、ドラッグにより視点が移動し、「+」「-」ボタンによりズーム倍率を変更できる。
◇リソース
- ne_50m_admin_0_countries.shp等(Natural Earth(*7)で公開されている地図ファイル)
◇サンプルコード
package application;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.geotools.data.FileDataStore;
import org.geotools.data.FileDataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.opengis.feature.simple.SimpleFeature;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Bounds;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
/**
* SHPファイル表示プログラム
* 2015年度の日本の主要輸出先への輸出額をプロット
* 「https://github.com/rafalrusin/geotools-fx-test/blob/master/src/geotools/fx/test/GeotoolsFxTest.java」をベースにしている
* @author karura
*
*/
public class TestGeoMap2 extends Application
{
// 定数宣言(輸出国情報)
private final Color[] COLORS = new Color[] { Color.RED, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE, Color.ORANGE };
private final String[] COUNTRIES = { "Japan" , "United States of America" , "China" , "South Korea" , "Taiwan" , "Hong Kong S.A.R." , "Thailand" , "Singapore" , "Germany" , "Australia" , "Vietnam" };
private final long[] EXPORT_VALUES = { 0 , 152246 , 132234 , 53266 , 44725 , 42360 , 33863 , 24026 , 19648 , 15549 , 15164 };
// 変数宣言(マウスドラッグ用の変数)
private double dragBaseX, dragBaseY;
private double dragBase2X, dragBase2Y;
public static void main(String[] args)
{
launch( args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
Group root = new Group();
Scene scene;
// シーンを作成
primaryStage.setTitle( "日本貿易統計:2015年度輸出額上位10ヵ国" );
scene = new Scene(root, 1100, 500, Color.LIGHTBLUE);
// 地図を作成
Group map = createMap( "shape/ne_50m_admin_0_countries.shp" );
root.getChildren().add(map);
// マウスドラッグ時の操作を定義
scene.addEventHandler( MouseEvent.MOUSE_PRESSED , e ->
{
// マウス押下時
scene.setCursor(Cursor.MOVE);
dragBaseX = map.translateXProperty().get();
dragBaseY = map.translateYProperty().get();
dragBase2X = e.getSceneX();
dragBase2Y = e.getSceneY();
});
scene.addEventHandler( MouseEvent.MOUSE_DRAGGED , event ->
{
// マウスドラッグ
map.setTranslateX(dragBaseX + (event.getSceneX()-dragBase2X));
map.setTranslateY(dragBaseY + (event.getSceneY()-dragBase2Y));
});
scene.addEventHandler( MouseEvent.MOUSE_RELEASED , event ->
{
// マウスを放したとき
scene.setCursor(Cursor.DEFAULT);
});
// 操作用ボタンを追加
VBox vbox = new VBox();
Button plus = new Button("+");
Button minus = new Button("-");
vbox.getChildren().add(plus);
vbox.getChildren().add(minus);
root.getChildren().add(vbox);
plus.addEventHandler( ActionEvent.ACTION , event ->
{
double zoom = 1.4;
map.scaleXProperty().set( map.scaleXProperty().get() * zoom );
map.scaleYProperty().set( map.scaleYProperty().get() * zoom );
});
minus.addEventHandler( ActionEvent.ACTION , event ->
{
double zoom = 1.0 / 1.4;
map.scaleXProperty().set( map.scaleXProperty().get() * zoom );
map.scaleYProperty().set( map.scaleYProperty().get() * zoom );
});
// ウィンドウ表示
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* 地図を表示するノードを作成する
* @param shpFilePath
* @return
* @throws IOException
*/
protected Group createMap( String shpFilePath ) throws IOException
{
// 変数定義
Group map = new Group(); // 戻り値
Group mapShape = new Group(); // 地図の図形データ
Group textNotExport = new Group(); // 輸出国以外の国名
Group textExport = new Group(); // 輸出国名
Group lineExport = new Group(); // 輸出国間に引く線
// SHPファイルを読込
File file = new File( shpFilePath );
FileDataStore store = FileDataStoreFinder.getDataStore(file);
// ファイルからfeature(地図オブジェクト)を取得
SimpleFeatureSource featureSource = store.getFeatureSource();
SimpleFeatureCollection c = featureSource.getFeatures();
SimpleFeatureIterator featuresIterator = c.features();
// 各featureを画面出力可能なクラスに変更
Map<String,Point> countryMap = new HashMap<String,Point>();
while ( featuresIterator.hasNext() )
{
// 1つのfeatureの取得
SimpleFeature o = featuresIterator.next();
int countryIndex = 0; // 処理対象が表す国番号
String name = (String) o.getAttribute("sovereignt"); // featureが表す国名を取得
String type = (String) o.getAttribute("type"); // featureが示すタイプ(国(country)や属国(dependency)など)
String subUnit = (String) o.getAttribute("subunit"); // featureが示すサブユニット(地域名)
Object geometry = o.getDefaultGeometry(); // featureが表すポリゴンを取得
// 地理情報が複合ポリゴンの場合のみ処理実行。処理対象のポリゴンを取得
if ( !( geometry instanceof MultiPolygon ) ){ continue; }
MultiPolygon multiPolygon = (MultiPolygon) geometry;
// ポリゴンを画面出力可能なクラスに変換
for ( int geometryI=0 ; geometryI<multiPolygon.getNumGeometries() ; geometryI++ )
{
// 変数の初期化
Geometry polygon = multiPolygon.getGeometryN(geometryI); // ポリゴンの1つを取得
Coordinate[] coords = polygon.getCoordinates(); // ポリゴンの頂点配列を取得
// 地形の塗りつぶし色を定義
java.util.List<String> countries = Arrays.asList( COUNTRIES );
Color currentColor = null;
if( countries.contains( name ) )
{
// 輸出国リストにある場合、国番号と色を取得
countryIndex = countries.indexOf( name );
currentColor = COLORS[ countryIndex ];
}else{
// 輸出国リストにない場合、デフォルトの国番号と色を取得
countryIndex = 0;
currentColor = Color.ALICEBLUE;
}
// 頂点をつないでポリゴンを描く
Path path = new Path();
path.setStrokeWidth(0.05);
path.setFill(currentColor);
path.getElements().add( new MoveTo(coords[0].x, coords[0].y) );
for (Coordinate p : coords){ path.getElements().add( new LineTo( p.x, p.y ) ); }
path.getElements().add( new LineTo(coords[0].x, coords[0].y) );
// マップに追加
mapShape.getChildren().add(path);
}
// 本国以外はテキストを表示しない
// 国を表すfeatureでない場合は、テキスト情報を出力しない。
// 本国名とサブユニット名が異なる場合はテキスト情報を出力しない。(香港だけ特別扱い)
if( type.equalsIgnoreCase( "dependency" ) ){ continue; }
if( ! ( subUnit.equals( name ) || subUnit.equals( "Hong Kong S.A.R." ) ) ){ continue; }
// 国名と輸出額表示ノードを作成
Node node = ( countryIndex == 0 )? new Text( subUnit )
: createExportValue( subUnit , EXPORT_VALUES[ countryIndex ] );
// 国名と輸出額表示ノードの位置を設定
Point centroid = multiPolygon.getCentroid(); // ポリゴンの重心座標を取得
Bounds bounds = node.getBoundsInLocal();
node.getTransforms().add(new Translate(centroid.getX(), centroid.getY()));
node.getTransforms().add(new Scale(0.1,-0.1));
node.getTransforms().add(new Translate(-bounds.getWidth()/2., bounds.getHeight()/2.));
// シーングラフに追加
Group group = ( countryIndex == 0 )? textNotExport : textExport;
group.getChildren().add( node );
// 後で矢印を描くために各国の重心位置を保持
countryMap.put( subUnit , centroid );
}
// 矢印を描く
for( int i=1 ; i<COUNTRIES.length ; i++ )
{
// 位置を取得
Point pFrom = countryMap.get( COUNTRIES[0] );
Point pTo = countryMap.get( COUNTRIES[i] );
if( ( pFrom == null ) || ( pTo == null ) ){ continue; }
// 矢印を記入
Path path = new Path();
path.setStrokeWidth( 0.25 );
path.setStroke( Color.BLACK );
path.setOpacity( 0.5 );
path.getElements().add( new MoveTo( pFrom.getX() , pFrom.getY() ) );
path.getElements().add( new LineTo( pTo.getX() , pTo.getY() ) );
lineExport.getChildren().add( path );
}
// 画面に地図と文字を追加
map.getChildren().add( mapShape ); // 地形
map.getChildren().add( textNotExport ); // 輸出国以外の国名
map.getChildren().add( lineExport ); // 輸出国間の線
map.getChildren().add( textExport ); // 輸出国名
// 初期表示位置とズーム
map.translateXProperty().set(520);
map.translateYProperty().set(300);
map.scaleXProperty().set(3);
map.scaleYProperty().set(-3);
// 地図を返す
return map;
}
/**
* 輸出額を示すテキスト情報を表示するノードを作成
* @param name 国名
* @param exportValue 輸出額
* @return
*/
protected Node createExportValue( String name , long exportValue )
{
// ルートノード
VBox root = new VBox();
// グラフを追加
ProgressIndicator pi = new ProgressIndicator();
long sum = 0;
for( long value : EXPORT_VALUES ){ sum += value; }
pi.setProgress( (double) exportValue / sum );
pi.setPrefSize( 50 , 50 );
root.getChildren().add( pi );
// テキストを追加
Text country = new Text( name );
Text value = new Text( String.format( "( %,3d億円 )" ,exportValue ) );
country.setFill( Color.RED );
value.setFill( Color.RED );
root.getChildren().addAll( country , value );
return root;
}
}
◇実行結果
◇解説
サンプル2ではサンプル1とは異なりFeatureクラス内の属性情報を利用している(156行目~158行目)。属性の名前はシェープファイル作成者が任意で設定できるので、シェープファイル毎に確認する必要がある。今回利用するシェープファイルでは「sovereignt」に国名、「type」に国土の種類、「subunit」に自治州の名前等が記入されている。
利用するシェープファイルの注意点としては、1つの国が1つのFeatureで表されていない点である。触った感じだと1つの閉じた形(日本でいえば北海道・本州・四国・九州等)が1つのfeatureで表されている模様。このため、欧米の国々のように世界各地に領地を持つ国はたくさんのfeatureを持つ。「type」が「dependency」でないfeatureが本国および代表的な領土を表している様子であったため、本プログラムではそのような場所のみ国名を表示している。
また、各Featureが持つシェイプの重心座標はMultiPolygon::getCentroid関数で取得している(209行目)。国名や輸出額および国をつなぐ線の描画にはこの座標を利用している。
■ 参照
- Googleマップ 「Google マップ、Google Earth、ストリートビューの使用」
- Googleマップ 「Google Maps APIs」
- Googleマップ 「Google マップと Google Earth の権利帰属表示に関するガイドライン」
- esriジャパン 「シェープファイルについて」
- PASCO 「シェープファイルとは?」
- 国土交通省 「国土数値情報 ダウンロードサービス」
- Natural Earth 「1:50m Cultural Vectors」
- Geo Tools 「About GeoTools」
- Source forge 「GeoTools, the Java GIS toolkit」
- GeoTools 「Freature Tutorial」
- グーグルアース4日本語版ダウンロード と その使い方 「世界の主要都市」
- 財務省貿易統計 「貿易相手先国上位10カ国の推移:輸出」
- JavaDoc GeoTools
- JavaDoc 「Interface SimpleFeature」
- JavaDoc 「Class SimpleFeatureBuilder」
- Github 「rafalrusin/geotools-fx-test」
- Stack Exchange 「Plot the longitude and latitudes on map using geotools」