PLATEAUのCityGMLからMVTを作成する作業の記録

## 1.概要
インディゴ株式会社さんの下記のエントリを参考に、PLATEAUのCityGMLから建物のMapbox Vector Tile（MVT）を作ってみました。
その作業記録です。


https://github.com/indigo-lab/plateau-tokyo23ku-building-mvt-2020

## 2.CityGMLからGeojsonを生成
PlateauのCityGMLを読み込み、Geojsonを生成するユーティリティを作成しました。
CityGMLの読み込みはcitygml4j、geojson生成はgsonを使用しています。
geojsonは、建物のLOD0FootPrintからgeometryを作成し、GenericAttributeからpropertiesを作成しています。

CiryGMLのフォルダを指定してCityGMLUtilを実行すると、同じフォルダにgeojsonファイルが生成されます。

https://github.com/citygml4j/citygml4j


```java:CityGMLUtil.java

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.citygml4j.CityGMLContext;
import org.citygml4j.builder.jaxb.CityGMLBuilder;
import org.citygml4j.builder.jaxb.CityGMLBuilderException;
import org.citygml4j.model.citygml.CityGML;
import org.citygml4j.model.citygml.CityGMLClass;
import org.citygml4j.model.citygml.building.Building;
import org.citygml4j.model.citygml.core.AbstractCityObject;
import org.citygml4j.model.citygml.core.CityModel;
import org.citygml4j.model.citygml.core.CityObjectMember;
import org.citygml4j.model.citygml.generics.AbstractGenericAttribute;
import org.citygml4j.model.citygml.generics.IntAttribute;
import org.citygml4j.model.citygml.generics.MeasureAttribute;
import org.citygml4j.model.citygml.generics.StringAttribute;
import org.citygml4j.model.gml.geometry.aggregates.MultiSurface;
import org.citygml4j.model.gml.geometry.aggregates.MultiSurfaceProperty;
import org.citygml4j.model.gml.geometry.primitives.DirectPositionList;
import org.citygml4j.model.gml.geometry.primitives.Exterior;
import org.citygml4j.model.gml.geometry.primitives.LinearRing;
import org.citygml4j.model.gml.geometry.primitives.Polygon;
import org.citygml4j.model.gml.geometry.primitives.SurfaceProperty;
import org.citygml4j.xml.io.CityGMLInputFactory;
import org.citygml4j.xml.io.reader.CityGMLReadException;
import org.citygml4j.xml.io.reader.CityGMLReader;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class CityGMLUtil {

	public static void readCityGML(File f,List<Map<String,Object>> list) throws CityGMLBuilderException, CityGMLReadException{
		CityGMLContext ctx = CityGMLContext.getInstance();
		CityGMLBuilder builder = ctx.createCityGMLBuilder();
		CityGMLInputFactory in = builder.createCityGMLInputFactory();
		CityGMLReader reader = in.createCityGMLReader(f);
		while (reader.hasNext()) {
			CityGML citygml = reader.nextFeature();
			if (citygml.getCityGMLClass() == CityGMLClass.CITY_MODEL) {
				CityModel cityModel = (CityModel)citygml;
				for (CityObjectMember cityObjectMember : cityModel.getCityObjectMember()) {
					AbstractCityObject cityObject = cityObjectMember.getCityObject();
					if (cityObject.getCityGMLClass() == CityGMLClass.BUILDING){
						Building b=(Building)cityObject;
						if(b.getMeasuredHeight()!=null){
							list.add(createFeature(b));
						}
					}
				}
			}
		}
		reader.close();
	}

	private static Map<String,Object> createFeature(Building b){
		Map<String,Object> ret=new HashMap<>();
		ret.put("type","Feature");
		Map<String,Object> geom=new HashMap<>();
		Map<String,Object> prop=new HashMap<>();
		ret.put("geometry",geom);
		ret.put("properties",prop);
		geom.put("type","Polygon");
		MultiSurfaceProperty msp=b.getLod0FootPrint();
		MultiSurface ms=msp.getMultiSurface();
		List<SurfaceProperty> spl=ms.getSurfaceMember();
		Polygon pp=(Polygon)spl.get(0).getGeometry();
		Exterior ex=(Exterior)pp.getExterior();
		LinearRing lr=(LinearRing)ex.getRing();
		DirectPositionList dpl=(DirectPositionList)lr.getPosList();
		List<Double> dl=dpl.toList3d();
		List<double[]> tmp=new ArrayList<>();
		double dem=0.0;
		for(int i=0;i<dl.size();i=i+3){
			Double d01=dl.get(i);
			Double d02=dl.get(i+1);
			Double d03=dl.get(i+2);
			tmp.add(new double[]{d02,d01});
			dem=d03;
		}
		List<List<double[]>> c=new ArrayList<>();
		c.add(tmp);

		geom.put("coordinates",c);
		prop.put("measuredHeight",b.getMeasuredHeight().getValue());
		List<AbstractGenericAttribute> ll=b.getGenericAttribute();
		for(AbstractGenericAttribute at : ll){
			if(at instanceof StringAttribute){
				StringAttribute st=(StringAttribute)at;
				prop.put(st.getName(), st.getValue());
			}else if(at instanceof MeasureAttribute){
				MeasureAttribute st=(MeasureAttribute)at;
				prop.put(st.getName(), st.getValue().getValue());
			}else if(at instanceof IntAttribute){
				IntAttribute st=(IntAttribute)at;
				prop.put(st.getName(), st.getValue());
			}
		}
		prop.put("dem", dem);
		return ret;
	}

	public static void main(String[] args){
		File in=new File(args[0]); //CityGMLのディレクトリ
		try {
			Gson gson=new GsonBuilder().setPrettyPrinting().create();
			for(File f : in.listFiles()){
				if(f.isDirectory())continue;
				if(f.getName().toLowerCase().endsWith(".gml")){
					System.out.println(f.getName());
					Map<String,Object> root=new HashMap<>();
					root.put("type", "FeatureCollection");
					List<Map<String,Object>> list=new ArrayList<>();
					root.put("features", list);
					readCityGML(f,list);
					try {
						File out=new File(f.getAbsolutePath().replace(".gml", ".geojson"));
						BufferedWriter bw=new BufferedWriter(new FileWriter(out));
						bw.write(gson.toJson(root));
						bw.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}

		} catch (CityGMLBuilderException e) {
			e.printStackTrace();
		} catch (CityGMLReadException e) {
				e.printStackTrace();
		}
	}
}

```
## 3.Mapbox Vector Tile生成
生成したgeojsonから、tippecanoeを使用してMVTを生成します。

https://github.com/mapbox/tippecanoe

tippecanoeについては、インディゴ株式会社さんの先のエントリの他、[@frogcat](https://qiita.com/frogcat)さんの下記エントリを参考にしました。

https://qiita.com/frogcat/items/dae6492c1fae17a9bc0d

geojsonのディレクトリで下記のコマンドを実行すると、指定したフォルダ（下記ではpbf）にMVTデータ（レイヤー名はbldg）が出力されます。

```shell
tippecanoe --no-tile-compression -ad -an -Z12 -z16 -e ../pbf -l bldg -ai *.geojson
```
## 4.成果物
Plateauの『[3D都市モデル（Project PLATEAU）大阪市（2020年度）](https://www.geospatial.jp/ckan/dataset/plateau-27100-osaka-shi-2020)』をMVTに変換した結果です。
建物外郭線（LOD0）を建物高さで立ち上げたLOD1相当のデータですが、3Dtilesに比べて軽く動作する感じです。
なお、大阪市のMVTタイルの容量は、ズームレベル12～16で、120MBでした。

![Failed to fetch -- ERR_CONTENT_DECODING_FAILED · Issue #8099 · mapbox_mapbox-gl-js - Google Chrome 2021_06_06 19_55_46 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55735/91fef881-7ed1-1fed-79a9-2053f20ebb5e.png)

![plateau-tokyo23ku-building-mvt-2020 - Google Chrome 2021_06_06 22_12_49 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55735/3ad4b921-42a4-a1ae-3afa-04b53c085e23.png)

## 5.最後に
PLATEAUはとても興味深いのですが、3DTilesはジオイドの関係でdeck.gl、maplibre、mapboxでは使いづらかったので、何かうまい方法はないかなと考えていたところでした。
Twitterでインディゴ株式会社さんのGithubを知り、「こんな事ができるんだ」と目からうろこでした。素晴らしい情報をありがとうございます！
試しに大阪市データからMVTを作ってみると、非常に簡単に作れたので、ちょっと色々と試してみたくなりました。
また、[国土基盤情報](https://www.gsi.go.jp/kiban/)の建物外郭線と[兵庫県全域数値地形図](https://www.geospatial.jp/ckan/dataset/2010-2018-hyogo-geo-potal)のようなDSM・DEMがあれば、同じようなLOD1相当の建物データが作れそうに感じました。