Mapboxの非表示シンボルを扱う (2. Featureの解決編)

2022-09-16 (2022-09-24 更新)

thumbnail

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

背景

このシリーズの前回の投稿で、以下の疑問が残りました。

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

このブログ投稿では上記の疑問に答え、前回のブログ投稿で見落としていたアイコンの大きさを計算する方法についてもカバーします。

このライブラリはhttps://github.com/codemonger-io/mapbox-collision-boxesで手に入ります。

Mapbox GL JS (mapbox-gl-js)バージョン2.9.2*を分析しました。

* このブログを書いている段階で最新バージョンは2.10.0でしたが、一貫性を保つためにバージョン2.9.2で続けます。

TileとSymbolBucketを取得する

前回のブログ投稿で、どのシンボルを画面に表示するかを決定するのにPlacement#placeLayerBucketPartが重要な役割を果たしていることがわかりました。 このメソッドがどのように呼び出されているかを調べれば、TileSymbolBucketをどうやって取得するかがわかるかもしれません。

SymbolBucketを解決する

LayerPlacement#continuePlacementPlacement#placeLayerBucketPartを繰り返し呼び出します(style/pauseable_placement.js#L50-L57)。

        while (this._currentPartIndex < bucketParts.length) {
            const bucketPart = bucketParts[this._currentPartIndex];
            placement.placeLayerBucketPart(bucketPart, this._seenCrossTileIDs, showCollisionBoxes, bucketPart.symbolInstanceStart === 0);
            this._currentPartIndex++;
            if (shouldPausePlacement()) {
                return true;
            }
        }

Placement#getBucketPartsによって作成されるbucketPartsの各要素は処理の対象となるSymbolBucketを提供します(BucketPart#parametersTileLayerParameters#bucket)。 Placement#getBucketPartsは当該メソッド3番目の引数のTileからSymbolBucketを取り出します(symbol/placement.js#L233-L234)。

    getBucketParts(results: Array<BucketPart>, styleLayer: StyleLayer, tile: Tile, sortAcrossTiles: boolean) {
        const symbolBucket = ((tile.getBucket(styleLayer): any): SymbolBucket);

つまり、StyleLayerTileがあれば、SymbolBucketも手に入ります

StyleLayerとTileを解決する

LayerPlacement#continuePlacementPlacement#getBucketPartsを繰り返し呼び出します(style/pauseable_placement.js#L35-L43)。

        while (this._currentTileIndex < tiles.length) {
            const tile = tiles[this._currentTileIndex];
            placement.getBucketParts(bucketParts, styleLayer, tile, this._sortAcrossTiles);

            this._currentTileIndex++;
            if (shouldPausePlacement()) {
                return true;
            }
        }

上記スニペットのtilesstyleLayerLayerPlacement#continuePlacementに対する引数です。 PauseablePlacement#continuePlacementLayerPlacement#continuePlacementを繰り返し呼び出します(style/pauseable_placement.js#L97-L123, layerTiles[layer.source]tiles, layerstyleLayer)。

        while (this._currentPlacementIndex >= 0) {
            const layerId = order[this._currentPlacementIndex];
            const layer = layers[layerId];
            const placementZoom = this.placement.collisionIndex.transform.zoom;
            if (layer.type === 'symbol' &&
                (!layer.minzoom || layer.minzoom <= placementZoom) &&
                (!layer.maxzoom || layer.maxzoom > placementZoom)) {
                // ... 可読性のため割愛
                const pausePlacement = this._inProgressLayer.continuePlacement(layerTiles[layer.source], this.placement, this._showCollisionBoxes, layer, shouldPausePlacement);
            // ... 可読性のため割愛
        }

以下の条件判定(style/pauseable_placement.js#L101)からわかるとおり、PausePlacement#continuePlacementLayerPlacement#continuePlacementを"Symbol"レイヤーにのみ適用します。

            if (layer.type === 'symbol' &&

上記スニペットのorder, layers, layerTilesPauseablePlacement#continuePlacementに対する引数です。 Style#_updatePlacementPauseablePlacement#continuePlacementを呼び出します(style/style.js#L1740)。

            this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles);

Style#_updatePlacementstyle/style.js#L1696-L1712layerTilesを用意しています。

        const layerTiles = {};

        for (const layerID of this._order) {
            const styleLayer = this._layers[layerID];
            if (styleLayer.type !== 'symbol') continue;

            if (!layerTiles[styleLayer.source]) {
                const sourceCache = this._getLayerSourceCache(styleLayer);
                if (!sourceCache) continue;
                layerTiles[styleLayer.source] = sourceCache.getRenderableIds(true)
                    .map((id) => sourceCache.getTileByID(id))
                    .sort((a, b) => (b.tileID.overscaledZ - a.tileID.overscaledZ) || (a.tileID.isLessThan(b.tileID) ? -1 : 1));
            }
            // ... 可読性のため割愛
        }

ということで上記コードを真似すれば、与えたレイヤーIDに対応するStyleLayerとすべてのTileを取得することができます

レイヤーのTileとSymbolBucketをリストする

まとめると、指定したレイヤーのTileSymbolBucketをリストするコードのアウトラインは以下のようになります。

// 仮定 map: mapboxgl.Map, layerId: string
const style = map.style;
const layer = style._layers[layerId];
const sourceCache = style._getLayerSourceCache(layer);
const layerTiles = sourceCache.getRenderableIds(true).map(id => sourceCache.getTileByID(id));
for (const tile of layerTiles) {
    const bucket = tile.getBucket(layer);
    // tileとbucketの処理 ...
}

追加のチェックを含む完成したコードは私のGitHubレポジトリにあります。

シンボルのFeatureを解決する

前回の投稿で、FeatureIndexはシンボルのFeatureを解決する上で重要であることに軽く触れました。 Map#queryRenderedFeaturesを調べることでそれがわかりました。

概要:Map#queryRenderedFeaturesがどのように機能するか

Map#queryRenderedFeaturesStyle#queryRenderedFeaturesを呼び出します(ui/map.js#L1719)。

        return this.style.queryRenderedFeatures(geometry, options, this.transform);

Style#queryRenderedFeaturesqueryRenderedSymbolsを呼び出します(style/style.js#L1384-L1391)。

                queryRenderedSymbols(
                    this._layers,
                    this._serializedLayers,
                    this._getLayerSourceCache.bind(this),
                    queryGeometryStruct.screenGeometry,
                    params,
                    this.placement.collisionIndex,
                    this.placement.retainedQueryData)

queyrRenderedSymbolsFeatureIndex#lookupSymbolFeaturesを呼び出し、シンボルに対応するFeatureを取得します(source/query_features.js#L95-L103)。

        const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures(
                renderedSymbols[queryData.bucketInstanceId],
                serializedLayers,
                queryData.bucketIndex,
                queryData.sourceLayerIndex,
                params.filter,
                params.layers,
                params.availableImages,
                styleLayers);

FeatureIndex#lookupSymbolFeaturesは最初のパラメータrenderedSymbols[queryData.bucketInstanceId]に対応するGeoJSON形式のFeatureを解決し、読み込みます。

なので、FeatureIndex#lookupSymbolFeaturesに適切なパラメータを与えることでFeatureを解決することができます

FeatureIndex#lookupSymbolFeaturesに対するパラメータを用意する

FeatureIndex#lookupSymbolFeaturesの呼び出し結果(source/query_features.js#L95-L103)を再現するには以下のパラメータを提供する必要があります。

ちなみに最初のパラメータrenderedSymbols[queryData.bucketInstanceId]を再現する必要はありません。なぜならSymbolBucketで特定のバウンディングボックスと重なるFeatureではなくすべてのFeatureが必要だからです。 このパラメータをどのように置き換えるかについては節「SymbolBucketのすべてのFeatureのインデックスをリストする」を参照ください。

パラメータ: queryData

queryDataはループ変数でRetainedQueryDataが代入されます(source/query_features.js#L94-L132)。

    for (const queryData of bucketQueryData) {
        const bucketSymbols = queryData.featureIndex.lookupSymbolFeatures(
        // ... 可読性のため割愛
    }

bucketQueryDataRetainedQueryDataの配列でsource/query_features.js#L88-L92で初期化されます。

    const bucketQueryData = [];
    for (const bucketInstanceId of Object.keys(renderedSymbols).map(Number)) {
        bucketQueryData.push(retainedQueryData[bucketInstanceId]);
    }
    bucketQueryData.sort(sortTilesIn);

retainedQueryDataPlacement#retainedQueryDataです。

queryRenderedSymbolsrenderedSymbolsを初期化します(source/query_features.js#L87)。

    const renderedSymbols = collisionIndex.queryRenderedSymbols(queryGeometry);

CollisionIndex#queryRenderedSymbolsbucketInstanceIdSymbolBucket内でそのbucketInstanceIdに対応し指定のバウンディングボックスと重なるFeatureのインデックス列にマップするオブジェクトを返します。 表示されているシンボルのみを扱うのでライブラリではCollisionIndex#queryRenderedSymbolsは使いません。

SymbolBucketのリストは持っているので、SymbolBucket#bucketInstanceIdに対応するRetainedQueryDataPlacement#retainedQueryDataから得ることができます

// 仮定 placement: Placement, bucket: SymbolBucket
const queryData = placement.retainedQueryData[bucket.bucketInstanceId];

パラメータ: serializedLayers

このパラメータはStyle#_serializedLayersです。

パラメータ: params.filter

Map#queryRenderedFeaturesはデフォルトで空のオブジェクトをStyle#queryRenderedFeaturesparams引数に指定します(optionsparams, ui/map.js#L1716-L1719)。

        options = options || {};
        geometry = geometry || [[0, 0], [this.transform.width, this.transform.height]];

        return this.style.queryRenderedFeatures(geometry, options, this.transform);

なのでこのパラメータはundefinedでよいです。

パラメータ: params.layers

params.filterと同様に、このパラメータもundefinedでよいです。

パラメータ: params.availableImages

Style#queryRenderedFeaturesStyle#_availableImagesをこのパラメータに指定しています(style/style.js#L1354)。

        params.availableImages = this._availableImages;

パラメータ: styleLayers

このパラメータはStyle#_layersです。

SymbolBucketのすべてのFeatureのインデックスをリストする

FeatureIndex#lookupSymbolFeaturesが最初の引数(symbolFeatureIndexes)として受け取るFeatureのインデックスとは何でしょうか? CollisionIndex#queryRenderedSymbolsは使いませんが、それが何をしているのかを調べればFeatureIndex#lookupSymbolFeaturesの正しい入力を理解するのに役立つはずです。

この後に続く分析は大変なので先に結論からお伝えします。 SymbolBucketのすべてのFeatureのインデックスをリストするには、SymbolBucket#symbolInstancesのすべての要素からfeatureIndexプロパティを取り出す、もしくはSymbolBucket#collisionArraysのすべての要素からiconFeatureIndexプロパティを取り出します

// 仮定 bucket: SymbolBucket
// SymbolBucket#symbolInstancesは通常の配列ではないので注意
for (let i = 0; i < bucket.symbolInstances.length; ++i>) {
    const featureIndex = bucket.symbolInstances.get(i).featureIndex;
    // ... Featureのインデックスを処理
    // ... 衝突ボックスはbucket.collisionArrays[i]から計算可能
}

Featureを衝突ボックスに対応づけるのは単純です。なぜなら上記コードのfeatureIndexbucket.collisionArrays[i](衝突ボックス再計算のためのパラメータ)に対応しているからです。 この節の残りは次の節「アイコンの大きさを計算する」まで飛ばして構いません。

CollisionIndex#queryRenderedSymbolsは何をしているのか?

CollisionIndex#queryRenderedSymbolsGridIndex#queryを呼び出し、指定したバウンディングボックスと重なるFeatureをリストします(symbol/collision_index.js#L360-L361)。

        const features = this.grid.query(minX, minY, maxX, maxY)
            .concat(this.ignoredGrid.query(minX, minY, maxX, maxY));

それからfeaturesのすべての要素をsymbol/collision_index.js#L366-L396のループで処理します。

        for (const feature of features) {
            const featureKey = feature.key;
            // ... 可読性のため割愛
            result[featureKey.bucketInstanceId].push(featureKey.featureIndex);
        }

CollisionIndex#queryRenderedSymbolsは最終的に上記コードで更新されたresultを返します。 ではGridIndex#queryが返すfeaturesとは何でしょうか? GridIndex#queryは指定したバウンディングボックスと重なるGridItemの配列を返します。 GridIndex#queryはこれらのGridItemGridIndexがFeatureキーと一緒に格納しているボックスとサークルの情報(GridIndex#bboxes, GridIndex#circles, GridIndex#boxKeys, GridIndex#circleKeys)から構築します。 ということで、GridIndex#boxKeys(サークルのことは忘れましょう)の起源を追跡すれば、SymbolBucketのすべてのFeatureのインデックスを取得する方法がわかるはずです。

GridIndex#boxKeysの起源

GridIndex#insertGridIndex#boxKeyskeyを追加します(symbol/grid_index.js#L73)。

        this.boxKeys.push(key);

CollisionIndex#insertCollisionBoxkeyを用意しGridIndex#insertに渡します(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]);
    }

ここで上記スニペットのkeyを構成するfeatureIndexの起源に関心が移ります。

featureIndexの起源

シンボルのアイコンに絞ると、Placement#placeLayerBucketPart.placeSymbol (placeSymbol)symbol/placement.js#L748-L749CollisionIndex#insertCollisionBoxを呼び出しています。

                this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'),
                        bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID);

placeSymbolsymbol/placement.js#L698iconFeatureIndexを設定しています。

                iconFeatureIndex = collisionArrays.iconFeatureIndex;

collisionArraysCollisionArraysでありplaceSymbolの3番目の引数です。 今度はCollisionArraysの起源を追いかけます。 Placement#placeLayerBucketPartplaceSymbolsymbol/placement.js#L791またはsymbol/placement.js#L795で呼び出しています。

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

よってCollisionArraysSymbolBucket#collisionArraysの要素です。 Placement#placeLayerBucketPartSymbolBucket#deserializeCollisionBoxesを呼び出してSymbolBucket#collisionArraysを初期化して埋めます(symbol/placement.js#L432)。

            bucket.deserializeCollisionBoxes(collisionBoxArray);

SymbolBucket#deserializeCollisionBoxesCollisionArrays#iconFeatureIndexを実際に設定するSymbolBucket#_deserializeCollisionBoxesForSymbolを呼び出します(data/bucket/symbol_bucket.js#L962-L968)。

        for (let k = iconStartIndex; k < iconEndIndex; k++) {
            // An icon can only have one box now, so this indexing is a bit vestigial...
            const box: CollisionBox = (collisionBoxArray.get(k): any);
            collisionArrays.iconBox = {x1: box.x1, y1: box.y1, x2: box.x2, y2: box.y2, padding: box.padding, projectedAnchorX: box.projectedAnchorX, projectedAnchorY: box.projectedAnchorY, projectedAnchorZ: box.projectedAnchorZ, tileAnchorX: box.tileAnchorX, tileAnchorY: box.tileAnchorY};
            collisionArrays.iconFeatureIndex = box.featureIndex;
            break; // Only one box allowed per instance
        }

ではSymbolBucket#deserializeCollisionBoxesに与えるcollisionBoxArrayとは何者でしょうか(symbol/placement.js#L432)?

collisionBoxArrayの起源

collisionBoxArrayTileLayerParametersに含まれており、Placement#getBucketPartsはそれをTile#collisionBoxArrayから取得します(symbol/placement.js#L242)。

        const collisionBoxArray = tile.collisionBoxArray;

Tile#loadVectorDataTile#collisionBoxArrayを設定します(source/tile.js#L245)。

        this.collisionBoxArray = data.collisionBoxArray;

Tile#loadVectorDatasource/geojson_source.js#L372source/vector_tile_source.js#L320で呼び出されています。

            tile.loadVectorData(data, this.map.painter, message === 'reloadTile');
            tile.loadVectorData(data, this.map.painter);

上記呼び出しはVectorTileWorkerSource#loadTileもしくはVectorTileWorkerSource#reloadTileの結果として発生します。 VectorTileWorkerSource#loadTileVectorTileWorkerSource#reloadTileのいずれも生のVector Tileデータを解析するのにWorkerTile#parseを呼び出します。 WorkerTile#parseは解析したデータをsource/worker_tile.js#L261-L272で出力します。

                callback(null, {
                    buckets: values(buckets).filter(b => !b.isEmpty()),
                    featureIndex,
                    collisionBoxArray: this.collisionBoxArray,
                    glyphAtlasImage: glyphAtlas.image,
                    lineAtlas,
                    imageAtlas,
                    // Only used for benchmarking:
                    glyphMap: this.returnDependencies ? glyphMap : null,
                    iconMap: this.returnDependencies ? iconMap : null,
                    glyphPositions: this.returnDependencies ? glyphAtlas.positions : null
                });

上記コードのcollisionBoxArray (=WorkerTile#collisionBoxArray)は結果的にTile#collisionBoxArrayになります。 シンボルのアイコンに絞ると、WorkerTile#parsesource/worker_tile.js#L239-L248performSymbolLayoutを呼び出し、WorkerTile#collisionBoxArrayを更新します。

                        performSymbolLayout(bucket,
                            glyphMap,
                            glyphAtlas.positions,
                            iconMap,
                            imageAtlas.iconPositions,
                            this.showCollisionBoxes,
                            availableImages,
                            this.tileID.canonical,
                            this.tileZoom,
                            this.projection);

このコンテキストではbucketSymbolBucketであり、またSymbolBucket#collisionBoxArrayはBucketを生成する関数(source/worker_tile.js#L155-L168)に与えられるWorkerTile#collisionBoxArrayを指していることにご注意ください。

                const bucket = buckets[layer.id] = layer.createBucket({
                    index: featureIndex.bucketLayerIDs.length,
                    layers: family,
                    zoom: this.zoom,
                    canonical: this.canonical,
                    pixelRatio: this.pixelRatio,
                    overscaling: this.overscaling,
                    collisionBoxArray: this.collisionBoxArray,
                    sourceLayerIndex,
                    sourceID: this.source,
                    enableTerrain: this.enableTerrain,
                    projection: this.projection.spec,
                    availableImages
                });

performSymbolLayoutbucket内のFeatureをリストし、symbol/symbol_layout.js#L322addFeatureを呼び出します。

            addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, availableImages, canonical, projection);

addFeature(正確にはその内部関数addSymbolAtAnchor)はaddSymbolを呼び出します(symbol/symbol_layout.js#L438-L442)。

        addSymbol(bucket, anchor, globe, line, shapedTextOrientations, shapedIcon, imageMap, verticallyShapedIcon, bucket.layers[0],
            bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex,
            bucket.index, textPadding, textAlongLine, textOffset,
            iconBoxScale, iconPadding, iconAlongLine, iconOffset,
            feature, sizes, isSDFIcon, availableImages, canonical);

シンボルのアイコンに絞ると、addSymbolsymbol/symbol_layout.js#L731evaluateBoxCollisionFeatureを呼び出します。

        iconBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, collisionFeatureAnchor, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconPadding, iconRotate);

evaluateBoxCollisionFeaturecollisionBoxArrayに要素を追加します(symbol/symbol_layout.js#L634)。

    collisionBoxArray.emplaceBack(projectedAnchor.x, projectedAnchor.y, projectedAnchor.z, tileAnchor.x, tileAnchor.y, x1, y1, x2, y2, padding, featureIndex, sourceLayerIndex, bucketIndex);

ここでcollisionBoxArrayWorkerTile#collisionBoxArrayと同一であることにご注意ください。

SymbolBucket#symbolInstances vs SymbolBucket#collisionArrays

symbol/placement.js#L791と行symbol/placement.js#L795によれば、SymbolBucket#symbolInstancesの要素はSymbolBucket#collisionArraysの要素に対応しています

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

addSymbolsymbol/symbol_layout.js#L854-L884SymbolBucket#symbolInstancesにも要素を追加しています。

    bucket.symbolInstances.emplaceBack(
        projectedAnchor.x,
        projectedAnchor.y,
        projectedAnchor.z,
        anchor.x,
        anchor.y,
        placedTextSymbolIndices.right >= 0 ? placedTextSymbolIndices.right : -1,
        placedTextSymbolIndices.center >= 0 ? placedTextSymbolIndices.center : -1,
        placedTextSymbolIndices.left >= 0 ? placedTextSymbolIndices.left : -1,
        placedTextSymbolIndices.vertical  >= 0 ? placedTextSymbolIndices.vertical : -1,
        placedIconSymbolIndex,
        verticalPlacedIconSymbolIndex,
        key,
        textBoxIndex !== undefined ? textBoxIndex : bucket.collisionBoxArray.length,
        textBoxIndex !== undefined ? textBoxIndex + 1 : bucket.collisionBoxArray.length,
        verticalTextBoxIndex !== undefined ? verticalTextBoxIndex : bucket.collisionBoxArray.length,
        verticalTextBoxIndex !== undefined ? verticalTextBoxIndex + 1 : bucket.collisionBoxArray.length,
        iconBoxIndex !== undefined ? iconBoxIndex : bucket.collisionBoxArray.length,
        iconBoxIndex !== undefined ? iconBoxIndex + 1 : bucket.collisionBoxArray.length,
        verticalIconBoxIndex ? verticalIconBoxIndex : bucket.collisionBoxArray.length,
        verticalIconBoxIndex ? verticalIconBoxIndex + 1 : bucket.collisionBoxArray.length,
        featureIndex,
        numHorizontalGlyphVertices,
        numVerticalGlyphVertices,
        numIconVertices,
        numVerticalIconVertices,
        useRuntimeCollisionCircles,
        0,
        textOffset0,
        textOffset1,
        collisionCircleDiameter);

つまり、SymbolBucket#symbolInstancesSymbolBucket#collisionArraysの対応する要素は、それぞれ featureIndexiconFeatureIndexに全く同じFeatureのインデックスを持っています。

FeatureIndex#lookupSymbolFeaturesは何をしているのか?

FeatureIndex#lookupSymbolFeaturesは実際には何をしているのでしょうか? SymbolBucket#symbolInstancesから取り出したFeatureのインデックス(featureIndex)を最初の引数(symbolFeatureIndexes)に指定するのは理にかなっているでしょうか?

FeatureIndex#lookupSymbolFeaturesFeatureIndex#loadMatchingFeatureを繰り返し呼び出しています(data/feature_index.js#L257-L272, symbolFeatureIndexfeatureIndexData.featureIndex)。

        for (const symbolFeatureIndex of symbolFeatureIndexes) {
            this.loadMatchingFeature(
                result, {
                    bucketIndex,
                    sourceLayerIndex,
                    featureIndex: symbolFeatureIndex,
                    layoutVertexArrayOffset: 0
                },
                filter,
                filterLayerIDs,
                availableImages,
                styleLayers,
                serializedLayers
            );

        }

FeatureIndex#loadMatchingFeaturefeatureIndexdata/feature_index.js#L188-L190でFeatureを読み込むのに使っています。

        const sourceLayerName = this.sourceLayerCoder.decode(sourceLayerIndex);
        const sourceLayer = this.vtLayers[sourceLayerName];
        const feature = sourceLayer.feature(featureIndex);

一方で、performSymbolLayoutSymbolBucket#features内のFeatureを列挙し、ひとつずつaddFeatureに渡しています(symbol/symbol_layout.js#L197-L324)。

    for (const feature of bucket.features) {
        // ... 可読性のため割愛
        if (shapedText || shapedIcon) {
            addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, availableImages, canonical, projection);
        }
    }

ではSymbolBucket#featuresはどこから来るのでしょうか? SymbolBucket#populateSymbolBucket#featuresを初期化して埋めています(data/bucket/symbol_bucket.js#L475-L626)。

    populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) {
        // ... 可読性のため割愛
        this.features = [];
        // ... 可読性のため割愛
        for (const {feature, id, index, sourceLayerIndex} of features) {
            // ... 可読性のため割愛
            const symbolFeature: SymbolFeature = {
                id,
                text,
                icon,
                index,
                sourceLayerIndex,
                geometry: evaluationFeature.geometry,
                properties: feature.properties,
                type: vectorTileFeatureTypes[feature.type],
                sortKey
            };
            this.features.push(symbolFeature);
            // ... 可読性のため割愛
        }
        // ... 可読性のため割愛
    }

WorkerTile#parseはBucketを生成した直後にSymbolBucket#populateを呼び出しています(source/worker_tile.js#L171)。

                bucket.populate(features, options, this.tileID.canonical, this.tileTransform);

そしてWorkerTile#parseは最初の引数のfeaturessource/worker_tile.js#L137-L142で用意しています。

            const features = [];
            for (let index = 0; index < sourceLayer.length; index++) {
                const feature = sourceLayer.feature(index);
                const id = featureIndex.getId(feature, sourceLayerId);
                features.push({feature, id, index, sourceLayerIndex});
            }

ということで上記コード内のindexは最終的に他のFeatureに関する情報と併せてSymbolBucket#symbolInstancesfeatureIndexとなります。

よって SymbolBucket#symbolInstancesfeatureIndexプロパティはFeatureIndex#lookupSymbolFeaturesの最初の引数symbolFeatureIndexesと整合性がとれていることがわかりました。

アイコンの大きさを計算する

前回の投稿で、アイコンのscaleパラメータを再現できると結論づけました。 しかし、partiallyEvaluatedIconSizeを計算するためのevaluateSizeForZoom関数がmapbox-gl-jsからエクスポートされていないことに後から気づきました。 ということでライブラリを実装するためにはmapbox-gl-jsからevaluateSizeForZoomの複製を作成しなければなりませんでした。

evaluateSizeForZoomが機能するために、以下の型や関数も複製しなければなりませんでした。

複製したものは私のGitHubレポジトリにあります。

まとめ

このブログ投稿では、TileSymbolBucketをリストし、シンボルのFeatureを解決する方法を示しました。 最終的なループは以下のようになります。

// 仮定 map: mapboxgl.Map, layerId: string
const style = map.style;
const layer = style._layers[layerId];
const sourceCache = style._getLayerSourceCache(layer);
const layerTiles = sourceCache.getRenderableIds(true).map(id => sourceCache.getTileByID(id));
for (const tile of layerTiles) {
    const bucket = tile.getBucket(layer);
    for (let i = 0; i < bucket.symbolInstances.length; ++i>) {
        const featureIndex = bucket.symbolInstances.get(i).featureIndex;
        // ... 衝突ボックスを再計算しFeatureに関連づける
    }
}

またアイコンのサイズを計算する方法もカバーしました。

ライブラリを実装している途中、mapbox-gl-jsの内部タイプが利用できないというTypeScript特有の課題に直面しました。 今後のブログ投稿で、その課題にどう対処したかを共有するつもりです。

私のGitHubレポジトリでライブラリもご確認ください!

補足

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

Map

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

Map#queryRenderedFeatures

定義: ui/map.js#L1697-L1720

このメソッドはMap#queryRenderedFeatures APIを実装します。

Style

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

Style#_layers

定義: style/style.js#L148

    _layers: {[_: string]: StyleLayer};
Style#_serializedLayers

定義: style/style.js#L152

    _serializedLayers: {[_: string]: Object};
Style#_availableImages

定義: style/style.js#L168

    _availableImages: Array<string>;
Style#queryRenderedFeatures

定義: style/style.js#L1330-L1396

query_features.queryRenderedSymbols

定義: source/query_features.js#L79-L149

Style#_updatePlacement

定義: src/style/style.js#L1692-L1766

このメソッドはPauseablePlacement#continuePlacementを呼び出します。

Tile

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

Tile#tileID

定義: source/tile.js#L96

    tileID: OverscaledTileID;
Tile#collisionBoxArray

定義: source/tile.js#L115

    collisionBoxArray: ?CollisionBoxArray;
Tile#loadVectorData

定義: source/tile.js#L221-L290

VectorTileWorkerSource

定義: source/vector_tile_worker_source.js#L140-L309

VectorTileWorkerSource#loadTile

定義: source/vector_tile_worker_source.js#L177-L238

このメソッドはWorkerスレッドで実行されます。

VectorTileWorkerSource#reloadTile

定義: source/vector_tile_worker_source.js#L244-L275

このメソッドはWorkerスレッドで実行されます。

WorkerTile

定義: source/worker_tile.js#L36-L277

WorkerTile#collisionBoxArray

定義: source/worker_tile.js#L57

    collisionBoxArray: CollisionBoxArray;
WorkerTile#parse

定義: source/worker_tile.js#L83-L276

symbol_layout.performSymbolLayout

定義: symbol/symbol_layout.js#L152-L329

WorkerTile#parseはこの関数を呼び出します(source/worker_tile.js#L239-L248)。

                        performSymbolLayout(bucket,
                            glyphMap,
                            glyphAtlas.positions,
                            iconMap,
                            imageAtlas.iconPositions,
                            this.showCollisionBoxes,
                            availableImages,
                            this.tileID.canonical,
                            this.tileZoom,
                            this.projection);
symbol_layout.addFeature

定義: symbol/symbol_layout.js#L367-L500

シンボルのアイコンに絞ると、symbol_layout.performSymbolLayoutsymbol/symbol_layout.js#L322でこの関数を呼び出します。

            addFeature(bucket, feature, shapedTextOrientations, shapedIcon, imageMap, sizes, layoutTextSize, layoutIconSize, textOffset, isSDFIcon, availableImages, canonical, projection);
symbol_layout.addSymbol

定義: symbol/symbol_layout.js#L657-L885

この関数はSymbolBucket#symbolInstancesを更新します(symbol/symbol_layout.js#L854-L884)。

    bucket.symbolInstances.emplaceBack(
        projectedAnchor.x,
        projectedAnchor.y,
        projectedAnchor.z,
        anchor.x,
        anchor.y,
        placedTextSymbolIndices.right >= 0 ? placedTextSymbolIndices.right : -1,
        placedTextSymbolIndices.center >= 0 ? placedTextSymbolIndices.center : -1,
        placedTextSymbolIndices.left >= 0 ? placedTextSymbolIndices.left : -1,
        placedTextSymbolIndices.vertical  >= 0 ? placedTextSymbolIndices.vertical : -1,
        placedIconSymbolIndex,
        verticalPlacedIconSymbolIndex,
        key,
        textBoxIndex !== undefined ? textBoxIndex : bucket.collisionBoxArray.length,
        textBoxIndex !== undefined ? textBoxIndex + 1 : bucket.collisionBoxArray.length,
        verticalTextBoxIndex !== undefined ? verticalTextBoxIndex : bucket.collisionBoxArray.length,
        verticalTextBoxIndex !== undefined ? verticalTextBoxIndex + 1 : bucket.collisionBoxArray.length,
        iconBoxIndex !== undefined ? iconBoxIndex : bucket.collisionBoxArray.length,
        iconBoxIndex !== undefined ? iconBoxIndex + 1 : bucket.collisionBoxArray.length,
        verticalIconBoxIndex ? verticalIconBoxIndex : bucket.collisionBoxArray.length,
        verticalIconBoxIndex ? verticalIconBoxIndex + 1 : bucket.collisionBoxArray.length,
        featureIndex,
        numHorizontalGlyphVertices,
        numVerticalGlyphVertices,
        numIconVertices,
        numVerticalIconVertices,
        useRuntimeCollisionCircles,
        0,
        textOffset0,
        textOffset1,
        collisionCircleDiameter);

symbol_layout.addFeatureの内部関数addSymbolAtAnchorがこの関数を呼び出します(symbol/symbol_layout.js#L438-L442)。

        addSymbol(bucket, anchor, globe, line, shapedTextOrientations, shapedIcon, imageMap, verticallyShapedIcon, bucket.layers[0],
            bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex,
            bucket.index, textPadding, textAlongLine, textOffset,
            iconBoxScale, iconPadding, iconAlongLine, iconOffset,
            feature, sizes, isSDFIcon, availableImages, canonical);
symbol_layout.evaluateBoxCollisionFeature

定義: symbol/symbol_layout.js#L580-L637

この関数はcollisionBoxArrayを更新します(symbol/symbol_layout.js#L634)。

    collisionBoxArray.emplaceBack(projectedAnchor.x, projectedAnchor.y, projectedAnchor.z, tileAnchor.x, tileAnchor.y, x1, y1, x2, y2, padding, featureIndex, sourceLayerIndex, bucketIndex);

シンボルのアイコンに絞ると、symbol_layout.addSymbolsymbol/symbol_layout.js#L731でこの関数を呼び出します。

        iconBoxIndex = evaluateBoxCollisionFeature(collisionBoxArray, collisionFeatureAnchor, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconPadding, iconRotate);

StyleLayer

定義: style/style_layer.js#L39-L323

SymbolBucket

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

SymbolBucket#bucketInstanceId

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

    bucketInstanceId: number;
SymbolBucket#features

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

    features: Array<SymbolFeature>;
SymbolBucket#collisionArrays

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

    collisionArrays: Array<CollisionArrays>;

このプロパティの要素はSymbolBucket#symbolInstancesの要素に対応します。

SymbolBucket#deserializeCollisionBoxesはこのプロパティを初期化して埋めます。

SymbolBucket#symbolInstances

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

    symbolInstances: SymbolInstanceArray;

このプロパティの要素はSymbolBucket#collisionArraysの要素に対応します。

SymbolBucket#collisionBoxArray

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

SymbolBucket#populate

定義: data/bucket/symbol_bucket.js#L475-L626

SymbolBucket#deserializeCollisionBoxes

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

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

SymbolBucket#_deserializeCollisionBoxesForSymbol

定義: data/bucket/symbol_bucket.js#L943-L977

FeatureIndex

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

FeatureIndex#lookupSymbolFeatures

定義: data/feature_index.js#L244-L274

このメソッドはFeatureIndex#loadMatchingFeatureを呼び出します(data/feature_index.js#L258-L270)。

            this.loadMatchingFeature(
                result, {
                    bucketIndex,
                    sourceLayerIndex,
                    featureIndex: symbolFeatureIndex,
                    layoutVertexArrayOffset: 0
                },
                filter,
                filterLayerIDs,
                availableImages,
                styleLayers,
                serializedLayers
            );
FeatureIndex#loadMatchingFeature

定義: data/feature_index.js#L172-L240

このメソッドはシンボルのFeature(GeoJSON形式)をVector Tileデータから読み込みます。

CollisionIndex

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

CollisionIndex#queryRenderedSymbols

定義: symbol/collision_index.js#L341-L399

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]);
    }

GridIndex

定義: symbol/grid_index.js#L24-L341

GridIndex#bboxes

定義: symbol/grid_index.js#L29

    bboxes: Array<number>;

GridIndex#boxKeysもご覧ください。

GridIndex#boxKeys

定義: symbol/grid_index.js#L26

    boxKeys: Array<any>;

このプロパティはGridIndex#bboxes内の同じインデックスにあるボックスに対応するFeatureのキーを格納します。

GridIndex#insertはこのプロパティを更新します。

GridIndex#circles

定義: symbol/grid_index.js#L30

    circles: Array<number>;

GridIndex#circleKeysもご覧ください。

GridIndex#circleKeys

定義: symbol/grid_index.js#L25

    circleKeys: Array<any>;

このプロパティはGridIndex#circles内の同じインデックスにあるサークルに対応するFeatureのキーを格納します。

GridIndex#insert

定義: symbol/grid_index.js#L71-L78

このメソッドは指定したkeyGridIndex#boxKeysに追加します(symbol/grid_index.js#L73)。

        this.boxKeys.push(key);
GridIndex#query

定義: symbol/grid_index.js#L163-L165

    query(x1: number, y1: number, x2: number, y2: number, predicate?: any): Array<GridItem> {
        return (this._query(x1, y1, x2, y2, false, predicate): any);
    }

GridIndex#_queryもご覧ください。

GridIndex#_query

定義: symbol/grid_index.js#L98-L137

このメソッドはGridIndex#_forEachCellを呼び出します(symbol/grid_index.js#L134)。

            this._forEachCell(x1, y1, x2, y2, this._queryCell, result, queryArgs, predicate);
GridIndex#_forEachCell

定義: symbol/grid_index.js#L291-L303

    _forEachCell(x1: number, y1: number, x2: number, y2: number, fn: any, arg1: any, arg2?: any, predicate?: any) {
        const cx1 = this._convertToXCellCoord(x1);
        const cy1 = this._convertToYCellCoord(y1);
        const cx2 = this._convertToXCellCoord(x2);
        const cy2 = this._convertToYCellCoord(y2);

        for (let x = cx1; x <= cx2; x++) {
            for (let y = cy1; y <= cy2; y++) {
                const cellIndex = this.xCellCount * y + x;
                if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) return;
            }
        }
    }

このメソッドは指定したバウンディングボックスと重なるすべてのセルにfnを適用します。

GridIndex#queryのコンテキストでは、fnGridIndex#_queryCellです。

GridIndex#_queryCell

定義: symbol/grid_index.js#L175-L240

このメソッドは指定したセルで指定したバウンディングボックスと重なるボックスとサークルを集めます。

GridItem

定義: symbol/grid_index.js#L3-L9

type GridItem = {
    key: any,
    x1: number,
    y1: number,
    x2: number,
    y2: number
};

Placement

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

Placement#retainedQueryData

定義: symbol/placement.js#L205

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

RetainedQueryDataもご覧ください。

Placement#getBucketParts

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

LayerPlacement#continuePlacementはこのメソッドを呼び出します(style/pauseable_placement.js#L37)。

            placement.getBucketParts(bucketParts, styleLayer, tile, this._sortAcrossTiles);

前回のブログ投稿の関連する節もご覧ください。

Placement#placeLayerBucketPart

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

LayerPlacement#continuePlacementはこのメソッドを呼び出します(style/pauseable_placement.js#L52)。

            placement.placeLayerBucketPart(bucketPart, this._seenCrossTileIDs, showCollisionBoxes, bucketPart.symbolInstanceStart === 0);

前回のブログ投稿の関連する節もご覧ください。

Placement#placeLayerBucketPart.placeSymbol

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

RetainedQueryData

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

export class RetainedQueryData {
    bucketInstanceId: number;
    featureIndex: FeatureIndex;
    sourceLayerIndex: number;
    bucketIndex: number;
    tileID: OverscaledTileID;
    featureSortOrder: ?Array<number>
    // ... 可読性のため割愛
}

BucketPart

定義: symbol/placement.js#L183-L188

export type BucketPart = {
    sortKey?: number | void,
    symbolInstanceStart: number,
    symbolInstanceEnd: number,
    parameters: TileLayerParameters
};

TileLayerParametersもご覧ください。

TileLayerParameters

定義: symbol/placement.js#L169-L181

type TileLayerParameters = {
    bucket: SymbolBucket,
    layout: any,
    posMatrix: Mat4,
    textLabelPlaneMatrix: Mat4,
    labelToScreenMatrix: ?Mat4,
    scale: number,
    textPixelRatio: number,
    holdingForFade: boolean,
    collisionBoxArray: ?CollisionBoxArray,
    partiallyEvaluatedTextSize: any,
    collisionGroup: any
};

PauseablePlacement

定義: style/pauseable_placement.js#L62-L132

PauseablePlacement#continuePlacement

定義: style/pauseable_placement.js#L89-L126

このメソッドはLayerPlacement#continuePlacementを呼び出します。

Style#_updatePlacementはこのメソッドを呼び出します(style/style.js#L1740)。

            this.pauseablePlacement.continuePlacement(this._order, this._layers, layerTiles);

LayerPlacement

定義: style/pauseable_placement.js#L15-L60

LayerPlacement#continuePlacement

定義: style/pauseable_placement.js#L32-L59

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

PauseablePlacement#continuePlacementはこのメソッドを呼び出します(style/pauseable_placement.js#L109)。

                const pausePlacement = this._inProgressLayer.continuePlacement(layerTiles[layer.source], this.placement, this._showCollisionBoxes, layer, shouldPausePlacement);

symbol_size.SizeData

定義: symbol/symbol_size.js#L15-L32

export type SizeData = {
    kind: 'constant',
    layoutSize: number
} | {
    kind: 'source'
} | {
    kind: 'camera',
    minZoom: number,
    maxZoom: number,
    minSize: number,
    maxSize: number,
    interpolationType: ?InterpolationType
} | {
    kind: 'composite',
    minZoom: number,
    maxZoom: number,
    interpolationType: ?InterpolationType
};

symbol_size.InterpolatedSize

定義: symbol/symbol_size.js#L34-L37

export type InterpolatedSize = {|
    uSize: number,
    uSizeT: number
|};

symbol_size.evaluateSizeForZoom

定義: symbol/symbol_size.js#L92-L118

interpolate.InterpolationType

定義: style-spec/expression/definitions/interpolate.js#L17-L20

export type InterpolationType =
    { name: 'linear' } |
    { name: 'exponential', base: number } |
    { name: 'cubic-bezier', controlPoints: [number, number, number, number] };

Interpolate.interpolationFactor

定義: style-spec/expression/definitions/interpolate.js#L45-L57

interpolate.exponentialInterpolation

定義: style-spec/expression/definitions/interpolate.js#L255-L266

util.interpolate

定義: style-spec/util/interpolate.js#L5-L7

export function number(a: number, b: number, t: number): number {
    return (a * (1 - t)) + (b * t);
}

util.clamp

定義: util/util.js#L211-L213

export function clamp(n: number, min: number, max: number): number {
    return Math.min(max, Math.max(min, n));
}

SymbolInstanceArray

定義: data/array_types.js#L1188-L1198

この配列の各要素はSymbolInstanceStructです。

SymbolInstanceStruct

定義: data/array_types.js#L1146-L1179

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;
};