diff --git a/flow-typed/style-spec.js b/flow-typed/style-spec.js index 72b7ce4efef..ce38e309978 100644 --- a/flow-typed/style-spec.js +++ b/flow-typed/style-spec.js @@ -208,7 +208,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, @@ -239,7 +238,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 10d88250e46..d81f1f7a8bf 100644 --- a/src/data/bucket.js +++ b/src/data/bucket.js @@ -14,7 +14,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 473c8348808..45965e348ff 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -293,6 +293,7 @@ class SymbolBucket implements Bucket { collisionCircle: CollisionBuffers; uploaded: boolean; sourceLayerIndex: number; + sourceID: string; constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; @@ -313,6 +314,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 d4b1c886ab2..c404deed477 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -979,19 +979,6 @@ }, "property-type": "data-constant" }, - "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", "default": false, @@ -2018,19 +2005,6 @@ }, "property-type": "data-constant" }, - "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", "default": false, 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 38c93414912..bad95a0c2de 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -1004,7 +1004,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; @@ -1033,7 +1033,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 0f4bb32dff6..0a3671d06d8 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 a674a07c830..5dec1b1e3fa 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -123,7 +123,8 @@ const defaultOptions = { maxTileCacheSize: null, transformRequest: null, - fadeDuration: 300 + fadeDuration: 300, + crossSourceCollisions: true }; /** @@ -198,6 +199,7 @@ const defaultOptions = { * 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 {number} [options.fadeDuration=300] Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. + * @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', @@ -247,6 +249,7 @@ class Map extends Camera { _hash: Hash; _delegatedListeners: any; _fadeDuration: number; + _crossSourceCollisions: boolean; _crossFadingFactor: number; _collectResourceTiming: boolean; _renderTaskQueue: TaskQueue; @@ -306,6 +309,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(); @@ -1654,7 +1658,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();