コンテンツにスキップ

空域制限ドメインER図

空域制限(飛行禁止エリア)ドメインの論理データモデルを示す。上位の概念モデルは utm-design-docs/docs/data-model/data-model.mdの 「空域制限ドメイン」、API仕様はgeospatial.yaml、 DB列とAPIプロパティの対応はgeospatial-api-design.mdの5.2節、 図の記法・命名規約はconventions.mdを参照。

各設計判断の理由は末尾の「設計方針」に、図中に収まらないenum値の全列挙・制約・参照先は 図の直後の「カラム補足」に記載する。

設計上の分岐と結論は「決定事項」に一覧で示す。

10月デモにおける空域制限はDBへの事前投入であり、DIPSからの定期収集は実装対象外である (utm-design-docs/docs/usecase/flight-plan/implementation-schedule.mdの8.1節、 pr-breakdown.mdの「空域制限情報の最新化API(DIPS収集)| 実装しない」の行)。 本設計はこれに合わせ、参照用の最小モデルを対象とする。

概念モデルのテーブル本設計での扱い
AIRSPACE_RESTRICTION採用する。内容も本テーブルが持つ(1テーブル)
AIRSPACE_RESTRICTION_REVISION採用しない。 履歴が必要になった時点でマイグレーションで起こす
AIRSPACE_SOURCE採用しない。 kindのみAIRSPACE_RESTRICTION.source_kind列へ降格する
AIRSPACE_SOURCE_SYNC_LOG採用しない。 収集処理と対で設計する
取得元レスポンスの原文・無変更判定のハッシュ採用しない。 収集処理と対で設計する

AIRSPACE_SOURCEを落とす代償は「提供元は最小モデルでは列として持つ」に、 リビジョン表を落とす判断は「リビジョンは履歴が必要になった時点で起こす」に記す。

用語意味
DIPS FPRDIPS 2.0 APIのうち飛行禁止エリア情報取得API。本ドメインの一次データ源
制限分類ソースを横断して正規化した空域制限の種別。docs/openapi/domain.yamlAirspaceRestrictionType(両OASが共有)に対応する
元種別IDソース固有の生の種別ID。DIPSのflightProhibitedAreaTypeId。APIでは非公開とする
消失取得元の取得結果からexternal_idが消えた状態。物理削除せずstatus=DISAPPEAREDで表す

図1: 空域制限ドメイン(参照用最小モデル)

Section titled “図1: 空域制限ドメイン(参照用最小モデル)”
erDiagram
    AIRSPACE_RESTRICTION |o--o{ CONFLICT_DETECTION : "id で対応づけ (別スキーマ・FKなし)"

    AIRSPACE_RESTRICTION {
        uuid id PK
        string external_id UK
        enum source_kind
        enum status
        enum category
        int external_type_id
        string name
        string description
        string info_url
        enum geometry_type
        geometry geometry
        geometry geometry_wgs84
        geometry top_bottom_3d_geometry
        geometry top_bottom_3d_geometry_wgs84
        double circle_center_lng
        double circle_center_lat
        double circle_radius_m
        double min_altitude_agl
        double max_altitude_agl
        double min_altitude_amsl
        double max_altitude_amsl
        double min_altitude_wgs84
        double max_altitude_wgs84
        enum altitude_unit
        enum altitude_reference
        timestamptz valid_from
        timestamptz valid_to
        timestamptz collected_at
    }
    CONFLICT_DETECTION {
    }
テーブルカラム補足
AIRSPACE_RESTRICTIONid代理キー。UUIDv7。再同期でも振り直さないexternal_idで突き合わせて維持する)。flight_planning.conflict_detection.airspace_restriction_idが本列を参照するため、安定性が前提条件になる。安定性の確認はIssue #136
AIRSPACE_RESTRICTIONexternal_id取得元の安定識別子(DIPS: flightProhibitedAreaId)。VARCHAR(255)・単独UNIQUE・NULL可(外部IDを持たないMANUALのソース)。再同期時のUPSERTキーとして使い、GeoSpatialのAirspaceRestrictionProperties.externalIdとして公開する。ただし同一性の判定に使うのはidである(APIが返すのはUTMが採番したidである)。UNIQUE制約はNULLを重複扱いしないため、外部IDなしの行が複数あっても成立する
AIRSPACE_RESTRICTIONsource_kindDIPS_FPR / GSI / MANUAL の3値。OASのAirspaceSourceKindと同値。NOT NULL。GSIの行は10月デモでは存在しない(GSIは人口集中地区専用のソースで、そのcategory決定事項のD2によりデモ期間は行を持たないため。デモ以降はDIDの行とともに現れる)。値としては収集の実装時にGSI由来の他のcategoryが現れる余地を残して定義する
AIRSPACE_RESTRICTIONcollected_at取得元から取得した時刻。TIMESTAMPTZ・NOT NULL。事前投入では投入時刻。外部から収集したデータを保持する点で同じ性質のflight_planning.dips_flight_plan.collected_atに合わせ、1列にする
AIRSPACE_RESTRICTIONstatusACTIVE / DISAPPEARED。OASのAirspaceRestrictionStatusと同値。取得元での存在状態であり、飛行計画の運航状態とは別概念
AIRSPACE_RESTRICTIONcategory制限分類。AIRPORT_VICINITY / DENSELY_INHABITED_DISTRICT / SMALL_UAV_PROHIBITION_RED_ZONE / SMALL_UAV_PROHIBITION_YELLOW_ZONE / ORDINANCE_DESIGNATED_AREA / MANNED_AIRCRAFT_TAKEOFF_LANDING_AREA / EMERGENCY_OPERATIONS_AIRSPACE / OTHER_1 / OTHER_2 の9値。docs/openapi/domain.yamlAirspaceRestrictionType(両OASが$refする共通enum)およびflight_planning.airspace_restriction_type_typeと同値(制限分類の列挙値は1つに統一する)。うちDENSELY_INHABITED_DISTRICTは10月デモの間だけ行を持たない(決定事項のD2)ため、デモ期間の実データに現れるのは残り8値である
AIRSPACE_RESTRICTIONexternal_type_idソース固有の元種別ID。DIPS: flightProhibitedAreaTypeId(1/2/5/6/7/8/9/10/11)。NULL可(source_kind=GSIMANUALでは持たない)。APIでは非公開
AIRSPACE_RESTRICTIONname / description / info_urlDIPSのname / detail / urlnameはNOT NULL(OASで必須)、他2つはNULL可
AIRSPACE_RESTRICTIONgeometry_typeCIRCLE / POLYGON / MULTIPOLYGON の3値。OASのAirspaceGeometryTypeと同値。概念モデルはCIRCLE/POLYGONの2値であり、MULTIPOLYGONは本設計で追加したgeospatial-api-design.mdの5.2節が「データモデル側のgeometry_type列挙値にもMULTIPOLYGONの追加が必要」と指摘済み)
AIRSPACE_RESTRICTIONgeometryPostGIS型はPolygonMultiPolygonの双方を許すSRID 4326。次元は縛らないgeometryの次元は縛らない)。型修飾子は使わずCHECKST_SRID=4326、GeometryTypePOLYGONまたはMULTIPOLYGON)で保証する(理由はdb/README.mdの「psqldefが解釈できない記法」)。CIRCLEもPolygon近似で格納する。GiSTインデックスを張る。Z値を持つ場合の基準はaltitude_reference現時点では常にAGL
AIRSPACE_RESTRICTIONgeometry_wgs84geometryのZ値を頂点ごとにWGS84楕円体高へ換算した実体化列。PostGIS型・SRIDはgeometryと同じ(CHECKは型・SRIDのみで次元は縛らない)。NULL可(標高(DEM)・ジオイド高の取得経路を持つ収集を実装するまでは、投入元が提供しない場合にNULL)。10月デモは事前投入でAGL・WGS84の両方を直接ロードするため遅延充填を経ずに値が入る(WGS84基準の形状も実体化列で持つ
AIRSPACE_RESTRICTIONtop_bottom_3d_geometry上面(天面)・下面(床面)を2枚のリングで表したMultiPolygonZcoordinates[0]が上面、coordinates[1]が下面。PostGIS型はSRID 4326・MultiPolygon3次元必須CHECKST_NDims=3。飛行計画のck_flight_plan_area_top_bottom_3d_geometryと同じ)。NULL可(高度が取得できない行では組み立てられない)。天面が傾斜する制限表面は頂点ごとにZ値が異なるため本列が正となる(上面・下面は3次元のgeometryで持つ)。Z値の基準はaltitude_reference現時点では常にAGL
AIRSPACE_RESTRICTIONtop_bottom_3d_geometry_wgs84top_bottom_3d_geometryのZ値を頂点ごとにWGS84楕円体高へ換算した実体化列。PostGIS型はSRID 4326・MultiPolygon3次元必須(対になる列と同じ形のCHECK)。NULL可(同上)
AIRSPACE_RESTRICTIONcircle_center_lng / circle_center_lat / circle_radius_mgeometry_type=CIRCLEのときのみ設定する正規化前の元値。CHECKで「CIRCLEなら3列ともNOT NULL、それ以外なら3列ともNULL」を保証する
AIRSPACE_RESTRICTIONmin_altitude_agl / max_altitude_agl床面(floor)・天面(ceiling)の対地高度。飛行計画のFLIGHT_PLAN_AREA.min_altitude_agl / max_altitude_aglと同じ意味である(高度は床面と天面を格納する)。NULL可 — 取得元が高度を持たない場合と、categoryにより高度の概念がない場合(レッドゾーン・イエローゾーン等)。天面が傾斜する領域では領域全体の最小・最大を保持し、頂点ごとの正確な高度はtop_bottom_3d_geometryのZ値が持つ。単位はaltitude_unitCHECKは両方に値がある場合のみ床面が天面を超えないことを保証する
AIRSPACE_RESTRICTIONmin_altitude_amsl / max_altitude_amsl平均海面高度。地表標高(DEM)から換算した領域内の全頂点における換算値の最小・最大(領域全体の代表値)。床面・天面ではなく統計値であるためmin/maxの名前を使う。NULL可(未取得時はNULLのまま遅延充填する)
AIRSPACE_RESTRICTIONmin_altitude_wgs84 / max_altitude_wgs84WGS84楕円体高。ジオイド高から換算した領域内の全頂点における換算値の最小・最大(同上の統計値)。個々の頂点のZ値と一致するとは限らない。NULL可(同上)
AIRSPACE_RESTRICTIONvalid_from / valid_toDIPSのstartTime / finishTime。TIMESTAMPTZ・NOT NULL。DIPSはタイムゾーン指定子を持たないJSTで返すため取り込み時にUTCへ正規化する。無期限は9999-12-31T23:59:00が入る。CHECKvalid_fromvalid_to以前であることを保証する
AIRSPACE_RESTRICTIONaltitude_unit本行が保持する高度値の単位。mの1値。OASのAltitudeUnitと同値。NULL可(高度列がすべてNULLの行では記述対象がない)。対象は*_altitude_*の6列とgeometryのZ値で、同じ行にあるcircle_radius_mは対象外(高度カラムだけが単位を列名から外す)。高度を取得元から格納する方針のため、ソースによって単位が異なりうる
AIRSPACE_RESTRICTIONaltitude_reference接尾辞のないgeometry系カラム(geometrytop_bottom_3d_geometry)のZ値が準拠する高度基準。AGL / WGS84 の2値で、OASのAltitudeReferenceおよびflight_planning.altitude_reference_typeと同じ値域だが、現時点では常にAGL(飛行計画のflight_plan_area.altitude_referenceと同じ扱い)。_wgs84列の基準は列名で自明なため本カラムの対象外。NULL可(Z値を持つ列がない行)。スカラーの高度列は列名に基準を持つため対象外。取得元が平均海面高度(AMSL)基準で高度を与える場合は、列名に基準を持つmin/max_altitude_amslへ格納し、本列の値域は増やさない(同じ意味の型に別の値域を持たせないため。制限分類の列挙値は1つに統一すると同じ理由)

高度カラムだけが単位を列名から外す

Section titled “高度カラムだけが単位を列名から外す”

高度カラムの命名は_m_を含めない。 PR #211が 飛行計画ドメインでmin/max_altitude_m_{agl,amsl,wgs84}min/max_altitude_{agl,amsl,wgs84}へ改名し、 単位は列名に埋め込まずaltitude_unitへ外出しする方針を採ったため、本ドメインも同じ命名に揃える。 一方で基準(aglamslwgs84)は列名に残す。3基準は択一ではなく同じ高度を3通りに換算した値を 同時に保持する列であり、単一の判別カラムでは表せないためである。

この方針は高度カラムに限る。circle_radius_mは単位を列名に持ったままとする。 飛行計画ドメインの FLIGHT_PLAN_AREA_CIRCLE.radius_mFLIGHT_PLAN_AREA_ROUTE.buffer_mも PR #211 の適用後も_mを 保持しており、同PRの改名対象は_altitude_m_だけであった。単位を名前に持つ列はバックエンド全体で この3つ(radius_m / buffer_m / circle_radius_m)のみである。

ただしこの区別は設計原則から導かれたものではない。 altitude_unitの根拠として書かれている 「単位の追加変更に備える」(db/schema/flight_planning.sqlaltitude_unit_typeのコメント、および OASのAltitudeUnitの「将来的に単位追加・変更がありうる」)は、水平距離にも等しく当てはまる。 両者を実際に区別できる理由はGeoJSONの座標配列(RFC 7946のPosition)に要素名がなく、Z値の単位を 宣言する場所が他にないという点だけである。名前を持つスカラープロパティ(circleRadiusM)は 名前に単位を書ける。

本ドメインのgeometryもZ値を持ちうるため(geometryの次元は縛らない)、 この理由はそのまま当てはまる。加えて高度を取得元から格納する方針にしたことで、ソースによって単位が 異なりうるという実質的な理由も生じた。circle_radius_mも同じ要求を列名で満たしており、違うのは 記録場所だけである。

飛行計画ではaltitude_unitFLIGHT_PLAN_AREAradius_mFLIGHT_PLAN_AREA_CIRCLEと別テーブルに あるため同居しないが、本ドメインは円の元値をインラインで持つ判断(決定事項のD8)を採った 結果、両者が同じ行に並ぶaltitude_unitの適用範囲を誤読しやすいため、列コメントにも対象外である旨を 書いている。

記録場所をどちらに寄せるかはドメイン単位で決めるべきことではない。 単位の記録場所に関する バックエンド共通規則は存在せず(conventions.mdの命名規約に単位の項目がない)、 本ドメインは飛行計画との列名の一致を優先して現状に揃えたにすぎない。共通規則として決める必要があるため followups.mdへ種別他ドメイン合意で登録した。

altitude_referencegeometry系カラムのZ値の基準を宣言する。 飛行計画ドメインの FLIGHT_PLAN_AREAと同じ役割・同じ値域(AGLWGS84)である(db/schema/flight_planning.sqlの 同列コメント「接尾辞のないgeometry系カラムのZ値が準拠する高度基準」)。スカラーの高度列は列名に 基準(aglamslwgs84)を持つため対象外で、geometryのZ値だけは列名に基準を書く場所がない。

飛行計画ドメインは高度を必ず持つため両列がNOT NULLだが、本ドメインは高度の概念がないcategory (レッドゾーン等)があるためNOT NULLにできない。かわりに相関のCHECK制約で、高度値やZ値を持つ行が メタデータを欠かないことを保証する(ck_airspace_restriction_altitude_unit_requiredck_airspace_restriction_altitude_reference_required)。OASのrequiredからも両プロパティを外している。

APIが返すのはUTMが採番したidである

Section titled “APIが返すのはUTMが採番したidである”

AirspaceRestrictionProperties.restrictionIdAirspaceRestrictionConflict.airspaceRestrictionIdは どちらもAIRSPACE_RESTRICTION.idformat: uuid)を返す。同一性の判定に使うIDをUTMが採番した1系統に 揃えるという趣旨であり、取得元の識別子を隠すという趣旨ではない。external_idAirspaceRestrictionProperties.externalId(任意項目)として参照できる。

同一性の判定にUTMが採番したidを使う理由は次のとおりである。

  • データソースが増える前提と噛み合わない。現時点の実データはDIPSの飛行禁止エリアのみだが、 国土地理院提供のデータセットや手動登録が加わる想定である。取得元の識別子を返す形にすると、 外部IDを持たないソースのために採番規則を作ってexternal_idを全ソースへ強制することになり、 APIの都合がデータモデルの制約として波及する
  • 値の出所が1系統に保たれる。同一性の判定に使う値が常にUTMの採番であれば、クライアントが値の形から 出所を推測する余地がない
  • 利用者にIDを見せる必要がない。画面上の同一性判定に使えるユニークなIDであれば足り、その値が DIPS由来である必要はない
  • 飛行計画側の実装も減るconflict_detection.airspace_restriction_idは既にuuidであり、 応答時に本ドメインを引いてexternal_idへ解決する処理が不要になる

本設計が負う契約はidの安定性である。再同期でも振り直さず、external_id(持つソースの場合)で 突き合わせて維持する。確認はIssue #136conflict_detectionからの参照はスキーマが異なるためFK制約を張らない。

これは飛行計画ドメインが「外部IDを持たないデータソースと競合した場合に必須項目の airspaceRestrictionIdへ何を入れるか」として空域制限ドメインへ預けていた未決事項 (flight-plan-er.md) への回答でもある。external_idはNULL可とし、外部IDを持たないソースのための採番は行わない。

external_idはAPIからも参照できる。 取得元の資料と突き合わせる用途があるため、 AirspaceRestrictionProperties.externalIdとして公開する(maxLength: 255任意項目)。 外部IDを持たないデータソースでは未設定になり、sourceKindGSIMANUALかどうかで有無が判断できる。 飛行計画側のAirspaceRestrictionConflictには追加しない。同スキーマは競合の検出結果のみを表し、 conflict_detectionは検出時点のスナップショットとしてuuidと種別しか持たないため、追加すると 本ドメインを引いて解決する処理が復活してしまう。

制限分類の列挙値は1つに統一する

Section titled “制限分類の列挙値は1つに統一する”

同じ「空域制限の種別」に対し、2つのOASがそれぞれ9値のenumを別名で定義していた。値の意味は1対1に 対応するが名前が一致せず、ドメインをまたぐたびに変換が必要になっていた。

変換を作らず、列挙値そのものを1つへ統一する。 定義をdocs/openapi/domain.yaml(両audienceが 共有する共通型の置き場)へ移し、geospatial.yamlflight-planning.yamlの双方が$refする。 GeoSpatial側のAirspaceCategoryは廃止し、AirspaceRestrictionProperties.categoryは共通の AirspaceRestrictionTypeを参照する。

旧 geospatial AirspaceCategory統一後 AirspaceRestrictionType意味
AIRPORTAIRPORT_VICINITY空港等の周辺空域
DIDDENSELY_INHABITED_DISTRICT人口集中地区
RED_ZONESMALL_UAV_PROHIBITION_RED_ZONEレッドゾーン
YELLOW_ZONESMALL_UAV_PROHIBITION_YELLOW_ZONEイエローゾーン
ORDINANCEORDINANCE_DESIGNATED_AREA条例等で定めるエリア
MANNED_AIRPORTMANNED_AIRCRAFT_TAKEOFF_LANDING_AREA有人機離着陸エリア
EMERGENCYEMERGENCY_OPERATIONS_AIRSPACE緊急用務空域
OTHER1OTHER_1その他1
OTHER2OTHER_2その他2

値は飛行計画側(AirspaceRestrictionType)に寄せた。 同じ値がflight_planning.airspace_restriction_type_typedomain.model.AirspaceRestrictionType・生成コードに既に存在し、疎通試験環境のDBにも適用済みである一方、 AirspaceCategory側は未実装の応答と本設計の新規列にしかなく、寄せ替えの影響が小さいためである。

統一の結果、次がすべて不要になった。

  • AirspaceCategoryからAirspaceRestrictionTypeへの変換(所有ドメイン・置き場所・全域性・網羅性の いずれも論点にならない)
  • 変換のために新設する予定だったdomain.model.AirspaceCategory
  • 両者の食い違いを追跡するフォローアップ(followups.mdの該当行は本PRで解消)

検索エンドポイントのパス(/api/v1/geo/airspace-restriction/red-zone/search等)は短い表記のままである。 パスと列挙値は別物であり、パスを変えるとcategoryごとにエンドポイントを分けた構造まで触ることになるため 本設計では揃えない。

提供元は最小モデルでは列として持つ

Section titled “提供元は最小モデルでは列として持つ”

概念モデルのAIRSPACE_SOURCEはエンドポイントURL・認証情報参照・最終フェッチ時刻を持つが、 これらはいずれも収集処理のための属性であり、事前投入では値が定まらない。参照側が必要とするのは kind(APIのsourceKind)だけであるため、最小モデルではAIRSPACE_RESTRICTION.source_kindという enum列として持つ。

収集を実装する際はAIRSPACE_SOURCE表を起こし、本列をsource_id(FK)へ置き換える。 既存行の移行を伴う破壊的なスキーマ変更になるため、db/migrations/のマイグレーションが必要になる。 この代償はスコープの決定に対する既知の申し送りである。

リビジョンは履歴が必要になった時点で起こす

Section titled “リビジョンは履歴が必要になった時点で起こす”

概念モデルは内容をAIRSPACE_RESTRICTION_REVISIONに分けているが、本設計は1テーブルに平坦化する。 内容の変更は行のUPDATEで反映し、変更履歴は持たない。

  • リビジョンが要るのは収集を実装してからである。 事前投入では更新が発生しないため、履歴として 残るものがない。AIRSPACE_SOURCEAIRSPACE_SOURCE_SYNC_LOGを落としたのと同じ理由である
  • 平坦化で消える複雑さが大きい。 相互参照(AIRSPACE_RESTRICTION.current_revision_idと リビジョン側のrestriction_id)がなくなるため、db/README.mdの 「ALTERを書いてよい例外(循環FK)」を使う必要がない。current_revision_idをNULL許容にする妥協も、 登録を「制限をINSERT → リビジョンをINSERT → 制限をUPDATE」の3ステップで1トランザクションに 収める必要もなくなり、取得元からのUPSERT 1文で済む
  • 参照側の書き直しは発生しない。 平坦化をやめて後から分割すると参照側(GeoSpatialの検索・ 競合判定)のクエリを書き直すことになるが、どちらもまだ未実装(PR-04・PR-11)である。 リビジョンは永続化の都合であってドメインモデルは「空域制限」1つであり、後から起こしても ポートの signature は変わらない
  • 変更時刻を持たない。 「いつ内容が変わったか」はリビジョンが答える問いであり、平坦化した モデルでは答えられない。collected_atは取得の時刻であって内容の変更時刻ではないため、 updated_atのような列を足して埋めた気にはしない

後から起こすときのマイグレーションは次の順で行う。データ移送を伴うためpsqldefの宣言的同期では 扱えず、db/migrations/NNN_*.sql(sql-migrate)の例外的マイグレーションになる (db/README.mdの「ツール役割分担」)。

  1. AIRSPACE_RESTRICTION_REVISIONを新設する
  2. 既存行の内容列をrevision_no=1のリビジョンとして移送する
  3. AIRSPACE_RESTRICTIONcurrent_revision_idを追加して張り替える
  4. AIRSPACE_RESTRICTIONから内容列を落とす

min_altitude_aglmax_altitude_agl床面(floor)と天面(ceiling)の対地高度である。 飛行計画ドメインのFLIGHT_PLAN_AREAの同じ2列と同じ意味である。名前もPR #211の改名後の形に揃えている(同PRは未マージのため、現在のmainでは飛行計画側がmin/max_altitude_m_aglのままである) (geospatial.yamlGeometryが「用いる対地高度は、そのリング(線・面)が天面か床面かで決まる。 天面はmaxAltitudeMAgl、床面はminAltitudeMAgl」と定める)。

値は取得元が提供するものを収集時に格納する。UTM側で一律の値を与えない。

  • どちらもNULLを許容する。 取得元が高度を提供しない場合(DIPS FPRのレスポンスに高度フィールドは 存在しない)と、categoryによって高度の概念がない場合(小型無人機等飛行禁止法のレッドゾーン・ イエローゾーンなど)がある
  • 床面は通常0(地表)である。 空域制限の床面は地表として扱う
  • 「地表から150mAGLまで」を格納しない。 この値はGeoSpatialの3D表示のための暫定的な扱いであり、 空域制限そのものの属性ではない。暫定値を行へ焼くと、表示上の決定がデータとして残り、 実データを入れる段で意味が二重になる。アプリケーション層の定数として持つ案も採らない (どの層に置いても暫定値が本番データの位置を占めることは変わらない)。ソースが高度を提供するまで NULLのままとし、提供され次第その値を格納する
  • 傾斜する天面ではスカラーは領域全体の最小・最大にとどまる。 空港周辺の 制限表面のように 天面が場所によって変わる領域は、スカラー1個では表せない。頂点ごとの正確な高度は top_bottom_3d_geometryのZ値が持ち、そちらが正である (上面・下面は3次元のgeometryで持つ
  • AMSL・WGS84の換算値スカラー4列は持つ。 GeoSpatialの検索がmin/maxAltitudeMAmslmin/maxAltitudeMWgs84をFeatureのプロパティとして返し(geospatial.yamlAirspaceRestrictionProperties)、AGL→AMSLに地表標高(DEM)、AMSL→WGS84にジオイド高の参照を伴う。 応答のたびに算出すると地図表示の頻度でDEMへ問い合わせることになり見合わない。飛行計画・収集した 他社計画が同じ換算値カラムを持つため、3つ目の消費者である本ドメインも対称にする。未取得時はNULLとし 遅延充填する
  • altitude_unitaltitude_referenceを持つ。 取得元によって高度の単位・基準が異なりうるため 行ごとに保持する(図1 カラム補足の該当行)

OAS側もminAltitudeMAgl / maxAltitudeMAglは必須ではなく、値は取得元が提供するもので、種別により 高度の概念がない場合は未設定である。AirspaceRestrictionFeatureのZ値も頂点ごとに異なりうる。

geometryのCHECK制約はSRID(4326)と種別(PolygonMultiPolygon)のみを保証し、次元は縛らない

2次元に固定しないのは、API側の都合をDBの制約に焼かないためである。応答時にZ値を付与するかどうかは 応答の組み立て方の問題であり、格納できる次元を縛る理由にならない。10月デモで投入するデータが2次元で あることも、モデルを2次元限定に倒す理由にはならない。

次元を縛らないことで、傾斜した制限表面を頂点ごとのZ値で表せる (高度は取得元が提供する値を格納する)。10月デモでは2次元の データのみを投入するが、これは運用上の事実であって制約ではない。

ST_Intersects等の位相演算は2次元へ射影して判定されるため、2次元と3次元が混在してもbbox検索・ 競合判定の結果は変わらない。

上面・下面は3次元のgeometryで持つ

Section titled “上面・下面は3次元のgeometryで持つ”

上面(天面)・下面(床面)を2枚のリングで表したMultiPolygonZtop_bottom_3d_geometryに持つ。 飛行計画ドメインのFLIGHT_PLAN_AREA.top_bottom_3d_geometryDIPS_FLIGHT_PLAN.top_bottom_3d_geometryと 同じ構造で、coordinates[0]が上面、coordinates[1]が下面のリングである。

スカラー2列では傾斜した天面を表せないため、この列が必要になる。 空港周辺の制限表面のように天面が 場所によって変わる領域では頂点ごとにZ値が異なり、max_altitude_aglは領域全体の最大値にとどまる。 頂点ごとの正確な高度を保持できるのは3次元のジオメトリだけである。一様な天面の場合は水平形状と スカラー2列から組み立てられる(飛行計画と同じ導出)。

  • 3次元を必須とする。 上面・下面をZ軸方向に離して組み立てるため2次元では表現できない。 CHECKST_NDims=3を課す(飛行計画のck_flight_plan_area_top_bottom_3d_geometryが 同じ制約を持ち、2次元の型修飾子では登録できないことが実測で確認されている。 followups.mdの該当行)
  • NULLを許容する。 高度が取得できない行(レッドゾーン等、categoryにより高度の概念がないもの)では 上面・下面を組み立てられない
  • 並びを強制する制約は持たない。 CHECKが見るのは種別・SRID・次元だけで、coordinates[0]が上面で あることは投入・収集する側の責任になる。逆順で格納されると、天面を上面リングから組み立てるAPIは 床面を天面として返す。飛行計画ドメインでは同じ前提が実際に破れた(followups.mdの該当行)
  • 索引は張らない。 bbox検索と競合判定は接尾辞のないgeometry(水平形状)を対象にする。 本列を条件に使う実装が現れた時点で判断する
  • geometryとの役割分担: geometryは水平形状でbbox検索・競合判定に使い、 top_bottom_3d_geometryは3D表示のための上下面を持つ。飛行計画のplan_geometrytop_bottom_3d_geometryの関係と同じである。APIがAirspaceRestrictionFeature.geometryとして 返すのは天面であり、DBのgeometry列をそのまま返すわけではない(天面のZ値は top_bottom_3d_geometryの上面リング、またはZ値を持たない行では水平形状から組み立てる)

APIが床面を返すかは未決である。 現在のAirspaceRestrictionFeatureは「geometryは3D表示補助として 天面(ceiling)を表し、床面(floor)は返却しないため、クライアント側で地表面に沿わせて表示する」と 定めており、飛行計画のdataType=TOP_BOTTOM_3Dに相当するFeatureを持たない。DB側に上下面を持つ以上、 APIも上下面を返す形(飛行計画と同じdataType相当の分岐)へ寄せる余地があるが、これはGeoSpatialの 応答契約の変更であるためfollowups.mdへ登録した。

WGS84基準の形状も実体化列で持つ

Section titled “WGS84基準の形状も実体化列で持つ”

GeoSpatialはaltitudeReferenceAGLのFeatureとWGS84のFeatureを返す。前者はgeometrytop_bottom_3d_geometryのZ値をそのまま使えるが、後者は頂点ごとに 対地高度 + 地表標高 + ジオイド高 を算出した楕円体高が要る。飛行計画ドメインはこの換算コストを 避けるため形状を_wgs84列として実体化しており (飛行領域はGeoSpatialが返す形状ごとに実体化列を持つ)、 本ドメインも同じ形で実体化列(geometry_wgs84 / top_bottom_3d_geometry_wgs84)を持つ。

以前の設計は「充填する処理とDEM・ジオイド高の取得経路が未決のまま列を複製しても、常にNULLの列が 増えるだけ」という理由で実体化列を持たない判断を採っていた。10月デモは事前投入(db/seed/)で AGL・WGS84の両方の形状を直接ロードする前提であるため、この前提が成り立たない。 収集を実装する までアプリ側に換算処理は無いが、投入時点で換算済みの値を列へ入れることは可能であり、飛行計画側の 充填処理と対称化するかを待つ理由がなくなった。

  • geometry_wgs84geometryと同じPostGIS型(種別・SRID)を保証する。 次元はgeometryと同様に 縛らない(geometryの次元は縛らない
  • top_bottom_3d_geometry_wgs84top_bottom_3d_geometryと同じく3次元(MultiPolygonZ)を必須とする
  • いずれもNULLを許容する。 標高(DEM)・ジオイド高の取得経路を持つ収集を実装するまでは、 投入元がWGS84楕円体高を提供しない場合にNULLのまま格納できる。事前投入以外の経路(将来の収集)で 換算値が未取得の場合も同様にNULLとなる
  • スカラーの換算値(min/max_altitude_amsl / min/max_altitude_wgs84)とは別に持つ判断は変わらない。 あちらは領域あたり1つの値でAPIの応答プロパティに直結するのに対し、こちらは頂点ごとのZ値を持つ 形状そのものである
Feature必要なZ値本設計から生成できるか
altitudeReference=AGL取得元が与えた対地高度できる。 geometrytop_bottom_3d_geometryが持つZ値をそのまま返す。取得元が高度を持たない行では2次元の座標を返し、min/maxAltitudeMAglも未設定になる
altitudeReference=WGS84頂点ごとに 対地高度 + 地表標高 + ジオイド高できる(事前投入したデータに限る)。 geometry_wgs84top_bottom_3d_geometry_wgs84が持つZ値をそのまま返す。列がNULLの行(収集実装前で換算値が投入されなかった行)は生成できない
  • AGL・WGS84いずれのFeatureもマッピング上の問題がない。 ただし収集を実装するまでは、事前投入時に 換算済みの値を入れなかった行のWGS84 Featureは生成できない(列がNULLのため)
  • スカラーのminAltitudeMAmslmaxAltitudeMAmslminAltitudeMWgs84maxAltitudeMWgs84は 形状の実体化列とは独立したカラムのため、別々に埋まりうる(一方が未取得でも他方は取得済みという 行がありうる)

円の元値はサブテーブルに分けずインラインで持つ

Section titled “円の元値はサブテーブルに分けずインラインで持つ”

飛行計画ドメインは円の半径・経路のバッファ幅を形状ごとのサブテーブル(FLIGHT_PLAN_AREA_CIRCLEFLIGHT_PLAN_AREA_ROUTE)へ分けているが、本ドメインはcircle_center_lng / circle_center_lat / circle_radius_mAIRSPACE_RESTRICTIONにインラインで持つ。

飛行計画側の同じ節が、収集データについては分割しない方針を明示している。 「DIPSから収集した領域(DIPS_FLIGHT_PLAN)は分割しない: 収集データはDIPS応答のパススルーであり、 CIRCLEのときだけradius_mが入る単純な構造にとどまる。UTM内部の飛行領域のように入力検証・制約付けの 対象ではないため、インライン保持のままとする」 (flight-plan-er.md)。 空域制限は全ソースが外部由来のパススルーであり、この前例と同じ扱いになる。意味の異なる値を1列に 兼ねさせる問題(半径とバッファ幅の兼用)も本ドメインには生じない。

条件付き必須(geometry_type=CIRCLEなら3列そろって値を持つ)はサブテーブルに分けると 「行が存在しない」という不正状態を生むが、インラインならCHECK制約で表現できる。

idの安定性は投入する側が担保する

Section titled “idの安定性は投入する側が担保する”

flight_planning.conflict_detection.airspace_restriction_ididを保持するため、再投入でidが 振り直されると競合の記録が参照先を失う。この安定性をアプリケーションは保証しない。

  • external_idを持つソース(DIPS_FPR)はON CONFLICT (external_id)でUPSERTでき、idは維持される
  • external_idを持たないソース(MANUAL)は突き合わせる列がないため、投入側が既存のidを 引き継いで更新するidで特定してUPDATEする)。UNIQUE制約はNULLを重複扱いしないため、 そのままINSERTすると同じエリアが2行になる

db/seed/の規約は冪等性のためDELETE→INSERTを求めており(db/README.md)、 そのまま適用するとidが振り直される。投入手順の側で規約との整合を取る必要があるため followups.mdへ登録した。

external_idを持たないソースのための採番を行わない判断(決定事項のD7)は、APIが返す値を 1系統に保つためのものであり、投入運用で人が識別子を付けることを禁じるものではない。

飛行計画・Assetの各テーブルはorganization_idを持つが、本ドメインは持たない。空域制限は 公的機関が公開する情報であり、全組織が同一のデータを参照する。組織ごとに見えるものが変わらないため、 絞り込みの軸にも権限判定の軸にもならない。

ADR-009は 「ユーザーや組織ごとのデータ分離が必須要件」としてRLSを前提に置くが、本ドメインの2テーブルはその対象外 である。分離すべき単位が存在しないため、RLSポリシーを張る対象にならない。同じ理由で ADR-011が 言う「RLS不要な共有データセット」に当たり、DIDを静的PMTilesで配信する判断(決定事項のD2)とも整合する。

将来、組織が独自の飛行禁止エリアを登録する要件(source_kind=MANUALの一部)が出た場合は、 そのときに所有組織の概念を導入する。現時点のMANUALは運用者が全組織向けに登録するものを指す。

検索条件に合わせて索引を張る

Section titled “検索条件に合わせて索引を張る”

GeoSpatialの検索はcategoryごとのエンドポイントに分かれ、bboxstatus・有効期間で絞る (geospatial-api-design.mdの6.3節)。 これに対し次の索引を張る。

対象索引用途
AIRSPACE_RESTRICTION.geometryGiST(idx_airspace_restriction_geometrybboxとの交差判定、飛行計画領域(area_type=ROUTE/POLYGON)との競合判定
AIRSPACE_RESTRICTION.geometry::geographyGiST式インデックス(idx_airspace_restriction_geometry_geography飛行計画領域(area_type=CIRCLE)との競合判定(ST_DWithin
AIRSPACE_RESTRICTION(category, status, valid_from, valid_to)idx_airspace_restriction_searchカテゴリ別検索の絞り込み
AIRSPACE_RESTRICTION.external_idUNIQUE制約UPSERT、および競合応答時のidからexternal_idへの解決

上表がDDLに置く索引・一意制約の全量である。

メートル単位の距離判定にST_DWithingeographyキャストで行う場合、geometry::geographyに対する 式インデックスが別途要る(USING GIST (geometry)は式が異なるため使われない)。空域制限との競合判定が area_type=CIRCLEST_DWithinを発行することが決まったため、上表の式インデックスを置く (飛行計画側は判定対象が対象リビジョンの1行に絞り込まれており探索されないため、同じ式の索引は 置いていない。飛行計画側に張るかどうかを再検討する条件はfollowups.mdの 「飛行計画側のgeometryに式インデックスを張っていない」の行に記録している)。

ID論点決定根拠・影響
D1同じ意味に別名の列挙値が2つある問題をどう解くか列挙値を1つへ統一する。 定義をdocs/openapi/domain.yamlへ移し両OASが$refする。値は飛行計画側(AirspaceRestrictionType)に寄せ、AirspaceCategoryは廃止する制限分類の列挙値は1つに統一する。変換そのものが不要になる
D2category=DENSELY_INHABITED_DISTRICTの行をDBに持つか10月デモでは持たない。デモ以降は持つ。 デモ期間はck_airspace_restriction_no_didで行の存在を禁じ、DIDはGSI由来の静的PMTilesアーカイブとしてのみ配信する。デモ以降は同制約を落とし、DIDも他の分類と同じく行として保持する配信のためのデータと判定のためのデータは役割が別である。 静的PMTilesでの配信は、更新頻度が低く地図表示が目的のデータ(DID・レッドゾーン/イエローゾーン等)に合った手段だが、配信方式は競合判定の対象から外す理由にならない。10月デモの間はDIDの行を持たないためBusinessLogicSpecifications.mdの競合判定の対象から外れるが、これはデモ期間限定の制限である(残作業はfollowups.mdに登録している)。行を持つ判断へ変えてもD1の変換は壊れない。配信側の判断はADR-011が「背景地図・共有データセットはRLS不要でS3 + CloudFrontの静的配信が最適」とする区分に一致する
D3高度に何を格納するか。「地表から150mAGLまで」は固定値として扱えるか取得元が提供する値を格納する。 AGL 2列はNULL可とし、高度の概念がない種別・高度を提供しないソースを表せるようにする。150mの固定値はDBにもアプリ層にも持たない。傾斜面はgeometryの頂点ごとのZ値で表し、AGL 2列はその最小・最大とする高度は取得元が提供する値を格納する。150mAGLは3D表示のための暫定的な扱いであり、空域制限そのものの属性ではない。OAS側も必須ではない
D3-2WGS84基準の形状(*_geometry_wgs84相当)を実体化列で持つか持つ。 geometry_wgs84 / top_bottom_3d_geometry_wgs84を追加する。10月デモは事前投入でAGL・WGS84の両方を直接ロードするため、飛行計画側の充填処理を待つ理由がないWGS84基準の形状も実体化列で持つ
D3-3geometryの次元を2次元に固定するか固定しない。 CHECK制約はSRIDと種別のみを保証するgeometryの次元は縛らない。傾斜面を頂点ごとのZ値で表すために必要。10月デモのデータが2次元であることは運用上の事実で、モデルの制約にはしない
D4geometryのCHECK制約を共通関数やDOMAIN型へ括り出すか括り出さない。 本テーブルでも制約を複製し、共通化は別途判断する本テーブルが3つ目のgeometryテーブルとなり、followups.mdが「3つ目のテーブルを追加する際に、規約改定とpsqldefでの検証をあわせて判断する」とした契機に当たる。判断を先送りした事実を同台帳へ追記する
D5スキーマ名とファイル名スキーマairspace_restriction、ファイルdb/schema/airspace_restriction.sqlテーブル名はairspace_restriction.airspace_restrictionとなる。スキーマ名とテーブル名が重なるのはasset.assetdb/schema/asset.sql)の前例に倣う。ファイルは辞書順でasset.sqlより前に適用される
D6external_type_idAirspaceRestrictionTypeの対応を本ドメインが持つか持たない。 本ドメインが持つ正規化軸はcategoryのみとするexternal_type_idはソース固有の生値としてのみ保持し、APIでは非公開とする
D7APIが返す空域制限IDは何か(飛行計画ドメインから引き取った未決事項)同一性の判定に使うIDはUTMが採番したid(uuid)とする。 external_idはNULL可とし採番も行わないが、AirspaceRestrictionProperties.externalId(任意項目)として参照できるようにする。飛行計画側のAirspaceRestrictionConflictには追加しないAPIが返すのはUTMが採番したidである。データソースが増えても1系統のIDで通せ、飛行計画側に解決処理も要らない
D8円の元値をサブテーブルへ分けるか分けない。 同じテーブルにインラインで持つ円の元値はサブテーブルに分けずインラインで持つ。収集データを分割しない飛行計画側の前例に倣う
D9organization_idを持つか持たない。 全組織が同一のデータを参照する組織スコープを持たない
D10単位の記録場所を行内で統一するか統一しない。 高度はaltitude_unit、水平距離は列名(circle_radius_m)のまま、飛行計画ドメインの列名に揃える。min_altitude_m_aglのように名前へ単位を戻してaltitude_unitを落とす案は採らない高度カラムだけが単位を列名から外す。本ドメインがaltitude_unitを持つ理由は、保存した数値を将来の単位方針変更後も解釈できるようにするため(固定値の高度を記録するのと同じ理由)。記録場所をどちらへ寄せるかはバックエンド共通規則の話であり、followups.mdへ種別他ドメイン合意で登録した
D12リビジョン表を最小モデルに持つか持たない。 1テーブルに平坦化し、履歴が必要になった時点でマイグレーションで起こすリビジョンは履歴が必要になった時点で起こす。循環FK・current_revision_idのNULL許容・3ステップの登録がまとめて不要になる。参照側(PR-04・PR-11)は未実装で書き直しのコストがない
D11収集専用の列(取得元レスポンス原文・無変更判定のハッシュ)を最小モデルに持つか持たない。 収集の実装時に設計するスコープAIRSPACE_SOURCEAIRSPACE_SOURCE_SYNC_LOGを採用しないのと同じ理由。収集時に決めることはfollowups.mdの台帳に置く

本設計はorigin/mainを起点にしている。PR #178のレビュー指摘対応は当初 PR #190にまとめられていたが、同PRはクローズされ 指摘ごとに分割された。本設計が前提にしている決定はいずれもその分割後のPRにあり、まだmainに入っていない。

命名・採番の方針は本設計に取り込み済みである。

取り込んだ決定出所
高度カラム名から単位を外す(_altitude_m_*_altitude_*)とaltitude_unitへの外出しPR #211
IDの採番をUUIDv7にする(common.UuidV7)。空域制限のOASはrestrictionIdstringとしformat: uuidでバージョンを縛っていないため、運航調整ドメイン(ASTM OASがUUIDv4Formatを強制)のような制約を受けないPR #206

一方、ファイル構成に関わるものは本設計には取り込めない。 PR #197のマージ後に次を行う。

対応内容
order.txtへの登録#197がDDLの適用順の正を辞書順からdb/schema/order.txtへ移す。order.shorder.txtdb/schema/*.sql完全一致を要求するため、airspace_restriction.sqlを登録しないとDDL関連のタスクが失敗する。登録位置は「2. 各ドメインのテーブル」(ドメイン間に適用順の依存はない)
拡張定義ファイル名#197が00_shared_extensions.sql00_extensions.sqlへ統合する。本ドメインのDDL冒頭の参照を差し替える(#197はgrep -rn '00_shared_extensions'が0件であることを確認しているため、残すと再導入になる)

PR #199・#211はdocs/data-model/followups.mddocs/data-model/flight-plan-er.mdを、#211はさらに docs/geospatial/design/geospatial-api-design.mdを変更する。本設計も同じ3ファイルに触れているため、 先にマージされた側に合わせて解消する。

OAS・他ドキュメントへ反映が必要な項目

Section titled “OAS・他ドキュメントへ反映が必要な項目”

次はいずれも本PR内で反映済みである。

反映先内容
docs/openapi/domain.yaml共通enumAirspaceRestrictionTypeを新設(D1)
docs/openapi/frontend/geospatial.yamlAirspaceCategoryを廃止し共通enumへ$refrestrictionIdformat: uuidへ、min/maxAltitudeMAglを必須から外し説明を「取得元が提供する値」へ、AirspaceRestrictionFeatureのZ値の記述を頂点ごとに異なりうる形へ
docs/openapi/frontend/flight-planning.yamlAirspaceRestrictionTypeを共通enumへ$refairspaceRestrictionIdformat: uuid
docs/geospatial/design/geospatial-api-design.md5.2節の対応表(restrictionIdの対応先・列挙値・高度の各行)
docs/data-model/flight-plan-er.md外部IDの未決事項をuuid返却で決着させた旨と、external_idへの解決が不要になった旨

残る未決事項はfollowups.mdの台帳を参照する。