OSM(OpenStreetMap)のマルチポリゴンは一つの outer polygon の中に、 任意の数の inner polygon が含まれる。outer polygon をある色で塗りつぶし、 それぞれの inner polygon は透明な穴とする。
自前でマルチポリゴンを描画するには、まず、outer polygon を塗りつぶす。
その後、inner polygon を透明な色で塗りつぶせばよいように思うが、そうはいかない。 アルファ値が透明な場合、RGB がいかなる値であっても、塗りつぶしは起きない。
したがって、outer polygon の色とは異なる色(下の例では白色)で塗りつぶす。 その後、全画素の値を調べて、inner polygon の色であれば、アルファ値を 0、すなわち、透明に変える。
マイOSM地図のマルチポリゴンのレンダリングは当初からこの方法を用いている。
レンダリングとしては問題ないが、パフォーマンス上は効率がよくない。 まず、JavaプログラムはCなどネイティブ言語プログラムに比べて遅い。 第二にキャンバス全体の画素をチェックするため処理量が多い。 outer polygon の境界ボックス内だけでよいが、そうするにはそれなりのプログラムが必要になり、 その処理時間が必要になる。
Bitmap bmp = tile.bmpWork; bmp.eraseColor(Color.TRANSPARENT); Canvas work = tile.cvWork; fillPolygon(work, tile, paint, bgnData, multi[0]); // outer polygonの塗りつぶし int offset = bgnData + multi[0] * (slim ? 1 : 2); // outer polygon のノード数 for (int n = 1; n < multi.length; n++) { fillPolygon(work, tile, paintWhite, offset, multi[n]); offset += multi[n] * (slim ? 1 : 2); } // Pixel 操作部分 int[] pixels = tile.getWorkPixels(); // bmpWork の pixels を取り出す(参照) for (int k = 0; k < pixels.length; k++) { if (pixels[k] == 0xffffffff) { pixels[k] = 0x00ffffff; // 透明 } } // Bitmap に Pixel を設定 tile.setWorkPixels(); // pxWork(== pixels) を bmpWork にセット canvas.drawBitmap(bmp, 0, 0, null);
PorterDuffの実行プログラムはネイティブ言語で作られているであろうから、 自前の Javaプログラムより効率がいいであろう。 また、キャンバス全体の画素をチェックするような非効率なものではないであろう。
ただし、drawPath によるライン描画は非効率なものであるから、 PorterDuffが効率的かどうかは実際に試してみないと分からない。
記事[1]を参考にすると、 マルチポリゴンの場合、outer polygon を DST、inner polygon を SRC に指定して、 以下のようにすればよいようだ。
x, y は 0、W, H はキャンバスサイズでいいだろう。
もう少し事例を調べてから、プログラムを試したい。
int sc = canvas.saveLayer(x, y, x + W, y + H, paint, Canvas.MATRIX_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG ); // DST canvas.drawBitmap(bmpOuter, 0, 0, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); // SRC canvas.drawBitmap(bmpInner, 0, 0, paint); paint.setXfermode(null); canvas.restoreToCount(sc);
タイル地図画像のレンダリングでは、単純な単色ポリゴンの場合、直接、タイル Canvas に塗りつぶし処理を行えばいいが、 マルチポリゴンのレンダリングでは、タイルCanvasの Bitmap とは別に、SRCビットマップとDSTビットマップを使う。 DSTビットマップに outer polygon を描画する。これは現行プログラムと同じである。
問題はSRCビットマップに透明な inner polygon をどのようにして描画するかである。 PorterDuffには何か手があるだろうと思うが、まだ不明である。
createPolygonWithHoles androidで検索してみたが、これぞという記事が見つからない。
SRCビットマップは透明でなくてもいいのかもしれない。
Example #4 Source Project: android-map-sdk Author: navermaps File: PolygonOverlayActivity.java License: Apache License 2.0 6 votes vote down vote up @Override public void onMapReady(@NonNull NaverMap naverMap) { int color = ResourcesCompat.getColor(getResources(), R.color.primary, getTheme()); PolygonOverlay polygon = new PolygonOverlay(); polygon.setCoords(COORDS_1); polygon.setColor(ColorUtils.setAlphaComponent(color, 31)); polygon.setOutlineColor(color); polygon.setOutlineWidth(getResources().getDimensionPixelSize(R.dimen.overlay_line_width)); polygon.setMap(naverMap); PolygonOverlay polygonWithHole = new PolygonOverlay(); polygonWithHole.setCoords(COORDS_2); polygonWithHole.setHoles(HOLES); polygonWithHole.setColor( ColorUtils.setAlphaComponent(ResourcesCompat.getColor(getResources(), R.color.gray, getTheme()), 127)); polygonWithHole.setMap(naverMap); }
PorterDuffの使い方がまだ正確に掴めないため、自作プログラムの改良を行った。 Pixel操作はタイル全体(256x256画素)に対して行うのではなく、実際の描画範囲に限定する。
for (int i = px0; i < px1; i++) { for (int j = py0; j < py1; j++) { if (pixels[i * PX + j] == 0xffffffff) { pixels[i * PX + j] = 0x00ffffff; // 透明 } } }
当然、事前にこの範囲 px0, px1, py0, py1 を求めておく必要がある。
タイルのレンダリングに当たって、以下の三つの矩形を宣言して置き、これを再利用する。 タイル毎ではなく、プログラム起動時の方が、ガーベージコレクションの対象にならなくてよい。
レコード毎の処理での new は極力避けた方がよい。
// 初期化 RectF rectTile = new RectF(0, 0, PX, PX); RectF rectRecord = new RectF(); // 0, 0, 0, 0 RectF rect = new RectF(); // 0, 0, 0, 0
rectTile は全レコードを通して同じである。レコード毎に、境界ボックス(外接矩形)の相対座標(画素単位)を rectRecord に設定する。rect.setIntersect(rectTile, rectRecord)によって、rectTile と rectRecord が交差する部分が rect に設定される。
以上のようにすれば、実際にレンダリングが起きる矩形範囲(px0,py0)−(px1,py1)が効率的に算出できる。
// レコード(osm)毎の処理 rect.set(0, 0, 0, 0); rectRecord.set(x0*fact-x*PX, y0*fact-y*PX, x1*fact-x*PX, y1*fact-y*PX); rect.setIntersect(rectTile, rectRecord); osm.px0 = (short)rect.left; osm.py0 = (short)rect.top; osm.px1 = (short)rect.right; osm.py1 = (short)rect.bottom;
これによる時間短縮は以下のようになった。 共に一回の計測であり、実行時間は絶えず変動するため、絶対的ではないが、予想以上の結果となった。 PorterDuffではこれほどの成果は得られないかも知れない。
drawLine とより高度な drawPath の場合、低ズームでは drawPath の方が高速であるが、 高ズームでは、実際には描画が起きないものが増え、drawLine の方が高速になる。drawPath の場合、 空振りの描画を省くのが苦手なようである。
デジタル地図のレンダリングでは、ハイズームになるほど、空振りの描画が極端に多くなる。 このため、描画されることを前提としているような汎用プログラムの場合、効率が悪い。
PorterDuffのアルゴリズムがどうなっているか分からないが、空振りが多いとき効率的でない可能性はある。 よって、無理はせず、PorterDuffの適切なプログラム事例にであったときに試すことにする。
[改良前]zoom 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 平均 郊外 239 296 191 509 256 194 121 326 227 212 145 151 151 98 125 216ms 東京 259 287 226 423 329 156 120 333 412 489 298 157 169 138 129 262ms[改良後]
zoom 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 平均 郊外 206 240 210 424 240 191 124 323 243 223 137 158 160 111 123 208ms 東京 162 230 140 392 382 117 86 319 432 542 259 132 166 124 124 240ms
タイル数は 8〜12 であり、zoom により異なる。