Mapboxの非表示シンボルを扱う (1. 衝突ボックス編)

2022-08-20 (2022-09-16 更新)

thumbnail

Mapbox GL JSの画面上で別のシンボルに隠されたシンボルを扱うユーティリティライブラリを開発中です。 これはライブラリの開発過程を紹介するシリーズの最初のブログ投稿です。

背景

Mapbox GL JS (mapbox-gl-js)Mapboxのマップをウェブブラウザ上に表示するためのJavaScriptライブラリです。 mapbox-gl-jsはMapboxのプロプライエタリなライブラリですが、オープンソース化されており彼らのサービス規約に沿う限り改変することができます。

Symbol Layerを使用するとマップ上の点に任意のシンボルを表示することができます。 以下の画像は私が開発中のアプリのスクリーンショットで、落とし物のアイコンが「シンボル」です。 シンボルの例

シンボルが画面上で重なると、mapbox-gl-jsは最初のものだけ表示し他の重なっているものは非表示にします。 私の知っている限り、画面上で特定のシンボルに隠されたシンボルを取得するAPIはmapbox-gl-jsにありません*。 私のアプリではクリックされた場所のシンボルを非表示のものも含めてすべてリストしたいので、これでは不都合です。 mapbox-gl-jsに衝突検出をスキップさせてシンボルをもれなく画面上に表示するオプションはありますが、重なるシンボルが多い場合はマップがごちゃごちゃし過ぎてしまうでしょう。

ということでMapboxのマップ上で特定のシンボルと重なるシンボルを集約することのできるライブラリを開発することにしました。

2022-08-29追記: ライブラリはhttps://github.com/codemonger-io/mapbox-collision-boxesで手に入ります。

* Map#queryRenderedFeaturesは表示されているシンボル(Feature)だけを返し非表示のものは返しません。 "click"イベントも非表示のシンボルについては教えてくれません。

ライブラリに対する要件

私が開発中のライブラリを使うと、特定のLayerで指定したボックスと交差するシンボルを表示の有無にかかわらず問い合わせることができます

上記機能に基づき、特定のLayerでクリックされたシンボルに隠されているシンボルを問い合わせることができます

問題を単純化するため、ラベル(Text)のないシンボルのみ(アイコンのみ)を考えることにします。

衝突検出

mapbox-gl-jsはどのシンボルを画面に描画するか決めるために衝突検出を行います。 Map#showCollisionBoxesプロパティを有効にすると、mapbox-gl-jsは画面上に表示・非表示のシンボルすべての衝突ボックスを可視化します。 以下は衝突ボックス(とサークル)をMapboxのマップ上に表示する例です。 衝突ボックスの例

これらの衝突ボックスの位置と寸法をシンボルのFeatureと一緒に取得できれば、ライブラリを実装できます。

衝突ボックスを取得する

衝突ボックスを取得できるかどうか調べるためにmapbox-gl-jsのソースコード*を眺めていきましょう。

* 当時の最新バージョン2.9.2を分析しました。

デバッグのための衝突ボックス

Map#showCollisionBoxesを有効にしたときに描画される衝突ボックスの情報にMap#showCollisionBoxesfalseにした製品版環境でもアクセスできるでしょうか? 残念ながら、答えはノーです。

SymbolBucket#textCollisionBoxSymbolBucket#iconCollisionBoxがすべての描画された衝突ボックスの情報を含んでいます。 SymbolBucket#updateCollisionBuffersSymbolBucket#textCollisionBoxSymbolBucket#iconCollisionBoxを更新しており、Placement#placeLayerBucketPartsymbol/placement.js#L435-L437でそれを呼び出しています:

        if (showCollisionBoxes && updateCollisionBoxIfNecessary) {
            bucket.updateCollisionDebugBuffers(this.transform.zoom, collisionBoxArray);
        }

上記コードから分かるとおり、当該メソッドはshowCollisionBoxes(= Map#showCollisionBoxes)がtrueの場合のみ呼び出されています。 製品版ではMap#showCollisionBoxesfalseのはずなので、SymbolBucket#textCollisionBoxSymbolBucket#iconCollisionBoxに頼ることはできません。 もしこれらを使うことができたとしても、タイル座標系で表されているので画面座標系に射影しなければなりません。

衝突ボックスの別の入手元

Placement#placeLayerBucketPartはシンボルの衝突検出を担っています。 その内部関数placeSymbolが各シンボルを処理しています。 さらにその内部関数のplaceIconFeatureを呼び出してアイコンが画面上で優先するシンボルと衝突しているかどうか判定しています。 placeIconFeaturesymbol/placement.js#L709-L710CollisionIndex#placeCollisionBoxを呼び出しており、それが実際の衝突判定を行っています:

                    return this.collisionIndex.placeCollisionBox(bucket, iconScale, iconBox, shiftPoint,
                        iconAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);

衝突が検出されなければ、placeSymbolは当該シンボルの衝突ボックスをCollisionIndex#insertCollisionBoxで記憶します。 残念ながら、他の優先する衝突ボックスと衝突するシンボルの衝突ボックスは記録しません。 なのでそれらのシンボルについて衝突ボックスを再計算しなければなりません。

ということで、次の節では画面座標系で衝突ボックスをどのように計算しているか理解するためにCollisionIndex#placeCollisionBoxを覗いてみましょう。

CollisionIndex#placeCollisionBoxの解剖

以下はCollisionIndex#placeCollisionBoxの大まかな流れを示しています。

  1. symbol/collision_index.js#L97-L99で、衝突ボックスの位置をタイル空間で取得:

        let anchorX = collisionBox.projectedAnchorX;
        let anchorY = collisionBox.projectedAnchorY;
        let anchorZ = collisionBox.projectedAnchorZ;
    
  2. symbol/collision_index.js#L104-L111で、標高(Elevation)の処理:

        if (elevation && tileID) {
            const up = bucket.getProjection().upVector(tileID.canonical, collisionBox.tileAnchorX, collisionBox.tileAnchorY);
            const upScale = bucket.getProjection().upVectorScale(tileID.canonical, this.transform.center.lat, this.transform.worldSize).metersToTile;
    
            anchorX += up[0] * elevation * upScale;
            anchorY += up[1] * elevation * upScale;
            anchorZ += up[2] * elevation * upScale;
        }
    

    anchorX, anchorY, anchorZを標高で調整。

  3. symbol/collision_index.js#L114で、アンカーを画面座標系に射影:

        const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, [anchorX, anchorY, anchorZ], collisionBox.tileID, checkOcclusion, bucket.getProjection());
    

    概説すると、CollisionIndex#projectAndGetPerspectiveRatioposMatrixを点([anchorX, anchorY, anchorZ])にかけてパースを修正します。

  4. symbol/collision_index.js#L116-L120で、衝突ボックスをスケールしてprojectedPoint.pointに移動:

        const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
        const tlX = (collisionBox.x1 * scale + shift.x - collisionBox.padding) * tileToViewport + projectedPoint.point.x;
        const tlY = (collisionBox.y1 * scale + shift.y - collisionBox.padding) * tileToViewport + projectedPoint.point.y;
        const brX = (collisionBox.x2 * scale + shift.x + collisionBox.padding) * tileToViewport + projectedPoint.point.x;
        const brY = (collisionBox.y2 * scale + shift.y + collisionBox.padding) * tileToViewport + projectedPoint.point.y;
    
  5. symbol/collision_index.js#L125-L142で、衝突ボックス([tlX, tlY, brX, brY])が以前に記録した衝突ボックスと衝突するかどうかを判定。 このステップはスコープ外です。

上記の計算(ステップ1から4)を模倣するためには、以下のパラメータを用意しなければなりません。

私の分析では、Placement, Tile, SymbolBucketが利用できれば、上記パラメータを取得することは可能です。 PlacementStyle#placementとして手に入り、StyleMap#styleとして手に入ります。 しかし TileSymbolBucketの取得については更に調査が必要です。

以下の節では分析の詳細を示します。

パラメータ: collisionBox

collisionBoxplaceIconFeatureに与えられるiconBox引数でplaceSymbolsymbol/placement.js#L717(と別の行)でそれを呼び出します:

                    placedIconBoxes = placeIconFeature(collisionArrays.iconBox);

ではcollisionArraysとは何でしょうか? collisionArraysplaceSymbolの引数でPlacement#placeLayerBucketPartsymbol/placement.js#L795(と別の行)でそれを呼び出します:

                placeSymbol(bucket.symbolInstances.get(i), i, bucket.collisionArrays[i]);

したがってcollisionArraysSymbolBucket#collisionArraysのi番目の要素です。 Placement#placeLayerBucketPartSymbolBucket#deserializeCollisionBoxessymbol/placement.js#L431-L433で呼び出してSymbolBucket#collisionArraysを初期化していることに留意ください:

        if (!bucket.collisionArrays && collisionBoxArray) {
            bucket.deserializeCollisionBoxes(collisionBoxArray);
        }

Placement#placeLayerBucketPartcollisionBoxArrayをメソッドの最初の引数であるbucketPartからsymbol/placement.js#L388-L401で取得しています:

        const {
            // ... 可読性のため割愛
            collisionBoxArray,
            // ... 可読性のため割愛
        } = bucketPart.parameters;

bucketPart.parameterscollisionBoxArrayは対応するマップタイルTile#collisionBoxArrayから来ています。 詳しくはPlacement#getBucketPartsを参照ください。

シンボルの配置が終了した後は SymbolBucket#collisionArraysを使うことができます

パラメータ: posMatrix

Placement#placeLayerBucketPartposMatrixをメソッドの最初の引数であるbucketPartからsymbol/placement.js#L388-L401で取得しています:

        const {
            // ... 可読性のため割愛
            posMatrix,
            // ... 可読性のため割愛
        } = bucketPart.parameters;

Placement#getBucketPartsposMatrixsymbol/placement.js#L249で計算しています:

        const posMatrix = getSymbolPlacementTileProjectionMatrix(tile.tileID, symbolBucket.getProjection(), this.transform, this.projection);

getSymbolPlacementTileProjectionMatrixmapbox-gl-jsからエクスポートされていませんが、我々のバージョンを実装するのは難しくありません。 詳しくはgetSymbolPlacementTileProjectionMatrixを参照ください。

パラメータ: elevation

CollisionIndex#placeCollisionBoxelevationsymbol/collision_index.js#L102で取得しています:

        const elevation = collisionBox.elevation;

placeIconFeatureupdateBoxDatasymbol/placement.js#L704で呼び出しelevationプロパティをiconBox(=collisionBox)に追加しています:

                    updateBoxData(iconBox);

updateBoxDataelevationsymbol/placement.js#L507-L509で計算しています:

                box.elevation = this.transform.elevation ? this.transform.elevation.getAtTileOffset(
                    this.retainedQueryData[bucket.bucketInstanceId].tileID,
                    box.tileAnchorX, box.tileAnchorY) : 0;

Placement#placeLayerBucketPartSymbolBucket#collisionArraysの要素を直接updateBoxDataに渡しているので、シンボルの配置が終わった後は SymbolBucket#collisionArraysの各要素のiconBoxelevationを記憶しているはずです。

以下もご覧ください。

パラメータ: textPixelRatio

Placement#placeLayerBucketParttextPixelRatioをメソッドの最初の引数であるbucketPartからsymbol/placement.js#L388-L401で取得しています:

        const {
            // ... 可読性のため割愛
            textPixelRatio,
            // ... 可読性のため割愛
        } = bucketPart.parameters;

Placement#getBucketPartstextPixelRatiosymbol/placement.js#L244で計算しています:

        const textPixelRatio = tile.tileSize / EXTENT;

Tile#tileSizeEXTENTもご覧ください。 このパラメータの計算は朝飯前です

パラメータ: scale

placeIconFeatureiconScale(=scale)をsymbol/placement.js#L708で計算しています:

                    const iconScale = bucket.getSymbolInstanceIconSize(partiallyEvaluatedIconSize, this.transform.zoom, symbolIndex);

Placement#placeLayerBucketPartpartiallyEvaluatedIconSizeをメソッドの最初の引数であるbucketPartからsymbol/placement.js#L388-L401で取得しています:

        const {
            // ... 可読性のため割愛
            partiallyEvaluatedIconSize,
            // ... 可読性のため割愛
        } = bucketPart.parameters;

Placement#getBucketPartspartiallyEvaluatedIconSizesymbol/placement.js#L317で計算しています:

            partiallyEvaluatedIconSize: symbolSize.evaluateSizeForZoom(symbolBucket.iconSizeData, this.transform.zoom),

詳しくはSymbolBcuket#getSymbolInstanceIconSizeを参照ください。.

partiallyEvaluatedIconSizeは再現することができ、このパラメータを計算することができます*。

* symbolSize.evaluateSizeForZoom関数がmapbox-gl-jsからエクスポートされていないことに後から気づきました。 この問題は次回の投稿でカバーします。

パラメータ: shift

placeIconFeatureshiftPoint(=shift)をsymbol/placement.js#L705-L707で計算しています:

                    const shiftPoint: Point = hasIconTextFit && shift ?
                        offsetShift(shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle) :
                        new Point(0, 0);

上記コードのshiftは insrc/symbol/placement.js#L609で設定されています:

                                    shift = result.shift;

hasIconFitsymbol/placement.js#L409で設定されています:

        const hasIconTextFit = layout.get('icon-text-fit') !== 'none';

ラベルのないアイコンにフォーカスするのでこのパラメータはPoint(0, 0)であると想定できます

まとめ

非表示シンボルの衝突ボックスを画面座標系で取得するお手軽な方法はありません。 しかし分析によると、Placement, Tile, SymbolBucketが手に入ればそれぞれのシンボルの衝突ボックスを再計算することはできます。

以下の疑問が残っています。

  • TileSymbolBucketはどうやって取得するのか?
  • 再計算した衝突ボックスとシンボルのFeatureをどうやって対応づけるのか?

ということで、次のブログ投稿ではこれらの疑問に答えます。

補足

用語

この節ではmapbox-gl-js特有の用語を簡単に解説します。

Feature

Layerのデータソースとして利用するVector TileとGeoJSONはFeatureの集合です。 Featureは形状(ジオメトリ)とオプションのプロパティを持ちます。

Layer

mapbox-gl-jsはマップをLayerのスタックとして表します。 Layerのタイプはいくつかあり、"Symbol" Layerはそのひとつです。 詳しくは「Layers|Style specification」[1]を参照ください。

マップタイル

mapbox-gl-jsは世界をマップタイルのグリッドに分ます。

タイル座標系(空間)

map tileのジオメトリはローカルな座標系(タイル座標系)で表されます。 詳しくは「Vector tiles standards」[2]を参照ください。

ソースコードレファレンス

この節ではmapbox-gl-jsのソースコードに関する私の補足コメントを紹介します。

Map

定義: ui/map.js#L326-L3677

mapbox-gl-jsを使うためにインスタンス化する最初のクラスです。

Map#style

定義: ui/map.js#L327

    style: Style;

このプロパティはすべてのマップデータを管理します。

Styleもご覧ください。

Style

定義: style/style.js#L135-L1860

Style#placement

定義: style/style.js#L173

    placement: Placement;

Placementもご覧ください。

Tile

定義: source/tile.js#L95-L799

Tile#tileSize

定義: src/source/tile.js#L99

    tileSize: number;
Tile#collisionBoxArray

定義: source/tile.js#L115

    collisionBoxArray: ?CollisionBoxArray;

Placement

定義: symbol/placement.js#L192-L1184

StylePlacementのインスタンスをStyle#placementとして保持します。

Placement#retainedQueryData

定義: symbol/placement.js#L205

    retainedQueryData: {[_: number]: RetainedQueryData};

このプロパティはSymbolBucket(Bucket)をRetainedQueryDataと対応づけます。

Placement#getBucketPartsは新しいRetainedQueryDataをBucketにsymbol/placement.js#L297-L303で割り当てます:

        this.retainedQueryData[symbolBucket.bucketInstanceId] = new RetainedQueryData(
            symbolBucket.bucketInstanceId,
            bucketFeatureIndex,
            symbolBucket.sourceLayerIndex,
            symbolBucket.index,
            tile.tileID
        );

このプロパティはSymbolBucketに対応するFeatureIndex(bucketFeatureIndex)を取得するのに必要です。

Placement#getBucketParts

定義: symbol/placement.js#L233-L333

このメソッドはgetSymbolPlacementTileProjectionMatrixを呼び出しタイルから画面座標系への射影マトリクスを計算します。

projection_util.getSymbolPlacementTileProjectionMatrix

定義: geo/projection/projection_util.js#L35-L41

export function getSymbolPlacementTileProjectionMatrix(coord: OverscaledTileID, bucketProjection: Projection, transform: Transform, runtimeProjection: string): Float32Array {
    if (bucketProjection.name === runtimeProjection) {
        return transform.calculateProjMatrix(coord.toUnwrapped());
    }
    assert(transform.projection.name === bucketProjection.name);
    return reconstructTileMatrix(transform, bucketProjection, coord);
}
Placement#placeLayerBucketPart

定義: symbol/placement.js#L386-L808

このメソッドはシンボルの衝突検出を担います。

このメソッドはsymbol/placement.js#L786-L797placeSymbolを各シンボルについて呼び出します:

        if (zOrderByViewportY) {
            assert(bucketPart.symbolInstanceStart === 0);
            const symbolIndexes = bucket.getSortedSymbolIndexes(this.transform.angle);
            for (let i = symbolIndexes.length - 1; i >= 0; --i) {
                const symbolIndex = symbolIndexes[i];
                placeSymbol(bucket.symbolInstances.get(symbolIndex), symbolIndex, bucket.collisionArrays[symbolIndex]);
            }
        } else {
            for (let i = bucketPart.symbolInstanceStart; i < bucketPart.symbolInstanceEnd; i++) {
                placeSymbol(bucket.symbolInstances.get(i), i, bucket.collisionArrays[i]);
            }
        }
Placement#placeLayerBucketPart.placeSymbol

定義: symbol/placement.js#L439-L784

これはPlacement#placeLayerBucketPartの内部関数です。

この関数は与えられたシンボルについて衝突判定と配置を行います。

この関数はアイコンの衝突判定のためにplaceIconFeatureを呼び出します。

Placement#placeLayerBucketPart.placeSymbol.placeIconFeature

定義: symbol/placement.js#L703-L711

                const placeIconFeature = iconBox => {
                    updateBoxData(iconBox);
                    const shiftPoint: Point = hasIconTextFit && shift ?
                        offsetShift(shift.x, shift.y, rotateWithMap, pitchWithMap, this.transform.angle) :
                        new Point(0, 0);
                    const iconScale = bucket.getSymbolInstanceIconSize(partiallyEvaluatedIconSize, this.transform.zoom, symbolIndex);
                    return this.collisionIndex.placeCollisionBox(bucket, iconScale, iconBox, shiftPoint,
                        iconAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);
                };

これはplaceSymbolの内部関数です。

この関数はiconBoxタイル座標系から画面座標系に射影し、その射影したボックスについて衝突判定を行います。

以下もご覧ください。

Placement#placeLayerBucketPart.placeSymbol.updateBoxData

定義: symbol/placement.js#L504-L510

            const updateBoxData = (box: SingleCollisionBox) => {
                box.tileID = this.retainedQueryData[bucket.bucketInstanceId].tileID;
                if (!this.transform.elevation && !box.elevation) return;
                box.elevation = this.transform.elevation ? this.transform.elevation.getAtTileOffset(
                    this.retainedQueryData[bucket.bucketInstanceId].tileID,
                    box.tileAnchorX, box.tileAnchorY) : 0;
            };

これはPlacement#placeLayerBucketPartの内部関数です。

RetainedQueryData

定義: symbol/placement.js#L87-L105

export class RetainedQueryData {
    bucketInstanceId: number;
    featureIndex: FeatureIndex;
    sourceLayerIndex: number;
    bucketIndex: number;
    tileID: OverscaledTileID;
    featureSortOrder: ?Array<number>
    // ... truncated for simplicity
}

FeatureIndex

定義: data/feature_index.js#L54-L312

FeatureIndexはシンボルのFeatureを解決する上で重要な役割を果たします。

SymbolBucket

定義: data/bucket/symbol_bucket.js#L352-L1119

SymbolBucket#collisionArrays

定義: data/bucket/symbol_bucket.js#L380

    collisionArrays: Array<CollisionArrays>;

SymbolBucket#deserializeCollisionBoxesがこのプロパティを初期化します。

CollisionArraysもご覧ください。

SymbolBucket#textCollisionBox

定義: data/bucket/symbol_bucket.js#L398

    textCollisionBox: CollisionBuffers;

このプロパティはすべてのテキストラベルの衝突ボックスを格納します。

SymbolBucket#iconCollisionBox

定義: data/bucket/symbol_bucket.js#L399

    iconCollisionBox: CollisionBuffers;

このプロパティはすべてのアイコンの衝突ボックスを格納します。

SymbolBucket#deserializeCollisionBoxes

定義: data/bucket/symbol_bucket.js#L979-L995

このメソッドはSymbolBucket#collisionArraysを初期化します。

SymbolBucket#getSymbolInstanceIconSize

Definition: data/bucket/symbol_bucket.js#L882-L887

    getSymbolInstanceIconSize(iconSize: any, zoom: number, index: number): number {
        const symbol: any = this.icon.placedSymbolArray.get(index);
        const featureSize = symbolSize.evaluateSizeForFeature(this.iconSizeData, iconSize, symbol);

        return this.tilePixelRatio * featureSize;
    }
SymbolBucket#updateCollisionBuffers

定義: data/bucket/symbol_bucket.js#L914-L939

Placement#placeLayerBucketPartMap#showCollisionBoxestrueの場合のみこのメソッドを呼び出します。

CollisionArrays

定義: data/bucket/symbol_bucket.js#L90-L99

export type CollisionArrays = {
    textBox?: SingleCollisionBox;
    verticalTextBox?: SingleCollisionBox;
    iconBox?: SingleCollisionBox;
    verticalIconBox?: SingleCollisionBox;
    textFeatureIndex?: number;
    verticalTextFeatureIndex?: number;
    iconFeatureIndex?: number;
    verticalIconFeatureIndex?: number;
};

CollisionIndex

定義: symbol/collision_index.js#L64-L465

CollisionIndex#placeCollisionBox

定義: symbol/collision_index.js#L94-L143

    placeCollisionBox(bucket: SymbolBucket, scale: number, collisionBox: SingleCollisionBox, shift: Point, allowOverlap: boolean, textPixelRatio: number, posMatrix: Mat4, collisionGroupPredicate?: any): PlacedCollisionBox {
        assert(!this.transform.elevation || collisionBox.elevation !== undefined);

        let anchorX = collisionBox.projectedAnchorX;
        let anchorY = collisionBox.projectedAnchorY;
        let anchorZ = collisionBox.projectedAnchorZ;

        // Apply elevation vector to the anchor point
        const elevation = collisionBox.elevation;
        const tileID = collisionBox.tileID;
        if (elevation && tileID) {
            const up = bucket.getProjection().upVector(tileID.canonical, collisionBox.tileAnchorX, collisionBox.tileAnchorY);
            const upScale = bucket.getProjection().upVectorScale(tileID.canonical, this.transform.center.lat, this.transform.worldSize).metersToTile;

            anchorX += up[0] * elevation * upScale;
            anchorY += up[1] * elevation * upScale;
            anchorZ += up[2] * elevation * upScale;
        }

        const checkOcclusion = bucket.projection.name === 'globe' || !!elevation || this.transform.pitch > 0;
        const projectedPoint = this.projectAndGetPerspectiveRatio(posMatrix, [anchorX, anchorY, anchorZ], collisionBox.tileID, checkOcclusion, bucket.getProjection());

        const tileToViewport = textPixelRatio * projectedPoint.perspectiveRatio;
        const tlX = (collisionBox.x1 * scale + shift.x - collisionBox.padding) * tileToViewport + projectedPoint.point.x;
        const tlY = (collisionBox.y1 * scale + shift.y - collisionBox.padding) * tileToViewport + projectedPoint.point.y;
        const brX = (collisionBox.x2 * scale + shift.x + collisionBox.padding) * tileToViewport + projectedPoint.point.x;
        const brY = (collisionBox.y2 * scale + shift.y + collisionBox.padding) * tileToViewport + projectedPoint.point.y;
        // Clip at 10 times the distance of the map center or, said otherwise, when the label
        // would be drawn at 10% the size of the features around it without scaling. Refer:
        // https://github.com/mapbox/mapbox-gl-native/wiki/Text-Rendering#perspective-scaling
        // 0.55 === projection.getPerspectiveRatio(camera_to_center, camera_to_center * 10)
        const minPerspectiveRatio = 0.55;
        const isClipped = projectedPoint.perspectiveRatio <= minPerspectiveRatio || projectedPoint.occluded;

        if (!this.isInsideGrid(tlX, tlY, brX, brY) ||
            (!allowOverlap && this.grid.hitTest(tlX, tlY, brX, brY, collisionGroupPredicate)) ||
            isClipped) {
            return {
                box: [],
                offscreen: false,
                occluded: projectedPoint.occluded
            };
        }

        return {
            box: [tlX, tlY, brX, brY],
            offscreen: this.isOffscreen(tlX, tlY, brX, brY),
            occluded: false
        };
    }

placeIconFeatureはこのメソッドを呼び出します。

CollisionIndex#insertCollisionBox

定義: symbol/collision_index.js#L401-L406

    insertCollisionBox(collisionBox: Array<number>, ignorePlacement: boolean, bucketInstanceId: number, featureIndex: number, collisionGroupID: number) {
        const grid = ignorePlacement ? this.ignoredGrid : this.grid;

        const key = {bucketInstanceId, featureIndex, collisionGroupID};
        grid.insert(key, collisionBox[0], collisionBox[1], collisionBox[2], collisionBox[3]);
    }
CollisionIndex#projectAndGetPerspectiveRatio

定義: symbol/collision_index.js#L417-L445

Transform

定義: geo/transform.js#L42-L2061

Transform#elevation

定義: geo/transform.js#L220

Elevation

定義: terrain/elevation.js#L31-L237

Elevation#getAtTileOffset

定義: terrain/elevation.js#L110-L115

定数

EXTENT

定義: data/extent.js#L18

export default 8192;

この値はEXTENTとして参照されます。

Reference

  1. Layers|Style specification
  2. Vector tiles standards