コンテンツにスキップ

Elevation Service 設計書

本ドキュメントは、AGL(対地高度)と緯度経度を入力にWGS84楕円体高・AMSL(標高)を求める内部 コンポーネント(以下「Elevation Service」)の設計を記録する。対象は次のコード:

  • domain.port.AltitudeConverter(Port) — AltitudeConverter.java
  • infrastructure.elevation 配下の実装一式
  • config.ElevationProperties / config.ElevationConfig(設定・Bean定義)

Elevation Serviceは特定のドメインAPIではなく、飛行計画領域(FLIGHT_PLAN_AREAの高度換算結果を 保持する列)・空域制限・将来のテレメトリなど、高度をAGL/WGS84両基準で扱う複数ドメインから参照される 内部コンポーネントである(docs/geospatial/design/geospatial-api-design.md 5.1節のELEVATIONボックス参照)。 そのため設計ドキュメントもdocs/geospatial/から独立させ、docs/elevation/配下に置く (.claude/rules/design-docs.mdのドメイン区分)。

対象外: 飛行計画領域・空域制限が高度換算の結果をどのカラムに保持するか、応答時にどう使うかは それぞれのドメインの設計ドキュメント(docs/data-model/flight-plan-er.mddocs/geospatial/design/geospatial-api-design.md)の管轄であり、本書では扱わない。

  • データソース: 地表標高はGSJ(産業技術総合研究所 地質調査総合センター)が提供する統合DEM (シームレス標高タイル、事前にGeoTIFFへ変換したもの)を採用する。統合DEMは国土地理院の標高タイル (基盤地図情報数値標高モデル)・ASTER GDEM・GEBCO Grid等を統合した合成データセット (https://tiles.gsj.jp/tiles/elev/tiles.html「統合DEM (Mixed)」の出典表)。ジオイド高は 国土地理院「ジオイド2024日本とその周辺」(JPGEO2024、ISG形式)を採用する。旧モデル(GSIGEO2011)は 使用しない(JpGeoid2024HeightProvider.java:7-11)。 統合DEMを構成する各データセット(国土地理院・ASTER GDEM・GEBCO)の利用条件はそれぞれ個別に 定められており、本PR時点では確認していないdocs/data-model/followups.mdに計上済み)
  • 段階展開: DEMは当面、全国の一部地域のみをカバーする小さいデータセットから始め、段階的に 全国データへ拡張する運用とする(ElevationProperties.java:32-35
  • アーキテクチャ: 別マイクロサービスへ切り出さず、utm-backend内の独立コンポーネントとする。 DEM・ジオイドファイルは起動時に1回だけ読み込みプロセス内メモリに保持し、リクエストごとの 外部I/O・ファイル読込は発生しない設計とした(ADR-022「外部統合の状態遷移」を踏まえ docs/implementation-guide.mdが定める「トランザクションの中で外部I/Oを呼ばない」規約 (implementation-guide.md:122、禁止事項としての再掲は 同:264)に抵触しない構成にするための 決定でもある)
  • 測地系(CRS)の前提: 統合DEMの構成要素である国土地理院の標高タイルは日本測地系(JGD2011)の 座標を基準に作成されているが、統合DEM全体(ASTER GDEM・GEBCO Gridを含む合成データセット)としての 測地系がJGD2011に統一されているかは出典ページでは明言されておらず未確認である。本コンポーネントは いずれの区別もせずWGS84と同一視して扱う(両者の差はcm〜dm オーダーで、対地高度の参考値換算 というこの用途の精度要件では無視できるという判断。GsiDemTerrainElevationProvider.java:84-85IsgGridModel側もジオイド高の座標系変換は行わず緯度経度をそのままグリッド座標として扱う)。より 高精度な要件が出た場合は、この前提を見直す必要がある

正式な設計判断の記録(ADR化)は未着手: 上記データソース選定はutm-backend側の実装として先行して 決定・反映したが、正の記録先であるutm-design-docs/docs/data-model/data-model.mdへの反映とADRの新設は 別リポジトリ側の対応が必要で、本書執筆時点では未着手(followups.mdに 種別実体未作成で登録済み)。

flowchart TB
    subgraph domain["domain.port(ヘキサゴナルのPort)"]
        Port["AltitudeConverter «interface»<br/>aglToEllipsoidalHeightMeters / aglToAmslMeters<br/>ellipsoidalHeightToAglMeters / ellipsoidalHeightToAmslMeters<br/>いずれも(LonLat, 高度) : Optional#60;Double#62;"]
        Pos["LonLat «record»<br/>(経度・緯度。値域・NaN・Infinityを検証する値オブジェクト)"]
    end
    subgraph infra["infrastructure.elevation(Secondary Adapter)"]
        Impl["AltitudeConverterImpl<br/>(Portの実装。入力検証・合成)"]
        TerrainSrc["TerrainElevationSource «interface»<br/>(パッケージ内部専用)"]
        GeoidSrc["GeoidHeightSource «interface»<br/>(パッケージ内部専用)"]
        Fallback["FallbackTerrainElevationSource<br/>(優先順位順に問い合わせ、最初の有限値を採用)"]
        DemProvider["GsiDemTerrainElevationProvider<br/>(GeoTools/GeoTiffReaderでDEM読込、1ファイル1インスタンス)"]
        GeoidProvider["JpGeoid2024HeightProvider<br/>(ISG読込・基準面補正合成)"]
        IsgModel["IsgGridModel<br/>(ISGデータ部の読込・双線形補間。<br/>ヘッダ解析はIsgHeader)"]
    end
    subgraph config["config(Spring Bean定義)"]
        Props["ElevationProperties<br/>(@ConfigurationProperties(#34;elevation#34;))"]
        Cfg["ElevationConfig<br/>(enabled=trueなら実装、それ以外はNullObjectを登録)"]
    end

    Impl -.implements.-> Port
    Port --> Pos
    Impl --> TerrainSrc
    Impl --> GeoidSrc
    Fallback -.implements.-> TerrainSrc
    Fallback -->|優先順位順に複数保持| DemProvider
    DemProvider -.implements.-> TerrainSrc
    GeoidProvider -.implements.-> GeoidSrc
    GeoidProvider --> IsgModel
    Cfg -->|起動時に1回組み立て| Impl
    Cfg --> Props
  • Portは地点をLonLat (経度・緯度の値オブジェクト)で受け取る。緯度経度を素のdoubleで並べると引数の取り違えを 型で防げず、値域の検証も呼び出しごとに要るため(.claude/rules/java/main.md「値オブジェクト」)。 LonLatは元々模擬DIPS連携(DipsApiClient)向けに導入された型だが、GeoJSON/PostGIS向けの GeoPositionとは異なりコンパクトコンストラクタで値域・NaN・Infinityのすべてを検証する (GeoPositionは値域比較の前にDouble.isFiniteを通さないためNaNが素通りする。 docs/data-model/followups.mdの台帳に登録済みの既知のギャップ)。ElevationはGeoJSON表現にも DIPS連携にも属さない汎用の緯度経度を扱うため、より安全なLonLatを採用する(レビュー指摘で この選択に変更した)。LonLat自身の不変条件を直接検証するテストクラスがそれまで無かったため、 この変更にあわせてLonLatTestを新設した。パッケージ内部のTerrainElevationSourceGeoidHeightSourceも同じ型で受け取り、緯度経度の分解はGeoTools・ISGグリッドを呼ぶ最終段だけで行う
  • 入力検証はLonLatのコンパクトコンストラクタに一本化されている。値域(経度±180度・緯度±90度)・ NaN・Infinityのすべてをそこで検証するため、AltitudeConverterImplIsgGridModel側で座標の 有限性を別途検証する必要はない(高度の有限性はAltitudeConverterImplが引き続き検証する)
  • TerrainElevationSourceGeoidHeightSourceはパッケージ内部専用のインターフェースで、domain.portには 昇格させていない(消費者がAltitudeConverterImplしかいないため。将来DEM単体・ジオイド単体を 別usecaseが直接使う要求が出たら、その時にdomain.portへ切り出す) TerrainElevationSource.java:5-8GeoidHeightSourceは「TerrainElevationSourceと同様に昇格させない」とだけ書いており、 切り出し条件そのものはTerrainElevationSource側の記載を参照する形になっている (GeoidHeightSource.java:5-6
  • GsiDemTerrainElevationProviderJpGeoid2024HeightProviderIsgGridModelFallbackTerrainElevationSourceは いずれもパッケージ非公開(package-private)で、外部からはAltitudeConverterImpl.create(...)の 静的ファクトリメソッド経由でのみ組み立てる AltitudeConverterImpl.java
  • DEMは複数ファイルを優先順位順に指定でき、先頭のファイルにデータが無い地点は次点ファイルへ フォールバックする(一部地域は高解像度DEM、それ以外は低解像度DEMという構成を想定。 FallbackTerrainElevationSource.java、 6.3節参照)。ファイルが1件だけの場合はFallbackTerrainElevationSourceを経由せず GsiDemTerrainElevationProviderを直接使う(AltitudeConverterImpl.java:77-80
AMSL = 地表標高(DEM) + AGL
WGS84楕円体高 = AMSL + ジオイド高(JPGEO2024, 基準面補正込み)

変換はAMSLを中継し、AGL起点・楕円体高起点の両方向を提供する。 メソッド名は <入力の基準>To<出力の基準>Metersで揃えている。

用途入力メソッド必要なデータ
飛行計画・空域制限(ユーザー入力がAGL)AGLaglToAmslMetersDEM
同上AGLaglToEllipsoidalHeightMetersDEM+ジオイド
テレメトリ(Remote ID入力が測地高度)楕円体高ellipsoidalHeightToAglMetersDEM+ジオイド
同上楕円体高ellipsoidalHeightToAmslMetersジオイド

テレメトリでは対地高度のほうが地表標高データによる導出値になる(geospatial-api-design.mdの 「テレメトリもAGL/WGS84の両基準を提供する」。TELEMETRY.altitude_agl_mがこの導出値)。 DEM参照に失敗して対地高度が得られない場合の応答方針は同ドキュメントが定める。

AltitudeConverterImpl.javaが この合成を行う。AGLは地表(0)・空中(正)・地中(負)のいずれの値でも有限の実数であれば受け付ける (緯度・経度の範囲チェックのみ行い、AGLの符号や大きさに制約は設けていない)。

AMSLはジオイドモデルを参照しない。 GSJ統合DEMが持つ地表標高そのものがジオイド基準の標高 であるため、AGLを足したものがそのままAMSLになる。結果として、基準面補正が未確定でWGS84楕円体高が 求まらない地点(4.5節)でもAMSLは返せる。

AMSLは参考値である。機能要求5.1.3.2が定めるシステム内の標高基準は楕円体高で、AMSLは表示・参考の ために併記する値という位置づけ(geospatial-api-design.mdの「AMSL/WGS84高度の明確な分離」。旧APIの altitude_amslが実体はWGS84楕円体高だった経緯があるため、どちらの基準かを明示して扱う)。誤差の 性質はWGS84側と同じで、9章のとおり安全判断には使えない(AMSLの誤差はDEMの誤差そのもの)。

DEM・ジオイドいずれかのデータが当該地点をカバーしていない場合、変換できないことが正常系として 起こりうる(特にDEMは当面一部地域のみのデータセットのため)。異常系の例外ではなく Optional.empty()で表す方針とした(docs/implementation-guide.mdのポート契約規約に合わせる。 AltitudeConverter.java:25)。 空になる条件は必要なデータによって異なる。aglToAmslMetersはDEMのみ、 ellipsoidalHeightToAmslMetersはジオイドのみに依存するため、片方が未整備でも一方は返せる (例: 基準面補正が未確定の地点ではAGL起点のAMSLは求まるが楕円体高は求まらない。DEM未整備の地点では 楕円体高起点のAMSLは求まるがAGLは求まらない)。

起動時の失敗は非検査例外ElevationDataExceptionに統一する

Section titled “起動時の失敗は非検査例外ElevationDataExceptionに統一する”

DEM・ジオイドの読み込み(AltitudeConverterImpl.create(...)、起動時にBean生成の中で1回だけ呼ぶ)が 失敗する経路はdomain.exception.ElevationDataExceptionRuntimeExceptionのサブクラス)だけを 投げる。JDK・GeoToolsが投げる検査例外(IOExceptionとそのサブクラス)は、発生元(IsgHeaderIsgGridModelGsiDemTerrainElevationProviderAltitudeConverterImpl)でこの型へ詰め替えてから 伝播させる(.claude/rules/java/main.md「すべて非チェック例外にする」)。ファイルの代わりに ディレクトリを渡すテストで、検査例外を詰め替える経路そのものを実I/Oで踏んでいる。

GsiDemTerrainElevationProviderはSpringの既定スコープ(シングルトン)で1インスタンスだけ生成され、 複数リクエストが同時に来ると同じGridCoverage2D(GeoTools提供のDEMラスタ表現)へ複数スレッドから 並行してevaluate(...)が呼ばれる。IsgGridModelJpGeoid2024HeightProviderは不変なfloat[]への 読み取りのみで構成上スレッドセーフだが、GridCoverage2D側はJAI(ImageN)のタイル読込・キャッシュ機構を 経由するため、設計判断として明示しておく。

  • GridCoverage2D.evaluate(...)自体に排他制御(synchronized)は無く、内部で共有される可変フィールドへ 書き込みながら計算する実装にもなっていない(バイトコード確認済み。ローカル変数のみで計算しPlanarImage へ委譲する)。GeoServer等、同一のGridCoverage2Dインスタンスへ多数の同時リクエストを流す運用実績が ある構成であるため、安全と判断する
  • 上記の判断を固定するため、GsiDemTerrainElevationProviderConcurrencyTestで複数スレッドから同一 インスタンスへ同時に問い合わせても値が取り違わないことを検証する(8.2節) (GsiDemTerrainElevationProviderConcurrencyTest.java
  • 座標変換に使う一時オブジェクトは呼び出しごとにローカルへ確保するThreadLocalで使い回す案は 採らない。spring.threads.virtual.enabled=trueapplication.yaml)では仮想スレッドがリクエスト ごとに生成・破棄されるためリクエストをまたいだ再利用にならず、マップ参照のコストだけが増える。 本変換が走るのは高度を持つレコードの作成・更新時(飛行計画の作成・更新、他Operatorの飛行計画・ 飛行禁止エリアの収集、テレメトリ受信)に限られ、多数を並行処理する想定も現時点で無い

DEM(GeoTIFF)は、範囲内であってもデータが無い画素(NoData)を持ちうる(docker/elevation-data-prepgdal_translate -a_nodata -9999で無効値-9999を記録する。6.2節が報告する「23区データのうち一部 タイルは取得できずスキップ」もこのケースに該当し得る)。

GeoToolsのGridCoverage2D.evaluate(...)は、カバー範囲外の地点や座標変換できない地点では例外 (CannotEvaluateExceptionTransformException)を投げるが、カバー範囲内のNoData画素は例外に ならず、GeoTIFFに記録された無効値がそのまま戻り値として返ってくる(合成GeoTIFFで実験し確認済み)。 これを素通しすると、-9999という明らかに異常な値が「実際の地表標高」としてAltitudeConverterImpl の計算にそのまま使われてしまい、Optional.empty()にもならず、FallbackTerrainElevationSourceによる 次点DEMへの切り替えも発生しない。

そのためGsiDemTerrainElevationProviderは次の3段で無効値を判定し、該当すればDouble.NaNへ変換する (GsiDemTerrainElevationProvider.java)。 回帰テストはGsiDemTerrainElevationProviderTest参照(8.2節)。

  1. GeoTIFFが宣言するNoData値(GridSampleDimension.getNoDataValues())との完全一致
  2. 生成パイプラインの既定無効値-9999との完全一致(1が認識できないGeoTIFFでも取りこぼさないため)
  3. 実在しえない標高(-20000m以下)である場合の保険

閾値に-9999「以下」を使ってはいけない。このDEMは海陸シームレスで、伊豆・小笠原海溝付近では 実在する水深を持つ(実測値と閾値の根拠はGsiDemTerrainElevationProviderIMPOSSIBLE_ELEVATION_METERSのコメント)。対象範囲(bbox)を南へ広げれば-9999より深い 本物の水深が現れる。「-9999以下は 無効値」としてしまうと、本物の水深を「データ無し」と誤判定して次点DEMへ静かにフォールバックする。

生成パイプライン側のセンチネル値(-a_nodata -9999)自体は変更しない。 マリアナ海溝相当 (約-9,800m〜-10,900m)の実測値は-9999とわずか200m差であり、範囲をそこまで広げると理論上 「本物の水深がちょうど-9999.0と一致する」取り違えが起こりうる。しかし判定は-9999との 完全一致(浮動小数点の桁まで一致する必要がある)であり、対象範囲を全国10mメッシュ化しても 現実装のカバー範囲(3.6節・6.2節)はマリアナ海溝を含まないため、実害は無い。-32768等への 変更は生成済みDEMの再生成を要するため、全国展開でマリアナ海溝級の海域を対象に含める判断が 出た時点で、生成パイプラインとDEM側の閾値(IMPOSSIBLE_ELEVATION_METERS)をあわせて見直す。

なお、GeoTIFFにNoDataの宣言が無い場合は起動時にWARNログを出す(上記2・3のみが頼りになるため、 別の無効値を使うGeoTIFFを持ち込んだ場合に気づけるようにする)。DEMの読み込み結果(ファイル・ カバー範囲・NoData宣言・所要時間)は起動時にINFOログへ出す。

GridCoverage2D.evaluate(...)は補間を行わず、求点を含む画素の値をそのまま返す(最近傍)。 ジオイド高(IsgGridModel、4.3節)が双線形補間するのとは異なる点に注意する。

そのため、1kmメッシュのフォールバックDEMでは最大500m程度水平にずれた地点の標高が返り、地形が 階段状になる。9章「既知の制限」の解像度限界は、データの解像度そのものに加えてこの参照方式に 起因する分も含む。DEM側も双線形補間する選択肢はあるが、その場合は補間に使う4点のうち一部が NoDataだったときの扱い(3.5節)を別途設計する必要があるため、現時点では採用していない。

4. データ形式で判明した実データ仕様

Section titled “4. データ形式で判明した実データ仕様”

実際のJPGEO2024.isg・GSJシームレス標高タイルから変換したGeoTIFFで検証する中で、想定と異なる 実データ特有の仕様が6点見つかった。同じ実装を今後行う際に必ず踏む可能性が高い点のため、根拠 (国土地理院公式マニュアルmanual2024.pdf「ジオイド2024日本とその周辺 基準面補正パラメータ 説明書」 令和7年4月、およびGSJタイル仕様https://tiles.gsj.jp/tiles/elev/tiles.html)とあわせて記録する。

4.1 ISGヘッダの座標項目はDMS表記

Section titled “4.1 ISGヘッダの座標項目はDMS表記”

lat minlat maxlon minlon maxdelta latdelta lonは10進度ではなく、度分秒(DMS、 例: 15°00'00")で記載されている。IsgGridModelはDMS・10進度の両方をパースする (IsgHeader.java:35-38、 DMSパターンの定義)。

4.2 ヘッダ区切り行に装飾記号が続く

Section titled “4.2 ヘッダ区切り行に装飾記号が続く”

begin_of_headend_of_headの後ろに====...のような装飾が続く (例: begin_of_head ================================================)。完全一致ではなく前方一致で 判定する(IsgHeader.java:81-90)。

4.3 双線形補間: 格子線上の求点は「該当する」格子点のみで無効値判定する

Section titled “4.3 双線形補間: 格子線上の求点は「該当する」格子点のみで無効値判定する”

公式マニュアル3(2)※6は次のように定める(原文引用):

求点が格子点直上、又は、緯度方向若しくは経度方向に隣合う格子点を結ぶ線分上に位置する場合は、 該当する格子点の数値が”-9999.0000”の時に”NaN”となります。

つまり、求点が格子線上に乗る場合は実際に使う格子点(1点または2点)だけが無効値判定の対象で、 使わない対角の格子点が無効値でも結果に影響してはならない。IsgGridModel.interpolateは行・列 それぞれについて「格子線上かどうか」をPOSITION_EPSILONで判定し、使わない格子点を補間対象から 除外する (IsgGridModel.java:28-31POSITION_EPSILONと、同:220-228onRowLine/onColLineによる格子点の決定)。

4.4 データ部の並び順はN-to-S, W-to-Eを前提とし、宣言が異なれば読まない

Section titled “4.4 データ部の並び順はN-to-S, W-to-Eを前提とし、宣言が異なれば読まない”

データ部は緯度降順・経度昇順(N-to-S, W-to-E)で並ぶ前提で、行0をlat maxとして読む。 JPGEO2024・Hrefconv2024の実ファイルはこの並びで、ヘッダのdata orderingにもそう宣言されている。

ISG 2.0はS-to-Nも許すが、その場合範囲・間隔・格子数はすべて一致するため既存のガードを素通りし、 上下反転した格子として黙って読み込まれる(緯度35度の問い合わせに緯度30度の値が返り、数mの誤差に なる)。nrows/ncolsと同じ方針で、data orderingN-to-S, W-to-E以外を宣言していれば IOExceptionにする(IsgHeader)。項目が無いファイル(ISG 1.0等)は前提どおりとみなす。

4.5 基準面補正(Hrefconv2024)は全国同一グリッドで、未確定地点はNaNを伝播する

Section titled “4.5 基準面補正(Hrefconv2024)は全国同一グリッドで、未確定地点はNaNを伝播する”

「基準面補正パラメータ」(Hrefconv2024.isg)は「ジオイド2024日本とその周辺」(JPGEO2024.isg)と 同一の全国グリッド(緯度・経度の範囲・間隔・格子数)を持つ。日本水準原点を基準面とする範囲 (ほぼ全国)には補正量0.0000が明示的に入っており、基準面が異なる一部離島(吐噶喇列島以南・ 八丈島以南)のみ実際の補正値を持つ。補正が未確定の格子点は無効値-9999.0000で、公式の 「統合したファイル」(ジオイド高と基準面補正量を事前に足し合わせたもの)も同じ格子点で無効値に なる(マニュアル2章(1)「isgファイルの概要」)。

このためJpGeoid2024HeightProviderは、補正値がNaN(無効値または未設定)の地点を「補正なし」に フォールバックさせず、NaNをそのまま伝播させる(公式の統合ファイルの仕様に合わせる。 クラスjavadocの根拠はJpGeoid2024HeightProvider.java:13-18、 伝播の実装はJpGeoid2024HeightProvider.java:40-42)。

4.6 GSJタイルの無効値は「透明色の宣言」であり、アルファチャンネルではない

Section titled “4.6 GSJタイルの無効値は「透明色の宣言」であり、アルファチャンネルではない”

GSJシームレス標高タイルの仕様(https://tiles.gsj.jp/tiles/elev/tiles.html)は無効値について 次のように定める(原文引用):

無効値には透明の画素が書き込まれています。

この「透明」の実装方法が重要である。実タイルを取得して確認したところ、アルファチャンネル付きの PNG(RGBA)ではなく、tRNSチャンクでRGB値(128,0,0)0x800000を透明色として宣言する方式 だった(実測: z=7/50/113・z=8の3タイル・z=0/1/2の各タイルすべてでcolortype=2(RGB、アルファ チャンネルなし)かつtRNS=0x800000。z=0/0/0には実際に0x800000の画素が73個存在した)。

そのためconvert_gsj_tiles.pyimage.convert('RGB')は**tRNSのメタデータを落とすだけで、画素の RGB値0x800000は保持される**。同スクリプトの無効値判定elevation[encoded == 2**23]2**23 = 0x800000)がこの画素を正しく捕捉するため、変換パイプラインは仕様どおりに動作している

0x800000は、この標高エンコード(r' = r-256 (r≧128)h = (65536r' + 256g + b) × 0.01)における 符号付き24bitの最小値(-83886.08m)にあたり、地理院タイルと同じ無効値の慣習である。アルファを 落とすことで無効値が標高0.00m(海面)として扱われる懸念はない(レビューで指摘されたため実タイルで 検証した。検証は本PRのレビュー過程で実施したもので、再現可能なテストとしては残していない)。

データファイルの読み込みに時間がかかる。変換自体は速い。

項目実測
ファイル読み込み・初期化(AltitudeConverterImpl.create約3,900 ms
初回変換約140 ms
以降の変換1回あたり約0.026 ms

(JDK 25・東京23区10m 33MB+全国1km 27MB+JPGEO2024.isg 36MB。測定は ElevationStartupCostTest。 環境変数ELEVATION_DATA_DIRにデータの置き場所を指定したときだけ走り、未設定ならスキップする)

初期化が重いのはGeoToolsの初期化と、ISG形式(テキスト)36MBの解析が主である。ファイルサイズ自体より この処理が支配的で、常駐プロセスでは起動時の一度きりだが、起動と終了を繰り返す実行環境では毎回かかる。 テレメトリ取り込みのLambdaへ同梱する判断(telemetry-er.md)は この値を根拠にしている。

一方、変換そのものは1回0.026msで、1Hz・100機規模でも負荷にならない。

elevation.*config.ElevationProperties)で制御する。

プロパティ説明既定値
elevation.enabled変換機能の有効/無効。falseの場合もBeanは登録するが、常にOptional.empty()を返すNullObjectになる(下記参照)false
elevation.dem-geo-tiff-pathsGSJ統合DEMを事前変換したGeoTIFFファイルのパスを優先順位順(先頭が最優先)に並べたもの。1件でもよい。先頭のファイルにデータが無い地点は次のファイルへフォールバックする(3.1節・6.3節参照)空リスト
elevation.geoid-pathJPGEO2024(ISG形式)のジオイド高ファイルのパス
elevation.correction-path一部離島向けの基準面補正ファイル(Hrefconv2024相当、ISG形式)のパス。空なら補正なし(ジオイド高のみを使う)。enabled=trueでも任意

application.yaml:187-198ElevationProperties.java:32-47

enabled=trueのときは必須項目をバインド時に検証する。 dem-geo-tiff-pathsが空・空要素を含む、 またはgeoid-pathが空文字なら、設定キー名と環境変数名を添えたIllegalArgumentExceptionで起動を 失敗させる。Path.of("")は例外にならないため、弾かないと読込時にメッセージが空の NoSuchFileExceptionになり原因が分からない。enabled=false(既定)ではファイル未配置の ローカル開発を妨げないよう空を許す(correction-pathenabledに関わらず任意)。

compose.yamlでは4項目すべてをホストの環境変数で上書きできる。ELEVATION_CORRECTION_PATHだけは 既定値の適用を「未設定のときだけ」にしてあり(${VAR-既定値})、ELEVATION_CORRECTION_PATH=(空)を 明示すれば補正なしで起動できる。補正ファイルを置かない環境でcompose.yamlを編集せずに済ませるため。

dem-geo-tiff-pathsは環境変数からはカンマ区切りの1本の文字列で渡す(Spring Bootの緩やかな バインディングによりList<String>へ変換される。動作は ElevationPropertiesBindingTest.java で確認済み)。

elevation.enabled=falseが既定なのは、DEM・ジオイドのデータファイルが未配置のローカル開発環境等で 起動を妨げないため(DipsMqttPropertiesと同じ考え方)。trueにした場合、ElevationConfig@Bean メソッドが起動時にファイルを読み込み、失敗すればアプリの起動自体を失敗させる(フェイルファスト。 ElevationConfig.java)。

enabledの値に関わらずAltitudeConverterのBeanは必ず1つ登録する。無効時は常に Optional.empty()を返すNullObject(AltitudeConverterImpl.unavailable())を登録する。 Beanそのものを作らない方式にすると、将来usecaseがこのポートをコンストラクタで要求した時点で、 enabled=falseの環境(既定値。ローカル開発のほとんど)がすべて起動不能になるため。 「当該地点のデータが無ければOptional.empty()」というポートの契約(3.3節)に沿った表現であり、 消費側はenabledの値を意識しなくてよい。3状態(未設定・falsetrue)でBeanが1つだけ登録される ことはElevationConfigTestApplicationContextRunner)で検証している。

Beanの破棄時(@Bean(destroyMethod = "close"))には保持しているDEM(GeoToolsのラスタ)を解放する。 実運用(1プロセス1回起動)では実害はないが、bootRunのspring-boot-devtoolsはクラスパス変更ごとに コンテキストを再起動するため、旧コンテキストのラスタが残らないようにしている。

6.1 生成の分離: 使い捨てコンテナ

Section titled “6.1 生成の分離: 使い捨てコンテナ”

DEM(GeoTIFF)生成にはgdal-binpython3-gdalnumpyPillowが必要になるが、アプリ本体の 実行イメージ(eclipse-temurin JREのみの薄いイメージ、Dockerfile)に 焼き込むとサイズ・依存関係(Pythonスタックの混入)が大きく変わる。そのため生成処理は アプリ本体から切り離した使い捨てコンテナdocker/elevation-data-prep/) に分離し、生成結果だけをアプリと共有する。

flowchart LR
    subgraph prep["elevation-data-prep(使い捨てコンテナ、gdal/Python同梱)"]
        Check{"dem-#60;地域#62;-#60;メッシュ#62;.tifが<br/>既に存在?"}
        Download["GSJシームレス標高タイル(mixed)<br/>PNGダウンロード"]
        Convert["タイルGeoTIFF化<br/>→VRTモザイク→EPSG:4326再投影<br/>→COG化"]
    end
    Volume[("local/elevation/<br/>(.gitignore対象、bind mount)")]
    App["app(JREのみ、gdal/Python無し)"]

    Check -->|ある| Skip["何もせず終了"]
    Check -->|無い| Download --> Convert --> Volume
    Volume -.読むだけ.-> App
  • compose.yamlelevation-data-prepサービス(elevationプロファイル)が対応する。 docker compose --profile elevation run --rm elevation-data-prepで実行し、 出力先(local/elevation/dem-<地域>-<メッシュサイズ>.tif、6.2節の命名規則参照)に 既にファイルがあれば即終了、無ければGSJタイルから生成する
  • 出力先パス・範囲(bbox)・ズームレベル・出力解像度は既定値を持たせず必須パラメータにしている (対象地域を追加・変更する際に、無自覚に別地域を上書き生成してしまうことを防ぐため)
  • ジオイド(ISG)ファイルは自動生成の対象外。国土地理院から直接ダウンロードし、 local/elevation/へ手動配置する運用のまま(実データはライセンス・サイズの両面でリポジトリに コミットしない。8.3節参照)

詳細な使い方はdocker/elevation-data-prep/README.md参照。

6.2 対象範囲・解像度: 一部地域は高解像度、それ以外はフォールバック

Section titled “6.2 対象範囲・解像度: 一部地域は高解像度、それ以外はフォールバック”

データソースはGSJ(産業技術総合研究所)シームレス標高タイル(mixed)を採用する (https://tiles.gsj.jp/tiles/elev/tiles.html、ズームレベル0〜17対応、z=17で約1.2m/pixel、 z=10〜14相当がGSI DEM10B=10m解像度データの範囲)。

全国を一律の高解像度で用意するのは現実的でない(下表参照)ため、対象地域を絞って高解像度DEMを 生成し、それ以外は低解像度の全国版DEMへフォールバックする構成を採る (3.1節のFallbackTerrainElevationSource)。

  • 高解像度: 東京23区(lon139.56-139.92, lat35.53-35.82)。z=14の元タイルを出力0.0001度=約10mへ集約
  • フォールバック: 全国(lon122.9-154.0, lat20.4-45.6。南西諸島・小笠原諸島を含む日本全域)。 z=8の元タイルを出力0.01度=約1kmへ集約

指定値はREADMEのコマンド例を正とする。

ズームレベルと出力解像度は別物である点に注意する。z=8の元タイルは緯度35°付近で約501m/pixel、 z=14で約7.8m/pixelで、これをELEVATION_DEM_RESOLUTION_DEG(出力グリッド間隔、度単位)へ集約した 結果が下表の「約1km」「約10m」である(対応表は docker/elevation-data-prep/README.md「パラメータの目安」)。

範囲ズーム(元タイル解像度)出力解像度実測/概算タイル数所要時間
全国z=14(約7.8m/pixel)0.0001度=約10m概算約197万枚概算:直列0.2秒待機で約27時間、ストレージ数十〜100GB規模
東京23区(lon139.56-139.92, lat35.53-35.82)z=14(約7.8m/pixel)0.0001度=約10m実測306枚対象・284枚取得・22枚データ無し実測:約2分(ビルドキャッシュ済み状態)
全国(lon122.9-154.0, lat20.4-45.6)z=8(約501m/pixel)0.01度=約1km実測529タイル対象で404なし全件取得実測:約4分(ビルドキャッシュ済み状態、出力3110x2520px)

全国×10mは公開タイルサーバへの負荷・処理時間の両面で現実的でない一方、23区×10m・全国×1kmは いずれも数分オーダーで完了する(詳細はPR#157参照)。他の地域を追加で高解像度化したい場合は、 docker/elevation-data-prep/README.mdの手順で 別ファイルとして生成し、elevation.dem-geo-tiff-pathsの優先順位リストに追加すればよい。

ファイル名・配置先の規則docker/elevation-data-prep/README.md 「ファイル名・配置先の規則」を正とする。

6.3 段階的拡張: 複数ファイル+インデックス化(動作確認済み・未採用)

Section titled “6.3 段階的拡張: 複数ファイル+インデックス化(動作確認済み・未採用)”
  • 現行方式の限界: FallbackTerrainElevationSource(6.2節)は各ファイルがそれぞれ独立して 全域を評価する単純な優先順位フォールバックで、起動時に開くファイル数がそのまま増える

  • 検証した代替案: 複数のGeoTIFFタイルを結合せずGeoToolsのImageMosaicorg.geotools:gt-imagemosaic)でインデックス化し、1つのカバレッジとして読む方式。ディレクトリに 複数のGeoTIFFを置くだけでインデックス(シェープファイル+.properties)が自動構築され、タイル境界を またいだ問い合わせでも正しい値を返すことを確認した。カバー範囲が細かく分割される運用(例: 都道府県 単位で段階的に追加)に向く

  • 未採用の理由と注意: 現状の2段構成では単純な優先順位フォールバックで要件を満たす。なお この検証はPR #157の開発過程での手元確認にとどまり、再現可能なテスト・スクリプトとして残していない ため、改めて採用を検討する際は再度動作確認が必要

  • 利点: タイルを追加するだけでコード変更なしにカバー範囲を広げられる。全国分を一度に マージ・再投影する重い処理が不要。FallbackTerrainElevationSourceのようにファイルを1つずつ順に 開く必要がなく、大量の細分化タイルでもスケールする

  • 未採用の理由: 6.2節の2段構成(23区+全国)程度の規模であれば、単純な FallbackTerrainElevationSourceで十分要件を満たす。タイル数が数十〜数百規模に増える段階になったら 改めて採用を検討する。採用する場合はGsiDemTerrainElevationProviderGeoTiffReaderから ImageMosaicReaderへ差し替える改修が必要

6.4 ジオイド(ISG)ファイルの入手・配置

Section titled “6.4 ジオイド(ISG)ファイルの入手・配置”

DEMと異なり、ジオイド高ファイル(JPGEO2024.isg)・基準面補正ファイル(Hrefconv2024.isg)は elevation-data-prepによる自動生成の対象外(2章参照)。国土地理院の提供ページから手動で ダウンロードして配置する。

  • 入手元(一次情報): 「ジオイド2024日本とその周辺」と「基準面補正パラメータ」の提供 https://service.gsi.go.jp/kiban/app/geoid/(国土地理院)。ISG形式のJPGEO2024_isg一式 (JPGEO2024.isgHrefconv2024.isg等)をダウンロードできる
  • 配置先はどちらの利用シーンでも共通でlocal/elevation/直下.gitignore対象、 .gitignore:69-74):
    • 開発者(Gradle/JVMで直接テストを動かす場合): local/elevation/JPGEO2024.isgに置けば JpGeoid2024HeightProviderRealFileTest等のオプトイン統合テストが拾う(8.3節)
    • docker composeで動かす場合: 同じlocal/elevation/appelevation-data-prep両 サービスへ/var/lib/utm/elevationとしてbind mountされる(compose.yaml)ため、 配置先は開発者向けと同一。追加の変換・コピー作業は不要
  • 測量成果の複製・使用にあたっては測量法に基づく手続が必要な場合がある (https://www.gsi.go.jp/buturisokuchi/grageo_geoidprocedure.html参照)

7. usecaseからの呼び出し方(今後の実装に向けて)

Section titled “7. usecaseからの呼び出し方(今後の実装に向けて)”

本リポジトリは現時点で飛行計画領域・空域制限のusecaseが未実装のため、 AltitudeConverterを呼び出す消費者はまだ存在しない。今後usecaseを起こす際の 設計方針を記す。

7.1 呼び出しタイミング: 登録・収集時に1回

Section titled “7.1 呼び出しタイミング: 登録・収集時に1回”

docs/geospatial/design/geospatial-api-design.md 5.1節の決定どおり、変換は飛行計画領域・空域制限 の登録/更新時に一度だけ行い、結果を各ドメインの高度換算結果を保持する列にキャッシュする (列の構成はdocs/data-model/flight-plan-er.mdの図とカラム補足が正)。 応答(検索API)のたびには呼び出さない。

7.2 Tx境界: 現行アーキテクチャならTx内で呼んで問題ない

Section titled “7.2 Tx境界: 現行アーキテクチャならTx内で呼んで問題ない”

Elevation Serviceは起動時に読み込んだDEM/ジオイドをプロセス内メモリに保持するだけで、変換自体は 外部I/Oを伴わない(2章)。そのためTransactionManager.executeの中で呼び出しても、 ADR-022(トランザクション内での外部I/O呼び出し禁止。docs/implementation-guide.md参照)には 抵触しない。

ただしDEM参照をネットワークI/O化する構成(S3+COGのHTTPレンジリクエスト等。本書では未検討)を採用すると この前提が崩れる。 その場合はDEM参照が本物のネットワークI/Oになるため、Tx開始前に変換を すべて済ませ、結果だけをTx内で書き込む構成へ変更する必要がある。

7.3 頂点ごとの変換とスカラー値の集約

Section titled “7.3 頂点ごとの変換とスカラー値の集約”

WGS84基準のFeatureは頂点ごとに地表標高を反映する設計(geospatial-api-design.md 5.1節)のため、 Polygon/Route等の全頂点でAltitudeConverterを呼び、min/maxAltitudeMWgs84はその集約値になる。 この「頂点ループ+Optional.empty()のハンドリング+min/max集約」は飛行計画領域・空域制限で 共通処理になる見込みがあり、各usecaseがAltitudeConverterを直接呼ぶのではなく、間に共有の ドメインサービス(ジオメトリ全体の高度付与を担う)を挟む設計とする。register/update両usecase (飛行計画)と将来の空域制限usecaseが同じドメインサービスを呼ぶ。

  • 天面・床面のZは1頂点1回の変換で済む: WGS84楕円体高 = 地表標高 + AGL + ジオイド高はAGLに対して 線形なので、top_bottom_3d_geometry_wgs84の天面(maxAltitudeMAgl)で変換した結果から床面 (minAltitudeMAgl。10月デモでは0固定)のZは天面のWGS84楕円体高 - maxAltitudeMAgl + minAltitudeMAgl で算術的に導出できる(地表標高・ジオイド高の項はAGLに依存しないため打ち消し合う)。頂点ごとに DEM・ジオイドへ2回問い合わせる必要はなく、天面・床面が独立変換によって食い違うリスクも生じない

7.3.1 SQLとの役割分担: 材質化されたジオメトリの座標をJava側で先に取得する

Section titled “7.3.1 SQLとの役割分担: 材質化されたジオメトリの座標をJava側で先に取得する”

plan_geometryoperational_intent_geometrytop_bottom_3d_geometryは現在、FlightPlanMapper.xmlinsertAreaが1本のSQL(INSERT ... SELECTのCTE内でST_Buffer等を計算)で組み立てており、 バッファ計算後の実際の頂点座標はJavaに渡らない。AltitudeConverterはSQL関数ではなくJavaの コンポーネントであるため、頂点ごとの変換にはバッファ後の座標をJava側で持つ必要がある。

方針: 永続化前に読み取り専用クエリで材質化後の座標(AGL基準、2D)を取得し、Java側で変換・拒否判定 した上で、最終的な1回のINSERTへ進む。AGL側の列(plan_geometry等)はINSERT時にST_Bufferで 再計算させ、_wgs84列・AMSL/WGS84スカラーだけをJavaで組み立てたリテラル値として渡す。

  • 手順(実装はGeometryMapper.selectMaterializedAreaGeometriesPostgisFlightAreaGeometryDeriverFlightAreaAltitudeEnricherFlightPlanContentValidatorWgs84GeometryWkt):
    1. 現在insertAreaのCTEが行っているST_Buffer等の計算と同じ式を、読み取り専用の別クエリ (挿入を伴わないSELECTGeometryMapper.selectMaterializedAreaGeometries)として先に実行し、 plan_geometryoperational_intent_geometry(AGL基準、材質化後、2D)の座標をJavaへ取得する。 top_bottom_3d_geometryはこの時点では取得しない(3.のとおりJava側で組み立てるため不要)
    2. 取得した座標の全頂点について、共有ドメインサービス(7.3節)がAltitudeConverterを呼び、 WGS84楕円体高・AMSLへ変換する。1頂点でも変換できない場合(elevation.enabled=trueの環境)は ここで登録・更新を拒否する(7.4節)。まだ何も永続化していないため、ロールバックは不要
    3. 変換に成功したら、変換結果(頂点座標+WGS84楕円体高)からplan_geometry_wgs84operational_intent_geometry_wgs84top_bottom_3d_geometry_wgs84のWKTとAMSL/WGS84スカラーを Java側で組み立てる(Wgs84GeometryWkt)。AGL側の列(plan_geometry等)はこの結果を使わず、 1で確認したwktradiusMbufferMをそのままinsertAreaへ渡し、ST_Buffer等のSQL計算を もう一度実行させるST_Bufferは決定的な関数のため、1と同じ入力を渡せば1で確認した座標と 完全に一致する結果になり、二重計算によるAGL側とWGS84側の食い違いは生じない。既存の insertAreaのCTE(AGL側の計算ロジック)を書き換えずに済むことを優先し、1回のクエリで済ませる ことよりも既存コードへの変更を小さくすることを選んだ
  • 既存のGeometryValidator(2-1-6の自己交差チェック)と同じパターン: PostGISへ読み取り専用で 事前に問い合わせ、usecase層(FlightPlanContentValidator)で判定してから本INSERTへ進む構成。 この構成に合わせることで、変換不可(7.4節)による拒否もGeometryValidatorの自己交差チェックと 同じ経路で422として返せる(repositoryの書き込み失敗としてRepositoryException→500になる経路とは 別にできる)
  • INSERT単体で完結し、追加のUPDATEは不要: 「本INSERT実行→RETURNINGで座標取得→Java変換→ _wgs84列だけを埋めるUPDATE」という2段書き込み方式も検討したが、拒否時にINSERT済みの行の ロールバックに頼ることになり、かつ拒否のエラーがrepository層の書き込み失敗として扱われやすい ため採用しない。永続化前に判定を済ませる方式(上記)を採る
  • WktCodecの拡張は読み取り側では不要だった: plan_geometryoperational_intent_geometryは いずれも2D・単一リングのPolygon/LineStringのため、既存のWktCodec.parse(Z値なし)のまま読める。 top_bottom_3d_geometry_wgs84(Z値付きMultiPolygon)はSQL側から読み戻すのではなくJava側で 組み立てて書き込むだけのため、書き込み用のメソッド(WktCodec.polygonZmultiPolygonZ)を 追加するだけで済んだ。JTS(org.locationtech.jts)への切り替えは行っていない (docs/data-model/flight-plan-er.md「ジオメトリはWKT文字列として手組みで扱う」方針を維持する)
  • JTSでバッファ計算そのものを再実装する案(比較検討したが不採用): PostGISのST_Buffergeographyキャスト)と同じ結果をJava側で再現する案も検討したが、geography版のバッファは PostGIS内部の_ST_BestSRIDによる平面投影に依存しており(flight-plan-er.mdの「投影座標系を 明示的に選ばない理由」参照)、JTSで厳密に再現するのはリスクが高い。バッファ計算の定義を SQL側(読み取り専用クエリと最終的なINSERTの2箇所。ともにST_Bufferの式そのものは同一)に保つ 上記方針を採る

7.4 一部頂点だけDEM未整備だった場合の扱い: 登録・更新自体を拒否する

Section titled “7.4 一部頂点だけDEM未整備だった場合の扱い: 登録・更新自体を拒否する”

頂点の一部だけ変換できない(Optional.empty())場合、その_wgs84列を部分的に埋める、あるいはNULLの まま保存して後から補完する、という選択肢は取らない。登録・更新自体を拒否する(業務ルールは docs/flight-planning/BusinessLogicSpecifications.mdを正とする)。

  • 遅延充填は行わない: DBに格納された時点でAGL・AMSL・WGS84楕円体高の換算値が全て揃っていることを 保証する。これにより、応答側(GeoSpatialの領域検索等)はNULLの可能性を考慮する必要がなくなる
  • 一部だけ埋まった_wgs84列は作らない: 1つのFeatureの中で一部の頂点だけ変換済み・残りが欠測という 状態は地図表示上不自然になりうるため、Feature(plan_geometry_wgs84等の1列)単位で全頂点変換できた 場合のみ保存する。1頂点でもOptional.empty()ならその登録・更新全体を拒否する(1つのFLIGHT_PLAN_AREA 行が複数の_wgs84列を持つため、行単位でも同様にall-or-nothingになる)
  • 利用者への影響: DEMの段階展開(2章「段階展開」・6.2節)によりカバー範囲は現時点で東京23区10m+ 全国1kmフォールバック(南西諸島・小笠原諸島を含む日本全域)である。この範囲外の地点を含む飛行計画は 登録・更新が拒否される。カバー範囲が全国1kmフォールバックで概ね日本全域を覆うため実際に拒否される ケースは限られる見込みだが、ゼロではない(本文書更新時点で未検証)
  • elevation.enabled=false(既定値)の環境では拒否しない: enabled=falseの場合、AltitudeConverterは 設定に関わらず常にOptional.empty()を返すNullObjectになる(ElevationConfigのjavadoc参照)。 上記の拒否ルールをこの状態にも無条件に適用すると、DEM・ジオイドファイルを用意していないローカル 開発環境で飛行計画の登録・更新が一切できなくなり、enabled=falseをデフォルトにした本来の目的 (データファイル未配置でも起動・開発を妨げないこと。ElevationPropertiesのjavadoc参照)と衝突する。 そのため、flight-planningのusecaseはAltitudeConverterの戻り値だけでなくElevationProperties.enabled() も参照し、enabled=falseのときは高度換算チェック自体を行わず、スカラー・_wgs84列をNULLのまま 保存する(拒否しない)。enabled=trueのときのみ7.4節の拒否ルールを適用する。 このためFLIGHT_PLAN_AREAの換算値カラムにNOT NULLのDB制約は課さない(enabled=falseの環境で 正当にNULLになるため。データモデル側の記載は flight-plan-er.md参照)

上位の方針は全ドメイン共通のテスト方針ArchitecturePolicy_Common.md:620 の20章)とテストコード規約CodingConventions_Common.md:1359 の39〜44章)であり、本節はそれをElevationへ適用した内訳である。 共通方針の5層のうちElevationに 存在するのは次の2つだけで、残る3層(20.1 UseCase・20.2 Handler・20.4 統合)は該当するコードが無い。

共通方針の層Elevationでの扱い
20.3 Infrastructure本体。ただしRepository実装・MyBatis Mapperを持たないため、同節が挙げるBoundSql検証・Testcontainersによる実DBテストは該当しない。代わりに置くのが8.2・8.3節の3層(合成データ・合成GeoTIFFの実I/O・実データのオプトイン)
20.5 アーキテクチャArchitectureTestcom.intent_exchange.utm.infrastructure..com.intent_exchange.utm.domain..をワイルドカードで層に割り当てているため、infrastructure.elevationdomain.portの追加でルールの変更は不要(ArchitectureTest.java:32-34)。層・パッケージ追加時にルールを確認する決まり(20.5節・44章)に対する確認結果として記録する

規約のうちElevationが従うもの: テストクラス名は対象クラス名+Test(39章。派生の観点は ...RealFileTest...ConcurrencyTest...BindingTestのように観点名を挟む)、アサーションは AssertJに統一(43章)、テストメソッド名はtestXxx.claude/rules/java/test.md。共通規約42章は 形式を開発者に任せており、飛行計画forDemoの<対象メソッド>_when<条件>_<期待結果>はそのドメインの 上書きのためElevationには及ばない)。

カバレッジは分岐(C1)90%を目標値とする(TestPolicy_Flightplanning_forDemo.md:128。 飛行計画forDemoの数値だが、他に共通の基準が無いためこれに合わせる)。実測はinfrastructure.elevationconfigのElevation関連クラス合計で169/170(99.4%)で、未到達はGsiDemTerrainElevationProviderの 防御的分岐1件だけである(内訳は8.2節)。同方針が「目標値達成のためだけの機械的なテスト追加は行わない」と 定めているため、到達させるための不自然なテストは書かない。

以下はElevation固有の判断で、Elevationの設計記述の正はソースコードとテストであるため (.claude/rules/design-docs.mdの適用範囲の節)、本設計書が挙動として主張することには、 変われば落ちるテストを必ず対応させる。方針は次の6点。

  1. 依存の重さで3層に分ける。純粋ロジック(ISGのパース・双線形補間・優先順位フォールバック・ 基準の合成計算)はフェイクと合成データで常時実行し、ライブラリ境界(GeoToolsのGeoTIFF読込・ CRS変換・ラスタ解放)は合成GeoTIFFを書き出して実ファイルI/Oを通し、実データ依存(精度・ カバー範囲)はオプトインにしてファイルが無ければスキップする(8.2・8.3節)
  2. 値の精度は検証しない。実DEM・実ジオイドを使うテストで見るのは「例外なく変換できること」と 「基準どうしの関係が保たれること」(AGL⇄楕円体高の往復、AMSLと楕円体高の差がジオイド高)に 留める。地点座標が測量成果ではないため絶対値の正しさを主張できない(8.3節)
  3. 外部ライブラリの挙動は推測せず実測して固定する。GeoToolsとGDAL生成物の挙動は版で変わりうる ため、依存している挙動(GeoTiffWriterがNoData宣言を書き出さない・DEMのCRSの軸順)をテストで ピン留めし、変わればテストが落ちて気づける形にする
  4. 検証は、それを持つ型のテストへ置く。座標の値域・NaN・InfinityはいずれもLonLatTest (3.1節。LonLat自身のテストとして新設)で固定済みのため、Elevation側では重複して 検証しない。範囲外の座標を変換器のテストで突くとLonLatの生成時点で例外になり、変換器側の 検証が無くても緑になるため置かない(3.1節)
  5. 到達できない防御コードのために不自然なテストを書かない。リフレクションやモックで無理に 通さず、未到達の系統を列挙して残す(8.2節)
  6. 後始末と並行性を明示的に検証する。Beanはシングルトンで、DEMはGeoToolsのラスタを保持する。 複数スレッドからの同時問い合わせ・close()の伝播・組み立て途中で失敗したときの解放・後始末 自体が失敗したときの抑制例外を、それぞれテストで固定する(3.4節)

8.2 合成データによるユニットテスト

Section titled “8.2 合成データによるユニットテスト”

IsgHeaderTestIsgGridModelTestJpGeoid2024HeightProviderTestAltitudeConverterImplTestは、 テスト内で組み立てた小さい合成ISGデータ・フェイク実装を使い、パース・補間・NaN伝播・入力検証の ロジックを検証する。実データなしでCIから常に実行される。

ISGの読み込みはヘッダ解析とデータ部で分けてテストする(クラスの分割に合わせる。39章のテストクラス 命名規約)。IsgHeaderTestは[IsgHeader#parse(String, Path)]がテキストからの純粋な変換であることを 利用してファイルを書かずに直接呼び、DMS表記・格子数の突き合わせ・data ordering・必須項目の欠落を 検証する。IsgGridModelTestに残すのは、実ファイルを読んで初めて成り立つもの(ヘッダマーカーの検出・ 空行の読み飛ばし・格子点数の突き合わせ・非検査例外のIOExceptionへの変換)と補間の検証である。

合成データの生成と実ファイルの解決はElevationTestFixturesに集約する(合成ISG・合成DEMの生成コードが 各テストクラスへ写されていたため。ISGパーサの受理条件やDEMの書き出し方を変えるときに直す場所を1つに 保つ)。

FallbackTerrainElevationSourceTestはフェイク実装(優先順位フォールバックのロジック単体)、 AltitudeConverterImplFallbackTestGeoTiffWriterで書き出した合成GeoTIFFを使い、優先ファイルの カバー範囲内ではその値を、範囲外では次点ファイルへフォールバックすることを実ファイルI/Oを通して 検証する(3.1節)。

GsiDemTerrainElevationProviderConcurrencyTestは、複数タイルにまたがる合成GeoTIFF(256画素 タイルで16タイル)を使い、冷えたインスタンスへCyclicBarrierで複数スレッドを同時に飛び込ませて、 複数スレッドから同一インスタンスへ同時に問い合わせても値が取り違わないことを検証する(3.4節)。 期待値は並行実行の後に別インスタンスから単独スレッドで読み取る(自前で導出しない)。単一タイルの ラスタや、事前に単独スレッドで全地点をウォームアップしてから並行実行する構成では、リスクがある 初回タイル実体化の瞬間を並行区間の外に逃してしまうため、あえてこの形にしている。 GsiDemTerrainElevationProviderTestは、 カバー範囲内のNoData画素・海溝の実水深・実在しえない標高がそれぞれ正しく扱われることを検証する(3.5節)。

ElevationConfigTestはBeanの組み立て(ElevationPropertiesAltitudeConverter)に加え、 ApplicationContextRunnerelevation.enabledの3状態(未設定・falsetrue)それぞれで Beanが1つだけ登録されることを検証する(5章)。

カバレッジ: 上記によりinfrastructure.elevationconfigのElevation関連クラスは、 GsiDemTerrainElevationProviderを除き行・分岐とも100%(IsgHeaderIsgGridModelも、 グリッドサイズの上限・範囲の向き・間隔の正値性を検証する分岐を足したうえで100%を維持している)。 同クラスは多重防御として置いている次の2系統(JaCoCo計測では分岐17/18)が未到達で残る。

  1. CRS解決失敗時のIOException変換(FactoryExceptionのcatch節)
  2. evaluateが非有限値を返した場合(!Double.isFinite(value)
  3. GeoTiffReaderのコンストラクタは成功したがread()だけが失敗する場合(検査例外をElevationDataExceptionへ詰め替える2箇所のcatch節のうち後者。前者はディレクトリを渡すテストで到達させている)

読み込んだGeoTIFFが単バンドであることは起動時に検証する(標高DEMのつもりでRGBのタイル画像を 配置した場合、弾かないと起動は成功してevaluate用のバッファ長が合わずリクエスト時に落ちる)。 検証で例外を投げる経路では、既に確保したラスタをdisposeしてから送出する。

いずれもGeoToolsが手前で処理してしまうため合成テストでは到達させられず、100%化のために リフレクション等の不自然なテストは書かない方針とした(本番のGDAL生成ファイルでの挙動差に備えた 保険として実装は残す)。

GeoTIFF宣言値との一致判定(declaredNoDataValuesとの照合)は、GDAL生成のテストリソースで 到達させた。 当初はGeoTiffWriterGridSampleDimensionのNoData Categoryを GDAL_NODATAタグとして書き出さないため(書き戻したGeoTIFFのNoData宣言は空になる。起動ログの WARNDEM GeoTIFF has no declared NoData valueで観測できる。この挙動は GsiDemTerrainElevationProviderTesttestWrittenGeoTiffCarriesNoDeclaredNoDataで固定しており、 GeoTools側の挙動が変われば同テストが落ちて気づける)合成テストでは到達できなかった。 本番のDEMはdocker/elevation-data-prepgdal_translate -a_nodataで生成するため、 その形の小さいGeoTIFF(src/test/resources/elevation/dem-with-declared-nodata.tif。 国土地理院・産総研のデータは含まない合成値)を1つコミットし、 testTreatsValueMatchingDeclaredNoDataAsNoDataで宣言値の照合だけに効かせて検証する (宣言値は実在しえない標高・パイプライン既定値のいずれとも重ならない-1000を使う)。

8.3 実ファイルによるオプトイン統合テスト

Section titled “8.3 実ファイルによるオプトイン統合テスト”

GsiDemTerrainElevationProviderRealFileTestJpGeoid2024HeightProviderRealFileTestAltitudeConverterImplRealFileTestは、実際のDEM(GeoTIFF)・JPGEO2024.isgファイルを使って動作確認する。

  • 実データはリポジトリにコミットしない: 全国版は数MB〜あり、国土地理院データのライセンス・ git肥大化の両面からlocal/elevation/.gitignore対象)に置く運用とする (.gitignore:69-74
  • ファイルが存在しない場合はAssumptions.assumeTrue自動的にスキップされ、CIや実データ未配置の 開発者の手元でも失敗しない
  • 既定パス(local/elevation/dem-nationwide-1km.tiflocal/elevation/JPGEO2024.isg)は環境変数 (ELEVATION_TEST_DEM_PATHELEVATION_TEST_GEOID_PATH)で上書き可能

AltitudeConverterImplRealFileTestは、市街地・埋立地・河川・海上・山間部・ダムという多様な地形/水域と、 地中〜地表〜空中という多様なAGL入力の組み合わせで例外なく変換できることを確認するスモークテストで、 値そのものの精度を検証するテストではない(地点座標は目視による概略値であり測量成果ではないため)。

  • 機能要求5.1.3.3(DEM10B精度の最低保障・バイリニア補間)を満たしていない: geospatial-api-design.mdの「地表標高データ(DEM)の 参照手段はドメイン間で共有する」が引く同要求に対し、本実装は23区外が1kmメッシュ(下記)で、DEMの サンプリングも最近傍(3.6節)である。全国10mメッシュ化(6.3節)とDEM側の双線形補間(3.6節)の 両方が要求充足の条件になる。台帳(followups.md)に計上済み
  • DEMの解像度限界(東京23区以外): 23区は10m相当のDEMを用意したが、それ以外は1kmメッシュの DEMにフォールバックするため、23区外の山間部では地形が大きく平滑化されたままである。実際に 三峰神社・小河内ダム付近(いずれも23区外・山間部)で検証した際、一般に知られる標高より数百m 低い値になった(検証記録はPR #157参照。座標自体の精度も要因の一つ)。高解像度化の対象地域拡大は 6.2節参照。なお平滑化には参照方式に起因する分(DEMは最近傍サンプリング、3.6節)も含まれる
  • 誤差は「地表標高を低く見る」方向に偏る(安全側ではない): DEM生成時の集約は gdalwarp -r average(平均)で行っており(docker/elevation-data-prep/entrypoint.sh)、平均化は 山の頂を削り谷を埋める方向に働く。実測でも三峰神社付近は一般に知られる標高(約1100m)に対し 約400m(-700m)となった。地表標高を過小評価するとWGS84楕円体高も同じだけ低く算出されるため、 この値をそのまま空域制限の上限との突き合わせ等の安全判断に使うことはできない
    • 現時点では-r averageを維持し、この制限を記録するにとどめるという判断とした。10月デモの 会場は東京23区内で高解像度DEM(約10m)のカバー範囲に収まり、平滑化の影響が大きい山間部は デモ対象外であること、および以後の開発で全国10mメッシュ化(複数タイルのインデックス化、6.3節) を想定しており、そちらで解像度そのものを上げる方が根本的であることが理由
    • 集約方法を-r max(保守側)へ変える等の選択は、全国10mメッシュ化の設計時、または安全判断に この値を使うusecaseが現れた時点で再判断する。なお解像度が粗い地域を「変換不可」として登録・更新を 拒否する選択は取らない(7.4節)。解像度が粗いことと変換自体が不可能なこと(Optional.empty())は 別の問題であり、後者のみが7.4節の拒否ルールの対象になる
  • 海上ではAGLの基準面が「海面」ではなく「海底」になる: GSJの標高タイルセットは海陸シームレス (2章)で、aglToAmslMeters地表標高(海底の水深)+ AGLをそのままAMSLとして返す。水深のある 地点でAGL=100mから変換すると、AMSLは「海面から100m」ではなく「海底からAGL分持ち上げた高さ」に なり、実際の対水面高度より過小に算出される(安全側ではない方向)。海上の運用(離着水・水上での 低高度飛行等)で本値を安全判断に使う場合はこの点に注意が必要。解法自体は既に成立する: 海面はAMSL≈0mであるため、対象地点が海上と分かっているなら地表標高を0とみなせばよく、 AMSL = AGLWGS84楕円体高 = AGL + ジオイド高になる(レビューで指摘)。ボトルネックは 「対象地点が海上かどうかの判定」で、現状のDEM自体(海陸シームレスな標高値)からは水深が 既知でも「海である」ことを明示的には区別できない(内陸の凹地形と値の上では区別が付かない)。 この判定手段を用意する・海域を別扱いにする等の対応は、海上での本サービスの利用要件が 具体化した時点で判断する (台帳(followups.md)に計上済み)
  • utm-design-docs側のADR化が未着手(2章参照)
  • DEM未整備地域の扱い: 全ての設定済みファイル(elevation.dem-geo-tiff-paths)でカバーされない 地点はOptional.empty()を返す。フォールバック値・代替データソース(衛星DEM等)は設けていない。 飛行計画領域等の消費側が「地表標高データ未取得の場合は未設定(NULL)」として扱う設計 (docs/data-model/flight-plan-er.md)と整合させている
  • 7章「usecaseからの呼び出し方」は未実装。実際にusecaseを起こす段階で、頂点ごとのDEM未整備時の 扱い(Feature全体を諦めるか部分的に持たせるか)を決める必要がある