Skip to content

Draw large polyline sets as raster tiles (PolylineTiler) - #36

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(+タイル)。

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

  • PolylineTilingOptions / PolylineTileSet / PolylineTiler / POLYLINE_TILE_LAYER_ID / polylineTileRasterLayerState と PolylineController.tiler。タイルで描く線は PolylineManager に残り(タップ用)、entity の polyline は null。
  • ポリゴンと共通の内部部品 ShapeTileSet(ルート・URL の版・16 ms のまとめ・削除スロット)/ ShapeTiler / ShapeTileRenderer(格子索引・ズームごとの簡略化・LRU と線幅ぶんの無効化・Bitmap のプール・PNG・キャンセル)/ ShapeGridIndex。PolygonTileSet / PolygonTiler / PolygonTileRenderer はその上の薄い層。
  • 開いた経路を dp の線幅で、round cap/join、線幅の半分だけ外の候補を丸ごと描く。1 px 未満は点。
  • タップ: tapTolerance + strokeWidth / 2、最も近い線、登録時に作る外接矩形で先に絞る。
  • PolylineOverlay が地図ごとの PolylineTilingOptions の flow を受け取る(引数の追加。1 引数のコンストラクタは残る)。
  • feat/overlay-on-added を merge commit で取り込み(その中身は develop の API ベースラインの修正 Record the hole union's EdgeGrid and IntArrayList in the API baseline #34 のみ)。

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

  • 単体テスト(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 に見えず未確認です。

挙動の変わる所

  • タップの判定に線幅の半分を足しました(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)。

マージの順番

各リポジトリの feat/overlay-on-added →(base を develop に付け替えて)android-sdk-core → android-sdk-compose → 各プロバイダ → 親。プロバイダは android-sdk-core の PolylineTiler / 重なり順を使うので core が先です。マージはレビューのうえで判断してください(この PR はまだマージしていません)。

親の PR: MapConductor/android-sdk#119

PolylineTilingOptions / PolylineTileSet / PolylineTiler / POLYLINE_TILE_LAYER_ID /
polylineTileRasterLayerState and PolylineController.tiler, the same shape as the
polygon tiling: tiled polylines stay in PolylineManager for taps and give the
renderer nothing (their entity's polyline is null).

The tiles share everything below the polygon ones: ShapeTileSet (route, version
in the URL, 16 ms coalescing, removal slots), ShapeTiler (routing), ShapeTileRenderer
(grid index, per-zoom simplification, LRU cache and stroke-wide invalidation,
pooled scratch bitmaps, PNG, cancellation) and ShapeGridIndex. PolygonTileSet,
PolygonTiler and PolygonTileRenderer are thin layers over them now.

A polyline tile is an open path: snapped to the zoom's pixel grid,
Douglas-Peucker at half a pixel with both ends kept, round caps and joins, the
stroke as wide in dp at every zoom, a dot as wide as the line when it collapses
to a point. Candidates are taken half the widest stroke beyond the tile and drawn
whole, so a line crossing a tile edge is the same on both sides.

Taps: tapTolerance + strokeWidth / 2, nearest line, with each line's bounding box
(made on registration) checked first.

PolylineOverlay takes an optional per-map PolylineTilingOptions flow, applied to
the map's tilers before every hand-over.
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