diff --git a/flow-typed/style-spec.js b/flow-typed/style-spec.js index 88439b72edb..df6e3260355 100644 --- a/flow-typed/style-spec.js +++ b/flow-typed/style-spec.js @@ -207,7 +207,6 @@ declare type SymbolLayerSpecification = {| "symbol-avoid-edges"?: PropertyValueSpecification, "icon-allow-overlap"?: PropertyValueSpecification, "icon-ignore-placement"?: PropertyValueSpecification, - "icon-collision-group"?: string, "icon-optional"?: PropertyValueSpecification, "icon-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, "icon-size"?: DataDrivenPropertyValueSpecification, @@ -238,7 +237,6 @@ declare type SymbolLayerSpecification = {| "text-offset"?: DataDrivenPropertyValueSpecification<[number, number]>, "text-allow-overlap"?: PropertyValueSpecification, "text-ignore-placement"?: PropertyValueSpecification, - "text-collision-group"?: string, "text-optional"?: PropertyValueSpecification, "visibility"?: "visible" | "none" |}, diff --git a/src/data/bucket.js b/src/data/bucket.js index 86e3b44fcef..ce0c94405a8 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -13,7 +13,8 @@ export type BucketParameters = { pixelRatio: number, overscaling: number, collisionBoxArray: CollisionBoxArray, - sourceLayerIndex: number + sourceLayerIndex: number, + sourceID: string } export type PopulateParameters = { diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index c4276144bf6..dc0ff46097c 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -288,6 +288,7 @@ class SymbolBucket implements Bucket { collisionCircle: CollisionBuffers; uploaded: boolean; sourceLayerIndex: number; + sourceID: string; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -308,6 +309,8 @@ class SymbolBucket implements Bucket { const layout = this.layers[0].layout; this.sortFeaturesByY = layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') || layout.get('text-ignore-placement') || layout.get('icon-ignore-placement'); + + this.sourceID = options.sourceID; } createArrays() { diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index ded6f677767..660cd31de47 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -109,7 +109,8 @@ class WorkerTile { pixelRatio: this.pixelRatio, overscaling: this.overscaling, collisionBoxArray: this.collisionBoxArray, - sourceLayerIndex: sourceLayerIndex + sourceLayerIndex: sourceLayerIndex, + sourceID: this.source }); bucket.populate(features, options); diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index bdc094c9ed5..706d616ef1f 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -904,19 +904,6 @@ "data-driven styling": {} } }, - "icon-collision-group": { - "type": "string", - "doc": "Restricts collision detection to other symbols in the same group.", - "requires": [ - "icon-image" - ], - "sdk-support": { - "basic functionality": { - "js": "0.45.0" - }, - "data-driven styling": {} - } - }, "icon-optional": { "type": "boolean", "function": "piecewise-constant", @@ -1790,19 +1777,6 @@ "data-driven styling": {} } }, - "text-collision-group": { - "type": "string", - "doc": "Restricts collision detection to other symbols in the same group.", - "requires": [ - "text-field" - ], - "sdk-support": { - "basic functionality": { - "js": "0.45.0" - }, - "data-driven styling": {} - } - }, "text-optional": { "type": "boolean", "function": "piecewise-constant", diff --git a/src/style/pauseable_placement.js b/src/style/pauseable_placement.js index 9a7c8323602..2417e19c7c6 100644 --- a/src/style/pauseable_placement.js +++ b/src/style/pauseable_placement.js @@ -40,9 +40,12 @@ class PauseablePlacement { _inProgressLayer: ?LayerPlacement; constructor(transform: Transform, order: Array, - forceFullPlacement: boolean, showCollisionBoxes: boolean, fadeDuration: number) { + forceFullPlacement: boolean, + showCollisionBoxes: boolean, + fadeDuration: number, + crossSourceCollisions: boolean) { - this.placement = new Placement(transform, fadeDuration); + this.placement = new Placement(transform, fadeDuration, crossSourceCollisions); this._currentPlacementIndex = order.length - 1; this._forceFullPlacement = forceFullPlacement; this._showCollisionBoxes = showCollisionBoxes; diff --git a/src/style/style.js b/src/style/style.js index a8dbde5ab9c..30789d77f54 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -962,7 +962,7 @@ class Style extends Evented { } } - _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number) { + _updatePlacement(transform: Transform, showCollisionBoxes: boolean, fadeDuration: number, crossSourceCollisions: boolean) { let symbolBucketsChanged = false; let placementCommitted = false; @@ -991,7 +991,7 @@ class Style extends Evented { const forceFullPlacement = this._layerOrderChanged; if (forceFullPlacement || !this.pauseablePlacement || (this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now()))) { - this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration); + this.pauseablePlacement = new PauseablePlacement(transform, this._order, forceFullPlacement, showCollisionBoxes, fadeDuration, crossSourceCollisions); this._layerOrderChanged = false; } diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index f0e03b611ce..2c00d943685 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -20,7 +20,6 @@ export type LayoutProps = {| "symbol-avoid-edges": DataConstantProperty, "icon-allow-overlap": DataConstantProperty, "icon-ignore-placement": DataConstantProperty, - "icon-collision-group": DataConstantProperty, "icon-optional": DataConstantProperty, "icon-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">, "icon-size": DataDrivenProperty, @@ -51,7 +50,6 @@ export type LayoutProps = {| "text-offset": DataDrivenProperty<[number, number]>, "text-allow-overlap": DataConstantProperty, "text-ignore-placement": DataConstantProperty, - "text-collision-group": DataConstantProperty, "text-optional": DataConstantProperty, |}; @@ -61,7 +59,6 @@ const layout: Properties = new Properties({ "symbol-avoid-edges": new DataConstantProperty(styleSpec["layout_symbol"]["symbol-avoid-edges"]), "icon-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["icon-allow-overlap"]), "icon-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["icon-ignore-placement"]), - "icon-collision-group": new DataConstantProperty(styleSpec["layout_symbol"]["icon-collision-group"]), "icon-optional": new DataConstantProperty(styleSpec["layout_symbol"]["icon-optional"]), "icon-rotation-alignment": new DataConstantProperty(styleSpec["layout_symbol"]["icon-rotation-alignment"]), "icon-size": new DataDrivenProperty(styleSpec["layout_symbol"]["icon-size"]), @@ -92,7 +89,6 @@ const layout: Properties = new Properties({ "text-offset": new DataDrivenProperty(styleSpec["layout_symbol"]["text-offset"]), "text-allow-overlap": new DataConstantProperty(styleSpec["layout_symbol"]["text-allow-overlap"]), "text-ignore-placement": new DataConstantProperty(styleSpec["layout_symbol"]["text-ignore-placement"]), - "text-collision-group": new DataConstantProperty(styleSpec["layout_symbol"]["text-collision-group"]), "text-optional": new DataConstantProperty(styleSpec["layout_symbol"]["text-optional"]), }); diff --git a/src/symbol/placement.js b/src/symbol/placement.js index e7bb29cc481..461e57d75e0 100644 --- a/src/symbol/placement.js +++ b/src/symbol/placement.js @@ -132,8 +132,9 @@ export class Placement { fadeDuration: number; retainedQueryData: {[number]: RetainedQueryData}; collisionGroups: CollisionGroups; + crossSourceCollisions: boolean; - constructor(transform: Transform, fadeDuration: number) { + constructor(transform: Transform, fadeDuration: number, crossSourceCollisions: boolean) { this.transform = transform.clone(); this.collisionIndex = new CollisionIndex(this.transform); this.placements = {}; @@ -142,6 +143,7 @@ export class Placement { this.fadeDuration = fadeDuration; this.retainedQueryData = {}; this.collisionGroups = new CollisionGroups(); + this.crossSourceCollisions = crossSourceCollisions; } placeLayerTile(styleLayer: StyleLayer, tile: Tile, showCollisionBoxes: boolean, seenCrossTileIDs: { [string | number]: boolean }) { @@ -195,10 +197,9 @@ export class Placement { const iconWithoutText = !bucket.hasTextData() || layout.get('text-optional'); const textWithoutIcon = !bucket.hasIconData() || layout.get('icon-optional'); - const textCollisionGroup = - this.collisionGroups.get(layout.get('text-collision-group')); - const iconCollisionGroup = - this.collisionGroups.get(layout.get('icon-collision-group')); + const collisionGroup = this.crossSourceCollisions ? + this.collisionGroups.get() : + this.collisionGroups.get(bucket.sourceID); for (const symbolInstance of bucket.symbolInstances) { if (!seenCrossTileIDs[symbolInstance.crossTileID]) { @@ -225,7 +226,7 @@ export class Placement { } if (symbolInstance.collisionArrays.textBox) { placedGlyphBoxes = this.collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.textBox, - layout.get('text-allow-overlap'), textPixelRatio, posMatrix, textCollisionGroup.predicate); + layout.get('text-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate); placeText = placedGlyphBoxes.box.length > 0; offscreen = offscreen && placedGlyphBoxes.offscreen; } @@ -246,7 +247,7 @@ export class Placement { textLabelPlaneMatrix, showCollisionBoxes, layout.get('text-pitch-alignment') === 'map', - textCollisionGroup.predicate); + collisionGroup.predicate); // If text-allow-overlap is set, force "placedCircles" to true // In theory there should always be at least one circle placed // in this case, but for now quirks in text-anchor @@ -260,7 +261,7 @@ export class Placement { } if (symbolInstance.collisionArrays.iconBox) { placedIconBoxes = this.collisionIndex.placeCollisionBox(symbolInstance.collisionArrays.iconBox, - layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, iconCollisionGroup.predicate); + layout.get('icon-allow-overlap'), textPixelRatio, posMatrix, collisionGroup.predicate); placeIcon = placedIconBoxes.box.length > 0; offscreen = offscreen && placedIconBoxes.offscreen; } @@ -276,15 +277,15 @@ export class Placement { if (placeText && placedGlyphBoxes) { this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, layout.get('text-ignore-placement'), - bucket.bucketInstanceId, textFeatureIndex, textCollisionGroup.ID); + bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID); } if (placeIcon && placedIconBoxes) { this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'), - bucket.bucketInstanceId, iconFeatureIndex, iconCollisionGroup.ID); + bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID); } if (placeText && placedGlyphCircles) { this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'), - bucket.bucketInstanceId, textFeatureIndex, textCollisionGroup.ID); + bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID); } assert(symbolInstance.crossTileID !== 0); diff --git a/src/ui/map.js b/src/ui/map.js index b4bdb706208..835ece598aa 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -124,7 +124,8 @@ const defaultOptions = { maxTileCacheSize: null, transformRequest: null, - fadeDuration: 300 + fadeDuration: 300, + crossSourceCollisions: true }; /** @@ -194,6 +195,7 @@ const defaultOptions = { * @param {RequestTransformFunction} [options.transformRequest=null] A callback run before the Map makes a request for an external URL. The callback can be used to modify the url, set headers, or set the credentials property for cross-origin requests. * Expected to return an object with a `url` property and optionally `headers` and `credentials` properties. * @param {boolean} [options.collectResourceTiming=false] If `true`, Resource Timing API information will be collected for requests made by GeoJSON and Vector Tile web workers (this information is normally inaccessible from the main Javascript thread). Information will be returned in a `resourceTiming` property of relevant `data` events. + * @param {boolean} [options.crossSourceCollisions=true] If `true`, symbols from multiple sources can collide with each other during collision detection. If `false`, collision detection is run separately for the symbols in each source. * @example * var map = new mapboxgl.Map({ * container: 'map', @@ -243,6 +245,7 @@ class Map extends Camera { _hash: Hash; _delegatedListeners: any; _fadeDuration: number; + _crossSourceCollisions: boolean; _crossFadingFactor: number; _collectResourceTiming: boolean; _renderTaskQueue: TaskQueue; @@ -302,6 +305,7 @@ class Map extends Camera { this._bearingSnap = options.bearingSnap; this._refreshExpiredTiles = options.refreshExpiredTiles; this._fadeDuration = options.fadeDuration; + this._crossSourceCollisions = options.crossSourceCollisions; this._crossFadingFactor = 1; this._collectResourceTiming = options.collectResourceTiming; this._renderTaskQueue = new TaskQueue(); @@ -1594,7 +1598,7 @@ class Map extends Camera { this.style._updateSources(this.transform); } - this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, this._fadeDuration); + this._placementDirty = this.style && this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, this._fadeDuration, this._crossSourceCollisions); // Actually draw this.painter.render(this.style, { diff --git a/test/integration/render-tests/icon-collision-group/default/style.json b/test/integration/render-tests/icon-collision-group/default/style.json index ca88c9c3371..adf78059c81 100644 --- a/test/integration/render-tests/icon-collision-group/default/style.json +++ b/test/integration/render-tests/icon-collision-group/default/style.json @@ -2,6 +2,7 @@ "version": 8, "metadata": { "test": { + "crossSourceCollisions": false, "height": 128, "width": 128, "description": "Three collision groups of two layers each. Each group should show one label (overlapping with labels from other groups)" @@ -13,16 +14,13 @@ ], "zoom": 0, "sources": { - "point": { + "source1": { "type": "geojson", "data": { "type": "FeatureCollection", "features": [ { "type": "Feature", - "properties": { - "name": "A" - }, "geometry": { "type": "Point", "coordinates": [ @@ -30,16 +28,39 @@ 0 ] } - }, + } + ] + } + }, + "source2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + } + } + ] + } + }, + "source3": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ { "type": "Feature", - "properties": { - "name": "B" - }, "geometry": { "type": "Point", "coordinates": [ - 1, + 0, 0 ] } @@ -53,7 +74,7 @@ { "id": "defaultGroup1", "type": "symbol", - "source": "point", + "source": "source1", "layout": { "symbol-placement": "point", "icon-image": "building-12" @@ -62,7 +83,7 @@ { "id": "defaultGroup2", "type": "symbol", - "source": "point", + "source": "source1", "layout": { "symbol-placement": "point", "icon-image": "night-building-12" @@ -71,11 +92,10 @@ { "id": "firstGroup1", "type": "symbol", - "source": "point", + "source": "source2", "layout": { "symbol-placement": "point", "icon-image": "restaurant-12", - "icon-collision-group": "group1", "icon-offset": [ 7, 7 @@ -85,11 +105,10 @@ { "id": "firstGroup2", "type": "symbol", - "source": "point", + "source": "source2", "layout": { "symbol-placement": "point", "icon-image": "night-restaurant-12", - "icon-collision-group": "group1", "icon-offset": [ 7, 7 @@ -99,11 +118,10 @@ { "id": "secondGroup1", "type": "symbol", - "source": "point", + "source": "source3", "layout": { "symbol-placement": "point", "icon-image": "school-12", - "icon-collision-group": "group2", "icon-offset": [ 14, 14 @@ -113,11 +131,10 @@ { "id": "secondGroup2", "type": "symbol", - "source": "point", + "source": "source3", "layout": { "symbol-placement": "point", "icon-image": "night-school-12", - "icon-collision-group": "group2", "icon-offset": [ 14, 14 diff --git a/test/integration/render-tests/text-collision-group/default/style.json b/test/integration/render-tests/text-collision-group/default/style.json index 4630ecf9e56..8415c3258ac 100644 --- a/test/integration/render-tests/text-collision-group/default/style.json +++ b/test/integration/render-tests/text-collision-group/default/style.json @@ -2,6 +2,7 @@ "version": 8, "metadata": { "test": { + "crossSourceCollisions": false, "height": 128, "width": 256, "description": "Three collision groups of two layers each. Each group should show one label (overlapping with labels from other groups)" @@ -13,7 +14,75 @@ ], "zoom": 0, "sources": { - "point": { + "source1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "A" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -20, + 0 + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "B" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 20, + 0 + ] + } + } + ] + } + }, + "source2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "A" + }, + "geometry": { + "type": "Point", + "coordinates": [ + -20, + 0 + ] + } + }, + { + "type": "Feature", + "properties": { + "name": "B" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 20, + 0 + ] + } + } + ] + } + }, + "source3": { "type": "geojson", "data": { "type": "FeatureCollection", @@ -53,7 +122,7 @@ { "id": "defaultGroup1", "type": "symbol", - "source": "point", + "source": "source1", "layout": { "text-field": "Default Group {name}", "text-max-width": 30, @@ -66,7 +135,7 @@ { "id": "defaultGroup2", "type": "symbol", - "source": "point", + "source": "source1", "layout": { "text-field": "2nd Layer Default Group {name}", "text-max-width": 30, @@ -79,7 +148,7 @@ { "id": "firstGroup1", "type": "symbol", - "source": "point", + "source": "source2", "layout": { "text-field": "First Group {name}", "text-max-width": 30, @@ -87,7 +156,6 @@ "Open Sans Semibold", "Arial Unicode MS Bold" ], - "text-collision-group": "group1", "text-offset": [ 0, 0.5 @@ -97,7 +165,7 @@ { "id": "firstGroup2", "type": "symbol", - "source": "point", + "source": "source2", "layout": { "text-field": "2nd Layer First Group {name}", "text-max-width": 30, @@ -105,7 +173,6 @@ "Open Sans Semibold", "Arial Unicode MS Bold" ], - "text-collision-group": "group1", "text-offset": [ 0, 0.5 @@ -115,7 +182,7 @@ { "id": "secondGroup1", "type": "symbol", - "source": "point", + "source": "source3", "layout": { "text-field": "Second Group {name}", "text-max-width": 30, @@ -123,7 +190,6 @@ "Open Sans Semibold", "Arial Unicode MS Bold" ], - "text-collision-group": "group2", "text-offset": [ 0, 1 @@ -133,7 +199,7 @@ { "id": "secondGroup2", "type": "symbol", - "source": "point", + "source": "source3", "layout": { "text-field": "2nd Layer Second Group {name}", "text-max-width": 30, @@ -141,7 +207,6 @@ "Open Sans Semibold", "Arial Unicode MS Bold" ], - "text-collision-group": "group2", "text-offset": [ 0, 1 diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 2c4cd91fb02..aca3e7354e4 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -44,7 +44,8 @@ module.exports = function(style, options, _callback) { // eslint-disable-line im preserveDrawingBuffer: true, axonometric: options.axonometric || false, skew: options.skew || [0, 0], - fadeDuration: options.fadeDuration || 0 + fadeDuration: options.fadeDuration || 0, + crossSourceCollisions: typeof options.crossSourceCollisions === "undefined" ? true : options.crossSourceCollisions }); // Configure the map to never stop the render loop diff --git a/test/unit/data/symbol_bucket.test.js b/test/unit/data/symbol_bucket.test.js index 3ec8b6b583a..6ba28cb31e5 100644 --- a/test/unit/data/symbol_bucket.test.js +++ b/test/unit/data/symbol_bucket.test.js @@ -50,7 +50,7 @@ test('SymbolBucket', (t) => { const bucketA = bucketSetup(); const bucketB = bucketSetup(); const options = {iconDependencies: {}, glyphDependencies: {}}; - const placement = new Placement(transform, 0); + const placement = new Placement(transform, 0, true); const tileID = new OverscaledTileID(0, 0, 0, 0, 0); const crossTileSymbolIndex = new CrossTileSymbolIndex();