Skip to content

Stack ArcGIS graphics by zIndex within a band, whatever order they arrive in - #28

Merged
wf9a5m75 merged 4 commits into
developfrom
fix/arcgis-zindex-within-band
Nov 9, 2026
Merged

wf9a5m75 merged 4 commits into
developfrom
fix/arcgis-zindex-within-band

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

なぜ

ArcGIS の 2D(MapView)で、同じ帯(ポリゴン・サークル・ポリライン・マーカー)の中の zIndex が、オーバーレイを足した順に左右されていました。zIndex=1 を先に、zIndex=0 を後に足すと、zIndex=1 が下に描かれます。タブレットで確かめた内訳:

  • ポリゴン: 3D は正しく、2D が誤り。renderer は zIndex を attribute に書き、onPostProcess でコレクションを並べ替えていました(clear + addAll)。コレクションは並んでいましたが、MapView は最初に足した順のまま描きます(コレクション順に従うのは SceneView だけ)。
  • サークル・ポリライン: 3D・2D とも誤り。zIndex をどこも読んでいませんでした。
  • マーカー: renderer が zIndex を無視していました(コードとテストで確認)。

さらに、ArcGIS は同じ zIndex の Graphic を足した順に描くとは保証していません(API リファレンスは「通常は」足した順で、順が大事なら zIndex を明示するように、と書いています)。実際タブレットの 2D で、zIndex=0 のポリゴン 2 つの間に zIndex=1 を足すと後の zIndex=0 が下になり(develop では正しかった)、同じ zIndex のマーカーはどちらの順にもなりませんでした(develop でも)。

確かめる途中で、次の 3 つの落ち方・ずれも見つけたので一緒に直しています。

  1. ポリラインを 2 本同時に外すと ConcurrentModificationException で落ちる(develop で毎回再現)。ポリラインの renderer が GraphicsOverlay.graphics を Dispatchers.Default から触っていました。
  2. GroundImage とラスターを同時に外すと ConcurrentModificationException で落ちる(1. を直すと見えるようになりました)。GroundImage の renderer が operational layers を Dispatchers.Default から触り、ラスターとマーカータイルはメインスレッドから触っていました。
  3. 起動直後に同じ id の Graphic が 2 つ入る(マーカー・ポリゴン・ポリラインで間欠的)。state が変わると Compose が走っている LaunchedEffect を取り消しますが、renderer は自分のスコープで地図に Graphic を載せるので、その間に取り消されると withContext が結果を捨て、Graphic は地図に残ったまま entity に登録されません。次の同期で同じ id をもう一度足し、最初の 1 つは外されず clearOverlays でも残ります。以前見えていたポリゴンの重複もこれが原因でした。

何を変えたか

  • 各 GraphicsOverlay が重なり順の表(ArcGISGraphicsZIndex)を持ち、(state の zIndex, 渡された順) で並べた順位を Graphic.zIndex に書きます。同じ値の Graphic は 2 つできません。zIndex だけの更新は Graphic を動かしても同じ zIndex の中での位置を保ち、外して戻したものはその zIndex の一番上に載ります。順位は増えるだけでよいので、外したところは空きのままにし、上に足すときは 1 つ書くだけです。
  • ポリゴンの renderer の並べ替え(clear + addAll)をやめました。Graphic.zIndex があれば 3D・2D とも並べ替えなしで正しく描け、clear + addAll は非同期の削除で外したばかりの Graphic を戻してしまうことがありました。
  • ポリラインの renderer は Graphic の操作をメインスレッドで行い、頂点の補間だけを Default に残しました(ポリゴン・サークルと同じ形)。
  • GroundImage の renderer は operational layers の操作をメインスレッドで行い、タイルを作る処理とレイヤーの読み込みは Default に残しました。
  • ArcGIS の各コントローラは add() を NonCancellable で最後まで走らせます。取り消しの元になった新しい state は、semaphore の後ろで待っている次の同期が反映します。削除はもともと取り消しに耐えます(地図から外してから entity を忘れる)。

GroundImage とラスターの帯はもともと順が正しかった(Android の GroundImage は zIndex を持たず、ラスターは ArcGISOverlayStacking を通る)ので変えていません。

公開 API: ArcGISGraphicsZIndex の追加のみです(apiCheck のベースラインを更新)。

検証

  • instrumented テスト 12 件を足しました。いずれも直す前に落ち、直した後に通ります。
    • ArcGISGraphicsZIndexTest(7 件): ポリゴン・サークル・ポリライン・マーカーそれぞれで zIndex=1 を先・zIndex=0 を後に足し ArcGIS が描く順を確かめる 4 件、同じ zIndex のポリゴンの間に高いものを足す・同じ zIndex のマーカー・サークルの zIndex 変更と足し直しの 3 件。
    • ArcGISGroundImageLayerThreadTest(1 件): レイヤーを足す・外すスレッドを記録。直す前は両方 DefaultDispatcher-worker でした。
    • ArcGISOverlaySyncCancellationTest(4 件): メインスレッドを塞いだ状態で add を取り消し、もう一度同期する。ポリゴン・ポリライン・サークル・マーカーとも、直す前は Graphic が 2 つ、直した後は 1 つ。
  • タブレット実機の 2D(検証用ページ、コミットしていません):
    • 帯の中・帯の間の重なり順が、zIndex=1 を先に足しても後に足しても正しい。
    • 更新後も正しい: zIndex の入れ替え、同じ id の新しい state、一部を外して戻す、clearOverlays してから送り直し、デザイン変更。
    • 足し外しを続けて行っても FATAL 0 件。
    • 起動直後の同じ id の重複: 8 回起動して 063184e の時点では 2 回、最終の 3a5c079 では 0 回。
    • 既存のサンプル 7 ページの見た目が直す前と同じ。

確認できていないもの

  • マーカーのドラッグ。
  • 3D での帯の中の順(3D の動的描画はカメラからの距離順で描くと公式に書かれています)。
  • 他のプロバイダにも、Compose の取り消しで Graphic が登録されないまま残る穴があるか(core で直すかは別の判断)。

関連: iOS 側 MapConductor/ios-for-arcgis#18

…rive in

Within the polygon, circle, polyline and marker bands, a zIndex=1 overlay
added before a zIndex=0 one ended up underneath it. Measured on the
tablet (stackctl page, zIndex=1 declared first):

- Polygons: right on 3D, wrong on 2D. The renderer wrote zIndex into an
  attribute and re-sorted the overlay's collection (clear + addAll) in
  onPostProcess. The collection did end up sorted (logged), but MapView
  kept drawing in the original order; only SceneView follows the
  collection order.
- Circles and polylines: wrong on both, because nothing read zIndex.
  The marker renderer ignored it too (seen in the code and the test; the
  check page has no overlapping markers).

ArcGIS draws a GraphicsOverlay by each Graphic's zIndex property, ties in
the order they were added, on both SceneView and MapView. Every renderer
now sets that property on create and on update (ArcGISGraphicsZIndex),
and the polygon renderer no longer re-sorts the collection: with the
property set, 3D and 2D both draw right without it, and clear + addAll
could put back a graphic an asynchronous removal had just taken out.

The polyline renderer touched GraphicsOverlay.graphics on
Dispatchers.Default. Removing two polylines at once made the two removals
race and crashed the app with ConcurrentModificationException (seen on
develop as soon as the check page swapped two polylines). It now touches
the graphics on the main thread like the polygon and circle renderers,
and only interpolates the vertices on Default.

The ground image and raster bands were already ordered: ground images
carry no zIndex on Android, and raster layers go through
ArcGISOverlayStacking with theirs.

ArcGISGraphicsZIndexTest (instrumented) adds a zIndex=1 overlay first and
a zIndex=0 one after for each kind, and checks the order ArcGIS draws
them in; all five tests failed before and pass now.
The ground image renderer added and removed its WebTiledLayer in the
map's operational layers from Dispatchers.Default, while the raster
renderer and the marker tiles change the same list on the main thread.
Taking a ground image and a raster layer away together (the stackctl
check page does this when it re-sends every overlay) crashed the app
on the tablet with ConcurrentModificationException in
ArcGISRasterLayerOverlayRenderer.removeLayer. It showed up once the
polyline crash in front of it was fixed.

The list is now only touched on the main thread; building the tiles
and loading the layer stay on Default.

ArcGISGroundImageLayerThreadTest (instrumented) records which thread
adds and removes the layer; it failed before (both on
DefaultDispatcher-worker) and passes now.
Writing the state's zIndex into Graphic.zIndex fixed zIndex=1 drawing
under zIndex=0, but ties were left to ArcGIS, which does not keep the
order graphics were added (its reference says they "usually" render in
that order and to set zIndex explicitly when order matters). On the
tablet in 2D:

- two zIndex=0 polygons with a zIndex=1 one added between them drew the
  later one underneath (it was right on develop, where nothing set
  Graphic.zIndex);
- markers with equal zIndex drew in neither order, on develop as well.

Each GraphicsOverlay now keeps a stack order (ArcGISGraphicsZIndex):
graphics sorted by (state zIndex, arrival), with that rank written to
Graphic.zIndex, so no two graphics share a value. A zIndex-only update
moves the graphic but keeps its place among equals; a graphic taken off
and added again goes on top of its zIndex. Ranks only need to increase,
so removals leave gaps and adding on top writes one value.

ArcGISGraphicsZIndexTest gets three tests (equal zIndex polygons around
a higher one, equal zIndex markers, zIndex change and re-add on
circles); they failed on the previous commit and pass now. The
stackctl check page shows every band right with zIndex=1 added first
or last, after a zIndex swap, same-id new states, partial removal and
restore, clearOverlays and re-send, and a design change.
Compose cancels the running LaunchedEffect whenever an overlay's states
change. The ArcGIS renderers put graphics and layers on the map in
their own scope (main thread or Default) while the caller waits on the
compose dispatcher; cancelled in between, withContext throws away the
result, so the graphic stays on the map but is never registered. The
next sync adds the same id again, and the first copy is never taken
off -- clearOverlays leaves it too.

Seen on the tablet in 2D: right after start, markers, polygons and
polylines intermittently came in twice per id (1 of 4 launches of the
check page; a run that hit it kept 5 extra marker graphics through
clearOverlays), and polylines added and removed in quick succession
were left behind. This, not the clear() + addAll() re-sort, is where
the duplicate polygons seen earlier came from.

The ArcGIS controllers now run add() to the end (NonCancellable); the
newer states that caused the cancellation are applied by the next sync,
which waits behind the semaphore. Removal already tolerated
cancellation (it removes from the map before forgetting the entity).

ArcGISOverlaySyncCancellationTest (instrumented) cancels an add while
the main thread is busy and then syncs again, for polygons, polylines,
circles and markers; all four found two graphics before and one now.
@wf9a5m75
wf9a5m75 merged commit fc44cb2 into develop Nov 9, 2026
2 checks passed
@wf9a5m75
wf9a5m75 deleted the fix/arcgis-zindex-within-band branch November 9, 2026 01:52
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