Skip to content

Draw many polylines as tiles, with the Boston roads sample - #119

Open
wf9a5m75 wants to merge 2 commits into
feat/overlay-on-addedfrom
feat/polyline-tiling
Open

wf9a5m75 wants to merge 2 commits into
feat/overlay-on-addedfrom
feat/polyline-tiling

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

なぜ

ポリゴンと同じように、ポリラインも数万本を渡せるようにするためです。ボストン市内の道路(54,645 本・234,765 頂点)をネイティブの線で描くと、iOS の Google で全 onAdded まで 114 秒・1.5 GB、Android の Google は Java ヒープが尽きて落ちます。ポリゴンのタイル化(feat/polygon-tiling)と同じ仕組みで、多数のポリラインをラスタータイルとして描きます。

設計の要点(web・Android・iOS で同じ)

  • Polylines(states): Polygons と同じ形のまとめて渡す部品。そのインスタンスが足した id だけを足し引きするので、複数の Polylines / Polyline が同じ地図で共存できます(iOS は既存の Polylines(_:) のまま)。
  • PolylineTilingOptions / PolylineTileSet / PolylineTiler / PolylineController.tiler: 名前と既定値(2,000 本以上または頂点 50,000 以上)はポリゴンと 1 対 1。レイヤー id は mc-polyline-tiles。
  • ポリゴンのタイル化と内部を共通化: タイル集合(ルート・URL の版・16 ms のまとめ・削除)、振り分け、描画(格子索引・ズームごとの簡略化・LRU キャッシュ・無効化・スクラッチのプール・PNG・キャンセル)を内部の ShapeTileSet / ShapeTiler / ShapeTileRenderer に移し、Polygon* はその上の薄い層になりました。公開 API は追加のみです。
  • 描き方: 開いた経路として、ズームの画素格子に丸め → 重複を捨て → Douglas-Peucker 0.5 px(両端は残す)。round cap / round join。線幅は画面上の幅(dp / pt / CSS px)でズームに依らない。候補は線幅の半分だけタイルの外まで引き、はみ出す分もそのまま描くので、タイルの境目で線が切れません。1 px 未満に潰れた線は線幅の点で描きます。
  • タイルにできないもの: geodesic、2 点未満、経度幅 180° 以上。その場の変更でそうなった線はネイティブ描画へ移ります(onAdded は呼ばない)。破線・キャップの指定は API に無いので対象外です。
  • onAdded: ポリゴンと同じ規則(初めて地図に載ったときだけ。タイル⇔ネイティブの移動では呼ばない)。
  • 地図ごとの指定: web はプロバイダの props / config の polylineTilingOptions、Android は compose の PolylineTiling(options)、iOS は MapViewContent.polylineTilingOptions / PolylineTiling 要素。
  • 重なり順: ポリラインのタイルはポリラインの帯の最下段(サークルの上、ネイティブのポリラインとマーカーの下)。下から raster → GroundImage → polygon(+タイル) → circle → polyline(+タイル) → marker(+タイル)。

このリポジトリで変えたこと

  • example-app にボストンの道路のページ(pages/polyline/boston/、ViewModel・データの読み込み・「Overlap check」)。読み込みの試験 BostonRoadsDataTest。
  • データ boston-city-roads.bin(1,019,811 B、gzip 597 KB): Overture transportation/segment 2026-10-21.0 の road を、Analyze Boston の市境で切ったもの。54,645 本・234,765 頂点(元 362,585)。1e-5°、直線上の頂点を 0.5 m で省く(両端は残す)。ID は持たず、種別(u8)と名前だけ。
  • 形式は MCPG v2 にヘッダの fields ビットを 2 つ足したもの(16 = 道路の種別、32 = lines: 開いた折れ線で読み手は閉じない)。3 プラットフォームの読み込み試験は同じ fixture を使います。作り方・出典・ライセンス(ODbL)は react-sdk の examples/basic/tools/boston/README.md。
  • ページ: 種別ごとの色・線幅・zIndex と凡例、読み込み中の表示、onAdded の数(全 54,645 本が来るまで)、タップで InfoBubble(種別・名前・長さ)、タイル化の有無の切り替えと「読み込み → 全 onAdded」の時間、重なり確認用の表示(ポリゴンのタイル・サークル・マーカー)。
  • gitlink: android-sdk-core・android-sdk-compose・10 プロバイダを各 feat/polyline-tiling へ。

検証(Android、追従後の 158d1ee)

  • 単体テスト(testDebugUnitTest、--rerun): core 375・compose 10・arcgis 18・googlemaps 5・here 6・longdo 6・mapbox 1・mappls 4・maptiler 9・tomtom 3・example-app 13 すべて通過。openmobilemaps は 17 件中 1 件失敗(OpenMobileMapsDriverConformanceTest、develop の 5df7b09 で入った Log.isLoggable がモックされていないため。このブランチの差分は触れていない)。
  • apiCheck: 12 モジュールすべて通過。base との差分は公開 API の追加のみ(core の削除行は非 public の PolygonTileRenderer$Model / $Simplified と、PolylineOverlay の 1 引数コンストラクタの行の位置が動いただけ)。
  • assembleDebug 通過。親ビルドの ktlintFormat は回していません。
  • 実機: 追従前の build でタブレットにてボストンの道路のページ(MapLibre / Google、タイルで全件・タップ・重なり順・切り替えのメモリ)を確認済み。追従後の APK はタブレットが adb に見えず未確認です。

時間とメモリ(ボストンの道路 54,645 本・234,765 頂点)

web(Chromium、Playwright。「全 onAdded / 次のフレーム」ms)

プロバイダ タイル z13 タイル z16 ネイティブへ切り替え タイルへ戻す
MapLibre 1758 / 1763 1357 / 1365 381 / 658 236 / 312
Google 611 / 626 226 / 308 403 / 799 250 / 332
Mapbox 1151 / 1161 923 / 937 345 / 587 262 / 345
MapTiler 1193 / 1199 596 / 603 363 / 642 238 / 312
TomTom 2077 / 2086 807 / 836 344 / 622 240 / 321
Mappls 5206 / 5226 2248 / 2260 546 / 1108 435 / 565
Longdo 4406 / 4430 1573 / 1587 564 / 990 448 / 595
Leaflet 276 / 281 317 / 410 19385 / 21150 701 / 1141
OpenLayers 264 / 270 209 / 275 1650 / 7931 224 / 291
HERE 349 / 450 376 / 489 992 / 4135 297 / 392
deck.gl 355 / 361 332 / 436 239 / 14743 284 / 1296
Cesium 610 / 627 545 / 559 874 / 7315 629 / 776
ArcGIS 1348 / 1355 292 / 303 1353 / 3656 227 / 298
Azure Maps 1139 / 1153 788 / 802 未測定 未測定
MapKit 724 / 729 391 / 397 未測定 未測定

JS ヒープ(GC 後): MapLibre ⇄ Google をタイルで 4 往復 251.0 → 266.4 → 267.0 MB(2 往復目以降は横ばい)。ネイティブだと Google の時に 475 MB 前後。MapLibre ⇄ Leaflet は 251.1 → 253.2 MB。

Android(タブレット、MapLibre)

タイル化あり ネイティブ
渡してから最初のフレーム 25 ms 49 ms
渡してから全 onAdded 1,202 ms 1,294 ms
画面が落ち着くまで(録画) 2.32 s 2.02 s

Google: タイル化ありで描画 0.87 s(録画)。ネイティブは OOM で落ちる(下記)。
メモリ: MapLibre ⇄ Google を 6 往復、PSS 899,652 K → 最大 1,060,426 K → 916,405 K、native heap 182,752 K → 181,524 K。増え続けません。

iOS(iPad、Release。「読み込み → 全 onAdded」と footprint)

タイル化あり ネイティブ
MapLibre z15 1,010 ms / 218 MB 3,961 ms / 478 MB
MapLibre z13 / z17 / z18 910 / 1,039 / 988 ms 3,826 / 3,967 / 3,838 ms
Google z15 796 ms / 360 MB 114,541 ms / 1,573 MB

ほかのプロバイダ(z16、タイル化あり): Mapbox 1,028 ms / 397 MB、MapTiler 914 / 310、Mappls 1,727 / 241、MapKit 1,021 / 393、ArcGIS 923 / 164、ArcGIS2D 943 / 370、HERE 1,175 / 483、TomTom 1,229 / 303、Longdo 2,499 / 144、OMM 895 / 394。
メモリ: MapLibre ⇄ Google を 8 往復、277 MB → 往復中 259〜396 MB → 260 MB。増え続けません。

挙動の変わる所

  • タップの判定に線幅の半分を足しました(3 プラットフォームの core の PolylineManager.find、タイルでもネイティブでも同じ)。許容距離 = 既存の指の大きさ(tapTolerance)+ 線幅 / 2。太い線は縁から指の大きさ以内であたります。複数あたれば最も近い線。外接矩形で先に候補を絞るので、5 万本でも毎タップ全点をなめません。返す位置(線上の最近点)は今までどおりです。
  • iOS TomTom: 合成スタイルで、SDK のタイル層(ポリゴン・ポリライン・マーカーのタイル)をスタイルの最上段に置きます。TomTom のスタイルは最初の symbol レイヤーより後ろに道路があり、「最初のラベルの前」では道路のタイルが覆われていたためです。そのためポリゴンのタイルも TomTom の道路・ラベルの上に来ます。annotation(ネイティブのサークル・線・マーカー)はこれまでどおりスタイル全体の上です。
  • web Google: overlayMapTypes の中でマーカーのタイルを最上位にしました(ポリゴン < ポリライン < マーカー)。3D 地図ではタイル化せず、すべてネイティブで描きます。
  • web MapLibre 系(maplibre / mapbox / maptiler / mappls / tomtom / longdo): GeoJSON で描くネイティブのポリラインを zIndex 順に並べるようにしました(これまでは渡した順)。タイルとネイティブで重なり方がそろいます。
  • iOS MapLibre / MapTiler / Mappls: raster レンダラが SDK タイル層の重なりのキーを id から取るようにしました(OverlayStacking.rasterKey(styleLayerId:))。追加の順に関係なく、ポリラインのタイルがネイティブのポリラインの下に入ります。

SDK の制約で重なり順を守れないもの

  • web Google(2D): overlayMapTypes は図形・GroundOverlay より必ず下に描かれるので、サークルとネイティブのポリゴンが道路のタイルの上に出ます(ポリゴンのタイル < 道路のタイル < マーカーは守れています)。
  • web Cesium: imagery レイヤーは ground primitive より必ず下。アプリのラスターより上に置くところまで。
  • Android HERE / ArcGIS / TomTom: ポリラインのタイルが SDK のオーバーレイ層(HERE は map item、ArcGIS は GraphicsOverlay)より下になるので、ネイティブのポリゴン・サークルがタイルの上に来ます。ポリゴンのタイルより上、マーカー(とそのタイル)より下は守れています。
  • iOS TomTom: annotation がスタイル全体の上に描かれるので、サークルがポリラインのタイルの上に来ます。ポリゴンのタイルも道路の上(上記)。
  • iOS Longdo / HERE / ArcGIS: z16 の画像を目で見た範囲では、サークルが道路のタイルの上に見えます(画素では測っていない)。iOS OMM: ポリゴンのタイルがほとんど見えません(要調査)。

確認できていないもの

  • Android Google のネイティブ比較: 54,645 本をネイティブで描かせると、main スレッドが OutOfMemoryError で落ちます(GoogleMap.addPolyline → PolylineOptions.writeToParcel、Java ヒープが上限 512 MB に到達)。1 本ずつ Parcel に詰めて GMS に渡すためで、タイル化ありなら落ちません。このため Google のタイル化あり/なしの時間は並べられていません。
  • Android の追従後の実機確認: develop を取り込んだ後の APK は、タブレットが adb に見えず実機で確かめていません(単体テストと apiCheck は通過)。追従前の build では MapLibre / Google で確認済みです。
  • web MapKit: トークンが 401 で地図が出ず、描画と重なり順は確かめていません(タップ判定は 43/43)。
  • web ArcGIS の初回タップ: カメラが一度動くまで当たりません(地図を一度動かすと 43/43)。
  • web Mappls: 右端でタップの位置がずれます(36/43)。
  • iOS の Mac 側照合のずれ: タップ試験の Mac 側の照合(初期カメラを仮定して最寄りの道路を計算)が、Google 16/30・TomTom 18/30・ArcGIS 13/30・ArcGIS2D 10/30・MapKit 11/30 しか合いません。アプリが返した最近点はどれも実在の道路の線から 0.05 pt 以内なので、判定の誤りではなく、Mac 側が仮定するカメラ(ズームの定義・safe area・2D の傾き補正)が SDK と合っていないと見ていますが、確かめてはいません。
  • 既存の失敗(このブランチ由来ではない): Android OMM の OpenMobileMapsDriverConformanceTest 1 件(develop の Log.isLoggable がモックされていない)、iOS ArcGIS・TomTom の単体テスト各 1 件、iOS Google のテストターゲットのビルド(GoogleMaps が iOS 17 を要求)。

base について

ポリラインの差分だけをレビューできるように、base は feat/overlay-on-added(feat/polygon-tiling + onAdded + ボストンのサンプル。そのリポジトリに無い場合は develop)にしています。feat/polygon-tiling は develop にマージ済みなので、feat/overlay-on-added が develop に入ったら、この PR の base を develop に付け替えてください。
追従は rebase せず merge commit で行いました(origin/feat/polygon-tiling → feat/overlay-on-added → feat/polyline-tiling)。

マージの順番

  1. 先に、各リポジトリの feat/overlay-on-added(onAdded とボストンのサンプル)を develop へ。入ったら下の PR の base を develop に付け替えます。
  2. android-sdk-core: https://github.com/MapConductor/android-sdk-core/pull/36(base: feat/overlay-on-added)
  3. android-sdk-compose: https://github.com/MapConductor/android-sdk-compose/pull/14(base: develop)
  4. android-for-googlemaps: https://github.com/MapConductor/android-for-googlemaps/pull/28(base: develop)
  5. android-for-mapbox: https://github.com/MapConductor/android-for-mapbox/pull/26(base: develop)
  6. android-for-maplibre: https://github.com/MapConductor/android-for-maplibre/pull/27(base: develop)
  7. android-for-mappls: https://github.com/MapConductor/android-for-mappls/pull/18(base: develop)
  8. android-for-maptiler: https://github.com/MapConductor/android-for-maptiler/pull/19(base: feat/overlay-on-added)
  9. android-for-longdo: https://github.com/MapConductor/android-for-longdo/pull/16(base: feat/overlay-on-added)
  10. android-for-openmobilemaps: https://github.com/MapConductor/android-for-openmobilemaps/pull/16(base: develop)
  11. android-for-here: https://github.com/MapConductor/android-for-here/pull/22(base: develop)
  12. android-for-arcgis: https://github.com/MapConductor/android-for-arcgis/pull/30(base: develop)
  13. android-for-tomtom: https://github.com/MapConductor/android-for-tomtom/pull/18(base: develop)
  14. この PR(親)

プロバイダは android-sdk-core の PolylineTiler / 重なり順を使うので core が先です。どの PR もまだマージしていません。マージはレビューのうえで判断してください。

Every road of Boston (Overture transportation segments, 54,645 polylines,
234,765 points, assets/boston/boston-city-roads.bin, about 1 MB) with one
Polylines() call: coloured, widened and stacked by road class, a loading overlay
until every road's onAdded, a tap InfoBubble (class, name, length) at the point on
the road nearest the tap, a Tiles / Native switch (PolylineTiling) and an overlap
check (polygon tiles < circle < road tiles < marker). Opens from
'--es page boston-roads' with camera / tiling / overlap extras for measurements.

The MCPG v2 reader learns the road class (16) and lines (32) bits; the roads
decoder test uses the fixture the React and iOS tests share.

android-sdk-core, android-sdk-compose and the ten providers move to their
feat/polyline-tiling commits.
android-sdk-core's gitlink conflicted; it points at android-sdk-core's
feat/polyline-tiling after that branch merged feat/overlay-on-added.
@wf9a5m75

wf9a5m75 commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

CI: base が develop/main でないので Test は PR で走らないため、workflow_dispatch で実行しました(https://github.com/MapConductor/android-sdk/actions/runs/42699020250)。android-sdk-core の ZoomAltitudeConverterGoldenTest 2 件が落ちます。develop でも同じ 2 件が落ちる(https://github.com/MapConductor/android-sdk/actions/runs/42699917590)ため、このブランチ由来ではありません(このブランチは ZoomAltitudeConverter に触れていません。手元の JDK では通る、浮動小数の完全一致の試験)。submodule 側の各 PR の build はすべて成功しています。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant