Skip to content

Draw large polygon sets as raster tiles (PolygonTiler) - #35

Merged
wf9a5m75 merged 10 commits into
developfrom
feat/polygon-tiling
Nov 9, 2026
Merged

wf9a5m75 merged 10 commits into
developfrom
feat/polygon-tiling

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

何をするか

多数のポリゴン(既定: 2,000 個以上または頂点 50,000 以上)を、ネイティブの図形ではなくラスタータイルとして描きます(PolygonTilingOptions / PolygonTiler)。web・Android・iOS で同じ設計です。

  • タイルは SDK のタイルサーバー(web はサービスワーカー、ネイティブは LocalTileServer)で描きます。ズームごとに画素格子へ丸めて簡略化し、外周と穴の巻きを逆にそろえて nonzero で塗ります。
  • タップ判定は従来どおり PolygonManager が元の形で行います(InfoBubble などは今までどおり)。
  • タイルにできなくなった変更(geodesic にする、経度幅 180° 以上、3 点未満)が入ったポリゴンは、その場でネイティブ描画へ戻ります。
  • ボストン全域の区画(98,815 件・穴あり)と建物(105,599 件)で確認しています。

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

  • PolygonTilingOptions / PolygonTiler / PolygonTileSet / PolygonTileRenderer(タイル描画)。PolygonController.tiler でタイル側のポリゴンは PolygonManager に残し、ネイティブの図形は作りません。タイルにできなくなった変更が入ったらネイティブ描画へ移します。コントローラ破棄でタイルを解放します。
  • スクラッチ Bitmap のプール化(4c9ccdd を cherry-pick): Google のタイルスレッドは地図より長生きで、スレッドごとの Bitmap が残っていました。develop の ScratchPool を使います。
  • ポリゴンのタップ判定: 辺の上と見なすのを eps 以内に限定。
  • holesAreDisjoint(穴が離れていれば結合を飛ばす近道)を、develop の穴の結合(穴の結合で、近くにある穴の面積を失わないようにし、速くする #33)の EdgeGrid と ringBounds / MEET_PAD に乗せ換え、独自の格子を消しました。
  • 衝突の解消: PolygonController.clear() / destroy() は develop の順番に、タイル側の片付けを加えました。
  • API ベースライン: PolygonTiler.remove、PolygonTileSet.remove、EdgeGrid / IntArrayList を追加。

develop の取り込み

このブランチは push 済みだったので、rebase せず origin/develop を merge commit で取り込みました。

  • 親 android-sdk(feat/polygon-tiling)で core / compose / kml / geojson / icons と各プロバイダの testDebugUnitTest: core 350 件ほか全部 pass(OMM は develop から引き継いだ 1 件の失敗のみ。android.util.Log.isLoggable がテストでモックされていない)。apiCheck: 全モジュール ok(増えた API はベースラインに追加)。
  • タブレット実機(MapLibre / Google / Mapbox)でボストンの区画 98,815 件: タイルで描かれ、街区(穴)が正しく、タップで InfoBubble。重なり順は GroundImage < ポリゴンタイル < サークル < ポリライン < マーカー(MapLibre / Google)。MapLibre で頂点ドラッグの追従。Google ↔ MapLibre の切り替え 10 往復で Native Heap が頭打ち(89〜96 MB。スクラッチ Bitmap のプール化の前は 156 MB まで増え続けた)。

マージの順番

この機能は複数のリポジトリにまたがります。android-sdk-core → android-sdk-compose(feat/polygons)→ 各 android-for- → android-sdk(親)* の順にマージが要ります。

この PR は大きな機能なので、マージするかはレビューのうえで判断してください。

pointOnSegment compared the dot product (p-a)·(p-b), a quantity in square
degrees, with eps = 1e-6, so any tap within about 0.001° (~100 m) of the
middle of a short edge counted as on that edge. With building footprints the
first nearby building in the list was returned instead of the tapped one.
Now it is the plain distance to the segment, within 1e-6° (~0.1 m).
The counterpart of js-sdk-core's polygon tiling, in the same shape:
PolygonTilingOptions (2,000 polygons or 50,000 vertices), PolygonTiler
(route / update / clear, onTemplate, POLYGON_TILE_LAYER_ID,
polygonTileRasterLayerState) and PolygonTileSet (route on the local tile
server, version in the URL, in-place changes drawn together after 16 ms).

PolygonTileRenderer draws as tile-sw.js does: polygons in Web Mercator with
the outline wound against the holes so a nonzero fill needs no hole union,
a grid index of bounding boxes, a simplified copy per zoom (pixel grid,
repeats dropped, Douglas-Peucker at half a pixel, a polygon under a pixel
as one pixel), outlines only on polygons of 4 px or more. PNG through the
Rust encoder. An in-place change drops only the cached tiles it overlaps;
the rest come back from the cache under the new version. A tile the map
abandons mid-draw fails rather than coming back empty.

PolygonController gets a tiler: tiled polygons stay in PolygonManager for
taps, against their original shapes, and the renderer never sees them.
The bookkeeping for a city of polygons runs off the main thread.

Also skip the quadratic hole union when no two holes cross, touch or nest
(checked on a grid in linear time), still returning holes clockwise.
…ge makes it untileable

Whether a polygon is tiled was decided only on add. Setting geodesic on a
tiled polygon in place, or giving it points that span half the world or fewer
than three, kept it in the tiles, drawn with straight edges. update() now
checks canTile: a polygon that fails it leaves the tiles (its slot in the
renderer is emptied, so only the tiles it was in are drawn again) and is
handed to the renderer. It stays on the map, so this is not an add. The
reverse is left alone: a native polygon that could now be tiled already looks
right. Same as js-sdk-core.
With exactly 2,000 parcels, the one that moved to native drawing left 1,999
tileable ones, so handing the same list back put them all on the native path
(the add path's decision, as intended), and the test read that as a failure.
# Conflicts:
#	src/main/java/com/mapconductor/core/polygon/PolygonController.kt
…hread

Same as TilePngEncoder: renderTile runs on Google's tile-overlay threads,
which outlive their map, and a ThreadLocal<Bitmap> left a tile bitmap
(native memory) on each of them.

(cherry picked from commit 4c9ccdd2a655fcdd6a298227b10ccdf35bebcedd)
The disjoint check had a grid of its own (and ringBox, a copy of ringBounds).
It now files the hole edges in the EdgeGrid the union uses, padded by
MEET_PAD, the reach of intersectionPoints, and tests each edge only against
the edges of other rings near it. The old grid padded by EPS, less than that
reach, so it could miss a pair the union would have seen meet.
…Set.remove

Also EdgeGrid and IntArrayList from the hole union (private classes, which the
apiCheck dump lists as package-private, like Edge).
@wf9a5m75
wf9a5m75 merged commit 111be29 into develop Nov 9, 2026
2 checks passed
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