理系学生日記

おまえはいつまで学生気分なのか

GeoToolsを使って地図上に座標点・連結線を表示する

GeoToolsで扱う地図上の要素

今日はGeoToolsを使って地図上に点や線を引いてみます。

最初に結果を示しておくと、以下のようになります。 これは、札幌時計台、大通公園、さっぽろテレビ塔を順に直線で結んだものですね。

基礎知識

FeatureとFeatureType

まずは、GeoTools上で点や線がどのような扱いになるのかを知らなければなりません。 GeoToolsでは、マップ上に配置するオブジェクトをFeatureと呼び、Feature interfaceとして提供されます。 それは例えばエベレストであったり、あるいは札幌空港であったりします。

The strict definition is that a feature is something in the real world – a feature of the landscape - Mt Everest, the Eiffel Tower, or even something that moves around like your great aunt Alice.

Feature Tutorial — GeoTools 29-SNAPSHOT User Guide

一方で、札幌空港と羽田空港という明らかに共通のフィールドを持つオブジェクトを個別のFeatureとして定義するのは無駄です。そのため、GeoToolsではこれらをグループ化できるようFeatureTypeという概念が存在しています。JavaのオブジェクトシステムにおいてはClassに相当するものですね。 GeoTools上ではFeatureType interfaceとして提供されます。

これらが持つ情報というのはもちろん多岐に及び、Feature同士の関連性を持たせたり、FeatureとFeatureTypeとの関連性を持たせたりも可能です。 そしてFeatureやFeatureTypeはそのための柔軟性を持ち合わせているわけですが、柔軟すぎるものは扱いづらい。 このため、GeoToolsはシンプルな情報のみを持つFeatureやFeatureTypeとして、それぞれSimpleFeatureSimpleFeatureTypeが提供されています。

具体的なFeatureType、Featureの生成

では具体的にどうやってそれらを生成するのか。このデータ周りのメソッドはDataUtilitiesクラスにまとまっています。

例えば単純な線を地図上に引きたい場合、まずはそのSimpleFeatureTypeを以下のようにして生成します。

  // Lineという名前で「線」のFeatureTypeを作成
  SimpleFeatureType lineStringType = DataUtilities.createType("Line", "geometry:LineString");

第二引数に指定しているのはSimpleFeatureがどのような情報を持つのかを示すtypeSpecというフィールドです。JavaにおけるPropertyDescriptorに似たようなものでしょうか。

保持するフィールドの型をname:Type:hintという形式で指定します。複数のフィールドを持つ場合は,区切りです。 上記の場合はgeometryというフィールドの型がLineStringであることを宣言しています。

Featureがgeometryという情報を持つことは前提になっています。これはOpen Geospatial ConsortiumのSimple Feature Accessの仕様あたりで定められているようです。 ぼく自身はそこまでは確認できていませんが。 また、このLineStringはどこから来ているのかというと、おそらくはwikipedia:Well-known textからのようですね。

次に、SimpleFeatureTypeからSimpleFeatureを生成するには、同じDataUtilitiesのtemplateメソッドを使います。これにより、空のfeatureが作れます。

  SimpleFeature feature = DataUtilities.template(lineStringType);

FeatureTypeの描画

GeoToolsでFeatureを描画する際、理解が必要となるのが以下の3 interfaceです。

これらの関係を示す図をStyling features for display — GeoTools 30-SNAPSHOT User Guideから引用します。

名前が示す通り、FeatureTypeStyleはFeatureTypeを描画する際のレンダリング情報(Style)を提供するinterfaceです。 FeatureTypeStyleは、以下の情報を基にしてFeatureTypeを描画します。

  • いつ描画するか (Rule)
  • どのように描画するか (Symbolizer)

FeatureTypeStyleに対してRuleは1:n、Ruleに対してSymbolizerは1:nです。 FeatureTypeStyleが任意のFeatureTypeを描画する際、当該FeatureTypeは保持するすべてのRuleに渡されます。 そして、そのRuleで描画が決まると当該Ruleが保持する全Symbolizerによって描画されます。

Ruleは、FeatureTypeを描画するか否かを制御します。 例えば、どの表示倍率であれば描画するか、事前に描画されていないFeatureのみを描画するか等です。

図にすると以下のようなイメージです。

実装

package com.kiririmode;

import org.geotools.data.DataUtilities;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.SchemaException;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.FeatureLayer;
import org.geotools.map.MapContent;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.styling.*;
import org.geotools.swing.JMapFrame;
import org.geotools.tile.TileService;
import org.geotools.tile.impl.osm.OSMService;
import org.geotools.tile.util.TileLayer;
import org.locationtech.jts.geom.*;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import java.awt.*;
import java.util.Arrays;
import java.util.function.Function;

public class OsmMapFrame {
  public static void main(String args[]) throws SchemaException {
    String baseURL = "https://tile.openstreetmap.org/";
    TileService service = new OSMService("OSM", baseURL);

    MapContent map = new MapContent();
    map.addLayer(new TileLayer(service));

    Coordinate[] coordinates = new Coordinate[] {
        new Coordinate(141.354496, 43.062096),  // 札幌時計台
        new Coordinate(141.356246, 43.055527),  // 大通公園
        new Coordinate(141.356882, 43.068624)   // さっぽろテレビ塔
    };

    FeatureLayer lineLayer = createLineLayer("Line", "geometry:LineString", coordinates,
        styleBuilder -> styleBuilder.createLineSymbolizer(styleBuilder.createStroke(Color.BLUE, 2.0)));
    map.addLayer(lineLayer);

    FeatureLayer pointLayer = createPointLayer("Point", "geometry:Point", coordinates,
        styleBuilder -> styleBuilder.createPointSymbolizer(
            styleBuilder.createGraphic(null,
                styleBuilder.createMark(StyleBuilder.MARK_CIRCLE, new Color(0,0,0,0), Color.BLUE, 1.0), null, 1,10, 0)));
    map.addLayer(pointLayer);

    setBound(map, coordinates);
    JMapFrame.showMap(map);
  }

  private static void setBound(MapContent map, Coordinate[] coordinates) {
    double minLat = Arrays.stream(coordinates).mapToDouble(c -> c.y).min().orElseThrow(IllegalStateException::new);
    double maxLat = Arrays.stream(coordinates).mapToDouble(c -> c.y).max().orElseThrow(IllegalStateException::new);
    double minLon = Arrays.stream(coordinates).mapToDouble(c -> c.x).min().orElseThrow(IllegalStateException::new);
    double maxLon = Arrays.stream(coordinates).mapToDouble(c -> c.x).max().orElseThrow(IllegalStateException::new);

    double offset = 0.001;
    map.getViewport().setBounds(new ReferencedEnvelope(minLon - offset, maxLon + offset, minLat - offset, maxLat + offset, DefaultGeographicCRS.WGS84));
  }

  private static FeatureLayer createLineLayer(String typeName, String typeSpec, Coordinate[] coordinates, Function<StyleBuilder, Symbolizer> symbolizerCreator) throws SchemaException {
    SimpleFeatureType type = DataUtilities.createType(typeName, typeSpec);
    DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();

    SimpleFeature feature = DataUtilities.template(type);
    feature.setDefaultGeometry(new GeometryFactory().createLineString(coordinates));
    featureCollection.add(feature);
    return getFeatureLayer(typeName, symbolizerCreator, featureCollection);
  }

  private static FeatureLayer createPointLayer(String typeName, String typeSpec, Coordinate[] coordinates, Function<StyleBuilder, Symbolizer> symbolizerCreator) throws SchemaException {
    SimpleFeatureType type = DataUtilities.createType(typeName, typeSpec);
    DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();

    for (Coordinate coordinate : coordinates) {
      SimpleFeature feature = DataUtilities.template(type);
      feature.setDefaultGeometry(new GeometryFactory().createPoint(coordinate));
      featureCollection.add(feature);
    }

    return getFeatureLayer(typeName, symbolizerCreator, featureCollection);
  }

  private static FeatureLayer getFeatureLayer(String typeName, Function<StyleBuilder, Symbolizer> symbolizerCreator, DefaultFeatureCollection featureCollection) {
    StyleBuilder styleBuilder = new StyleBuilder();
    Rule rule = styleBuilder.createRule(symbolizerCreator.apply(styleBuilder));
    FeatureTypeStyle fts = styleBuilder.createFeatureTypeStyle(typeName, rule);
    Style style = styleBuilder.createStyle();
    style.featureTypeStyles().add(fts);

    return new FeatureLayer(featureCollection, style);
  }
}