diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 1052c080180..ae4631d4b52 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -33,7 +33,6 @@ import type IndexBuffer from '../../gl/index_buffer'; import type VertexBuffer from '../../gl/vertex_buffer'; import type {SymbolQuad} from '../../symbol/quads'; import type {SizeData} from '../../symbol/symbol_size'; -import type {PossiblyEvaluatedPropertyValue} from '../../style/properties'; export type SingleCollisionBox = { x1: number; @@ -359,27 +358,8 @@ class SymbolBucket implements Bucket { sdfIcons: boolean; iconsNeedLinear: boolean; - // The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and - // `icon-size` at up to three: - // - // 1. `text-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `text-size` - // expressions, and to calculate the box dimensions for icon-text-fit. - // 2. `icon-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `icon-size` - // expressions. - // 3. `text-size` and `icon-size` at the zoom level of the bucket, plus one. Used to calculate collision boxes. - // 4. `text-size` at zoom level 18. Used for something line-symbol-placement-related. - // 5. For composite `*-size` expressions: two zoom levels of curve stops that "cover" the zoom level of the - // bucket. These go into a vertex buffer and are used by the shader to interpolate the size at render time. - // - // (1) and (2) are stored in `this.layers[0].layout`. The remainder are below. - // textSizeData: SizeData; iconSizeData: SizeData; - layoutTextSize: PossiblyEvaluatedPropertyValue; // (3) - layoutIconSize: PossiblyEvaluatedPropertyValue; // (3) - textMaxSize: PossiblyEvaluatedPropertyValue; // (4) - compositeTextSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue]; // (5) - compositeIconSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue]; // (5) placedGlyphArray: StructArray; placedIconArray: StructArray; @@ -413,26 +393,7 @@ class SymbolBucket implements Bucket { const unevaluatedLayoutValues = layer._unevaluatedLayout._values; this.textSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['text-size']); - if (this.textSizeData.functionType === 'composite') { - const {min, max} = this.textSizeData.zoomRange; - this.compositeTextSizes = [ - unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: min}), - unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: max}) - ]; - } - this.iconSizeData = getSizeData(this.zoom, unevaluatedLayoutValues['icon-size']); - if (this.iconSizeData.functionType === 'composite') { - const {min, max} = this.iconSizeData.zoomRange; - this.compositeIconSizes = [ - unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: min}), - unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: max}) - ]; - } - - this.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: this.zoom + 1}); - this.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate({zoom: this.zoom + 1}); - this.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate({zoom: 18}); const layout = this.layers[0].layout; this.sortFeaturesByY = layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') || diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js index 5c7f7738c53..852a763e1df 100644 --- a/src/source/worker_tile.js +++ b/src/source/worker_tile.js @@ -11,6 +11,7 @@ const {makeImageAtlas} = require('../render/image_atlas'); const {makeGlyphAtlas} = require('../render/glyph_atlas'); const {serialize} = require('../util/web_worker_transfer'); const TileCoord = require('./tile_coord'); +const EvaluationParameters = require('../style/evaluation_parameters'); import type {Bucket} from '../data/bucket'; import type Actor from '../util/actor'; @@ -182,17 +183,9 @@ class WorkerTile { function recalculateLayers(layers: $ReadOnlyArray, zoom: number) { // Layers are shared and may have been used by a WorkerTile with a different zoom. + const parameters = new EvaluationParameters(zoom); for (const layer of layers) { - layer.recalculate({ - zoom, - now: Number.MAX_VALUE, - defaultFadeDuration: 0, - zoomHistory: { - lastIntegerZoom: 0, - lastIntegerZoomTime: 0, - lastZoom: 0 - } - }); + layer.recalculate(parameters); } } diff --git a/src/style/evaluation_parameters.js b/src/style/evaluation_parameters.js new file mode 100644 index 00000000000..78c83e7f6ce --- /dev/null +++ b/src/style/evaluation_parameters.js @@ -0,0 +1,37 @@ +// @flow + +const ZoomHistory = require('./zoom_history'); + +class EvaluationParameters { + zoom: number; + now: number; + fadeDuration: number; + zoomHistory: ZoomHistory; + transition: TransitionSpecification; + + constructor(zoom: number, options?: *) { + this.zoom = zoom; + + if (options) { + this.now = options.now; + this.fadeDuration = options.fadeDuration; + this.zoomHistory = options.zoomHistory; + this.transition = options.transition; + } else { + this.now = 0; + this.fadeDuration = 0; + this.zoomHistory = new ZoomHistory(); + this.transition = {}; + } + } + + crossFadingFactor() { + if (this.fadeDuration === 0) { + return 1; + } else { + return Math.min((this.now - this.zoomHistory.lastIntegerZoomTime) / this.fadeDuration, 1); + } + } +} + +module.exports = EvaluationParameters; diff --git a/src/style/light.js b/src/style/light.js index 99de9939cbe..56d4ff50124 100644 --- a/src/style/light.js +++ b/src/style/light.js @@ -1,7 +1,5 @@ // @flow -import type {StylePropertySpecification} from "../style-spec/style-spec"; - const styleSpec = require('../style-spec/reference/latest'); const util = require('../util/util'); const Evented = require('../util/evented'); @@ -10,6 +8,9 @@ const {sphericalToCartesian} = require('../util/util'); const Color = require('../style-spec/util/color'); const interpolate = require('../style-spec/util/interpolate'); +import type {StylePropertySpecification} from '../style-spec/style-spec'; +import type EvaluationParameters from './evaluation_parameters'; + const { Properties, Transitionable, @@ -21,8 +22,7 @@ const { import type { Property, PropertyValue, - TransitionParameters, - EvaluationParameters + TransitionParameters } from './properties'; type LightPosition = { diff --git a/src/style/properties.js b/src/style/properties.js index 6863c8b9b1a..44efd4e1ed2 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -9,7 +9,7 @@ const {register} = require('../util/web_worker_transfer'); import type {StylePropertySpecification} from '../style-spec/style-spec'; import type {CrossFaded} from './cross_faded'; -import type {ZoomHistory} from './style'; +import type EvaluationParameters from './evaluation_parameters'; import type { Feature, @@ -21,12 +21,6 @@ import type { type TimePoint = number; -export type EvaluationParameters = GlobalProperties & { - now?: TimePoint, - fadeDuration?: number, - zoomHistory?: ZoomHistory -}; - /** * Implements a number of classes that define state and behavior for paint and layout properties, most * importantly their respective evaluation chains: @@ -610,11 +604,10 @@ class CrossFadedProperty implements Property> { } } - _calculate(min: T, mid: T, max: T, parameters: any): ?CrossFaded { + _calculate(min: T, mid: T, max: T, parameters: EvaluationParameters): ?CrossFaded { const z = parameters.zoom; const fraction = z - Math.floor(z); - const d = parameters.fadeDuration; - const t = d !== 0 ? Math.min((parameters.now - parameters.zoomHistory.lastIntegerZoomTime) / d, 1) : 1; + const t = parameters.crossFadingFactor(); return z > parameters.zoomHistory.lastIntegerZoom ? { from: min, to: mid, fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : { from: max, to: mid, fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; @@ -681,7 +674,6 @@ class Properties { } } -register(PossiblyEvaluatedPropertyValue); register(DataDrivenProperty); register(DataConstantProperty); register(CrossFadedProperty); diff --git a/src/style/style.js b/src/style/style.js index 88518f0cfdf..a46b2937134 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -25,6 +25,7 @@ const deref = require('../style-spec/deref'); const diff = require('../style-spec/diff'); const rtlTextPlugin = require('../source/rtl_text_plugin'); const Placement = require('./placement'); +const ZoomHistory = require('./zoom_history'); import type Map from '../ui/map'; import type Transform from '../geo/transform'; @@ -33,6 +34,7 @@ import type {StyleImage} from './style_image'; import type {StyleGlyph} from './style_glyph'; import type CollisionIndex from '../symbol/collision_index'; import type {Callback} from '../types/callback'; +import type EvaluationParameters from './evaluation_parameters'; const supportedDiffOperations = util.pick(diff.operations, [ 'addLayer', @@ -62,12 +64,6 @@ export type StyleOptions = { localIdeographFontFamily?: string }; -export type ZoomHistory = { - lastIntegerZoom: number, - lastIntegerZoomTime: number, - lastZoom: number -}; - /** * @private */ @@ -83,7 +79,7 @@ class Style extends Evented { _layers: {[string]: StyleLayer}; _order: Array; sourceCaches: {[string]: SourceCache}; - zoomHistory: ZoomHistory | {}; + zoomHistory: ZoomHistory; _loaded: boolean; _rtlTextPluginCallback: Function; _changed: boolean; @@ -109,7 +105,7 @@ class Style extends Evented { this._layers = {}; this._order = []; this.sourceCaches = {}; - this.zoomHistory = {}; + this.zoomHistory = new ZoomHistory(); this._loaded = false; this._resetUpdates(); @@ -271,32 +267,6 @@ class Style extends Evented { return ids.map((id) => this._layers[id].serialize()); } - _recalculate(z: number, fadeDuration: number) { - if (!this._loaded) return; - - for (const sourceId in this.sourceCaches) - this.sourceCaches[sourceId].used = false; - - const parameters = { - zoom: z, - now: browser.now(), - fadeDuration, - zoomHistory: this._updateZoomHistory(z) - }; - - for (const layerId of this._order) { - const layer = this._layers[layerId]; - - layer.recalculate(parameters); - if (!layer.isHidden(z) && layer.source) { - this.sourceCaches[layer.source].used = true; - } - } - - this.light.recalculate(parameters); - this.z = z; - } - hasTransitions() { if (this.light && this.light.hasTransition()) { return true; @@ -317,32 +287,6 @@ class Style extends Evented { return false; } - _updateZoomHistory(z: number) { - - const zh: ZoomHistory = (this.zoomHistory: any); - - if (zh.lastIntegerZoom === undefined) { - // first time - zh.lastIntegerZoom = Math.floor(z); - zh.lastIntegerZoomTime = 0; - zh.lastZoom = z; - } - - // check whether an integer zoom level as passed since the last frame - // and if yes, record it with the time. Used for transitioning patterns. - if (Math.floor(zh.lastZoom) < Math.floor(z)) { - zh.lastIntegerZoom = Math.floor(z); - zh.lastIntegerZoomTime = browser.now(); - - } else if (Math.floor(zh.lastZoom) > Math.floor(z)) { - zh.lastIntegerZoom = Math.floor(z + 1); - zh.lastIntegerZoomTime = browser.now(); - } - - zh.lastZoom = z; - return zh; - } - _checkLoaded() { if (!this._loaded) { throw new Error('Style is not done loading'); @@ -350,41 +294,56 @@ class Style extends Evented { } /** - * Apply queued style updates in a batch + * Apply queued style updates in a batch and recalculate zoom-dependent paint properties. */ - update() { - if (!this._changed || !this._loaded) return; + update(parameters: EvaluationParameters) { + if (!this._loaded) { + return; + } - const updatedIds = Object.keys(this._updatedLayers); - const removedIds = Object.keys(this._removedLayers); + if (this._changed) { + const updatedIds = Object.keys(this._updatedLayers); + const removedIds = Object.keys(this._removedLayers); - if (updatedIds.length || removedIds.length) { - this._updateWorkerLayers(updatedIds, removedIds); - } - for (const id in this._updatedSources) { - const action = this._updatedSources[id]; - assert(action === 'reload' || action === 'clear'); - if (action === 'reload') { - this._reloadSource(id); - } else if (action === 'clear') { - this._clearSource(id); + if (updatedIds.length || removedIds.length) { + this._updateWorkerLayers(updatedIds, removedIds); } - } + for (const id in this._updatedSources) { + const action = this._updatedSources[id]; + assert(action === 'reload' || action === 'clear'); + if (action === 'reload') { + this._reloadSource(id); + } else if (action === 'clear') { + this._clearSource(id); + } + } + + for (const id in this._updatedPaintProps) { + this._layers[id].updateTransitions(parameters); + } + + this.light.updateTransitions(parameters); - const parameters = { - now: browser.now(), - transition: util.extend({ duration: 300, delay: 0 }, this.stylesheet.transition) - }; + this._resetUpdates(); - for (const id in this._updatedPaintProps) { - this._layers[id].updateTransitions(parameters); + this.fire('data', {dataType: 'style'}); } - this.light.updateTransitions(parameters); + for (const sourceId in this.sourceCaches) { + this.sourceCaches[sourceId].used = false; + } - this._resetUpdates(); + for (const layerId of this._order) { + const layer = this._layers[layerId]; - this.fire('data', {dataType: 'style'}); + layer.recalculate(parameters); + if (!layer.isHidden(parameters.zoom) && layer.source) { + this.sourceCaches[layer.source].used = true; + } + } + + this.light.recalculate(parameters); + this.z = parameters.zoom; } _updateWorkerLayers(updatedIds: Array, removedIds: Array) { diff --git a/src/style/style_layer.js b/src/style/style_layer.js index d0b2b3bef8f..edb35dfdcd5 100644 --- a/src/style/style_layer.js +++ b/src/style/style_layer.js @@ -17,10 +17,8 @@ import type {Bucket} from '../data/bucket'; import type Point from '@mapbox/point-geometry'; import type RenderTexture from '../render/render_texture'; import type {FeatureFilter} from '../style-spec/feature_filter'; -import type { - TransitionParameters, - EvaluationParameters -} from './properties'; +import type {TransitionParameters} from './properties'; +import type EvaluationParameters from './evaluation_parameters'; const TRANSITION_SUFFIX = '-transition'; diff --git a/src/style/style_layer/fill_style_layer.js b/src/style/style_layer/fill_style_layer.js index d78ee259437..7ad4d89d637 100644 --- a/src/style/style_layer/fill_style_layer.js +++ b/src/style/style_layer/fill_style_layer.js @@ -15,7 +15,7 @@ const { import type {BucketParameters} from '../../data/bucket'; import type Point from '@mapbox/point-geometry'; import type {PaintProps} from './fill_style_layer_properties'; -import type {EvaluationParameters} from '../properties'; +import type EvaluationParameters from '../evaluation_parameters'; class FillStyleLayer extends StyleLayer { _transitionablePaint: Transitionable; diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index f51ca5660b9..d2e8c9f6fd8 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -18,7 +18,7 @@ const { import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './line_style_layer_properties'; -import type {EvaluationParameters} from '../properties'; +import type EvaluationParameters from '../evaluation_parameters'; const lineFloorwidthProperty = new DataDrivenProperty(properties.paint.properties['line-width'].specification, true); diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 27a2425c28a..b780628f42a 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -17,7 +17,7 @@ const { import type {BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './symbol_style_layer_properties'; import type {Feature} from '../../style-spec/expression'; -import type {EvaluationParameters} from '../properties'; +import type EvaluationParameters from '../evaluation_parameters'; class SymbolStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; diff --git a/src/style/zoom_history.js b/src/style/zoom_history.js new file mode 100644 index 00000000000..17599e0deae --- /dev/null +++ b/src/style/zoom_history.js @@ -0,0 +1,44 @@ +// @flow + +class ZoomHistory { + lastZoom: number; + lastFloorZoom: number; + lastIntegerZoom: number; + lastIntegerZoomTime: number; + first: boolean; + + constructor() { + this.first = true; + } + + update(z: number, now: number) { + const floorZ = Math.floor(z); + + if (this.first) { + this.first = false; + this.lastIntegerZoom = floorZ; + this.lastIntegerZoomTime = 0; + this.lastZoom = z; + this.lastFloorZoom = floorZ; + return true; + } + + if (this.lastFloorZoom > floorZ) { + this.lastIntegerZoom = floorZ + 1; + this.lastIntegerZoomTime = now; + } else if (this.lastFloorZoom < floorZ) { + this.lastIntegerZoom = floorZ; + this.lastIntegerZoomTime = now; + } + + if (z !== this.lastZoom) { + this.lastZoom = z; + this.lastFloorZoom = floorZ; + return true; + } + + return false; + } +} + +module.exports = ZoomHistory; diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index e9e832b42b1..00bc43e65f2 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -1,4 +1,5 @@ // @flow + const Anchor = require('./anchor'); const getAnchors = require('./get_anchors'); const clipLine = require('./clip_line'); @@ -12,6 +13,7 @@ const findPoleOfInaccessibility = require('../util/find_pole_of_inaccessibility' const classifyRings = require('../util/classify_rings'); const EXTENT = require('../data/extent'); const SymbolBucket = require('../data/bucket/symbol_bucket'); +const EvaluationParameters = require('../style/evaluation_parameters'); import type {Shaping, PositionedIcon} from './shaping'; import type CollisionBoxArray from './collision_box'; @@ -21,6 +23,7 @@ import type {StyleGlyph} from '../style/style_glyph'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {ImagePosition} from '../render/image_atlas'; import type {GlyphPosition} from '../render/glyph_atlas'; +import type {PossiblyEvaluatedPropertyValue} from '../style/properties'; const Point = require('@mapbox/point-geometry'); @@ -28,6 +31,28 @@ module.exports = { performSymbolLayout }; +// The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and +// `icon-size` at up to three: +// +// 1. `text-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `text-size` +// expressions, and to calculate the box dimensions for icon-text-fit. +// 2. `icon-size` at the zoom level of the bucket. Used to calculate a per-feature size for source `icon-size` +// expressions. +// 3. `text-size` and `icon-size` at the zoom level of the bucket, plus one. Used to calculate collision boxes. +// 4. `text-size` at zoom level 18. Used for something line-symbol-placement-related. +// 5. For composite `*-size` expressions: two zoom levels of curve stops that "cover" the zoom level of the +// bucket. These go into a vertex buffer and are used by the shader to interpolate the size at render time. +// +// (1) and (2) are stored in `bucket.layers[0].layout`. The remainder are below. +// +type Sizes = { + layoutTextSize: PossiblyEvaluatedPropertyValue, // (3) + layoutIconSize: PossiblyEvaluatedPropertyValue, // (3) + textMaxSize: PossiblyEvaluatedPropertyValue, // (4) + compositeTextSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue], // (5) + compositeIconSizes: [PossiblyEvaluatedPropertyValue, PossiblyEvaluatedPropertyValue], // (5) +}; + function performSymbolLayout(bucket: SymbolBucket, glyphMap: {[string]: {[number]: ?StyleGlyph}}, glyphPositions: {[string]: {[number]: GlyphPosition}}, @@ -43,6 +68,29 @@ function performSymbolLayout(bucket: SymbolBucket, bucket.iconsNeedLinear = false; const layout = bucket.layers[0].layout; + const unevaluatedLayoutValues = bucket.layers[0]._unevaluatedLayout._values; + + const sizes = {}; + + if (bucket.textSizeData.functionType === 'composite') { + const {min, max} = bucket.textSizeData.zoomRange; + sizes.compositeTextSizes = [ + unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(min)), + unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(max)) + ]; + } + + if (bucket.iconSizeData.functionType === 'composite') { + const {min, max} = bucket.iconSizeData.zoomRange; + sizes.compositeIconSizes = [ + unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(min)), + unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(max)) + ]; + } + + sizes.layoutTextSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1)); + sizes.layoutIconSize = unevaluatedLayoutValues['icon-size'].possiblyEvaluate(new EvaluationParameters(bucket.zoom + 1)); + sizes.textMaxSize = unevaluatedLayoutValues['text-size'].possiblyEvaluate(new EvaluationParameters(18)); const oneEm = 24; const lineHeight = layout.get('text-line-height') * oneEm; @@ -96,7 +144,7 @@ function performSymbolLayout(bucket: SymbolBucket, } if (shapedTextOrientations.horizontal || shapedIcon) { - addFeature(bucket, feature, shapedTextOrientations, shapedIcon, glyphPositionMap); + addFeature(bucket, feature, shapedTextOrientations, shapedIcon, glyphPositionMap, sizes); } } @@ -117,15 +165,16 @@ function addFeature(bucket: SymbolBucket, feature: SymbolFeature, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, - glyphPositionMap: {[number]: GlyphPosition}) { - const layoutTextSize = bucket.layoutTextSize.evaluate(feature); - const layoutIconSize = bucket.layoutIconSize.evaluate(feature); + glyphPositionMap: {[number]: GlyphPosition}, + sizes: Sizes) { + const layoutTextSize = sizes.layoutTextSize.evaluate(feature); + const layoutIconSize = sizes.layoutIconSize.evaluate(feature); // To reduce the number of labels that jump around when zooming we need // to use a text-size value that is the same for all zoom levels. // bucket calculates text-size at a high zoom level so that all tiles can // use the same value when calculating anchor positions. - let textMaxSize = bucket.textMaxSize.evaluate(feature); + let textMaxSize = sizes.textMaxSize.evaluate(feature); if (textMaxSize === undefined) { textMaxSize = layoutTextSize; } @@ -160,7 +209,7 @@ function addFeature(bucket: SymbolBucket, bucket.collisionBoxArray, feature.index, feature.sourceLayerIndex, bucket.index, textBoxScale, textPadding, textAlongLine, textOffset, iconBoxScale, iconPadding, iconAlongLine, iconOffset, - {zoom: bucket.zoom}, feature, glyphPositionMap)); + {zoom: bucket.zoom}, feature, glyphPositionMap, sizes)); }; if (symbolPlacement === 'line') { @@ -214,7 +263,8 @@ function addTextVertices(bucket: SymbolBucket, lineArray: any, writingMode: number, placedTextSymbolIndices: Array, - glyphPositionMap: {[number]: GlyphPosition}) { + glyphPositionMap: {[number]: GlyphPosition}, + sizes: Sizes) { const glyphQuads = getGlyphQuads(anchor, shapedText, layer, textAlongLine, globalProperties, feature, glyphPositionMap); @@ -227,8 +277,8 @@ function addTextVertices(bucket: SymbolBucket, ]; } else if (sizeData.functionType === 'composite') { textSizeData = [ - 10 * bucket.compositeTextSizes[0].evaluate(feature), - 10 * bucket.compositeTextSizes[1].evaluate(feature) + 10 * sizes.compositeTextSizes[0].evaluate(feature), + 10 * sizes.compositeTextSizes[1].evaluate(feature) ]; } @@ -259,26 +309,27 @@ function addTextVertices(bucket: SymbolBucket, * @private */ function addSymbol(bucket: SymbolBucket, - anchor: Anchor, - line: Array, - shapedTextOrientations: any, - shapedIcon: PositionedIcon | void, - layer: SymbolStyleLayer, - collisionBoxArray: CollisionBoxArray, - featureIndex: number, - sourceLayerIndex: number, - bucketIndex: number, - textBoxScale: number, - textPadding: number, - textAlongLine: boolean, - textOffset: [number, number], - iconBoxScale: number, - iconPadding: number, - iconAlongLine: boolean, - iconOffset: [number, number], - globalProperties: Object, - feature: SymbolFeature, - glyphPositionMap: {[number]: GlyphPosition}) { + anchor: Anchor, + line: Array, + shapedTextOrientations: any, + shapedIcon: PositionedIcon | void, + layer: SymbolStyleLayer, + collisionBoxArray: CollisionBoxArray, + featureIndex: number, + sourceLayerIndex: number, + bucketIndex: number, + textBoxScale: number, + textPadding: number, + textAlongLine: boolean, + textOffset: [number, number], + iconBoxScale: number, + iconPadding: number, + iconAlongLine: boolean, + iconOffset: [number, number], + globalProperties: Object, + feature: SymbolFeature, + glyphPositionMap: {[number]: GlyphPosition}, + sizes: Sizes) { const lineArray = bucket.addToLineVertexArray(anchor, line); let textCollisionFeature, iconCollisionFeature; @@ -292,10 +343,10 @@ function addSymbol(bucket: SymbolBucket, // As a collision approximation, we can use either the vertical or the horizontal version of the feature // We're counting on the two versions having similar dimensions textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedTextOrientations.horizontal, textBoxScale, textPadding, textAlongLine, bucket.overscaling); - numGlyphVertices += addTextVertices(bucket, anchor, shapedTextOrientations.horizontal, layer, textAlongLine, globalProperties, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, placedTextSymbolIndices, glyphPositionMap); + numGlyphVertices += addTextVertices(bucket, anchor, shapedTextOrientations.horizontal, layer, textAlongLine, globalProperties, feature, textOffset, lineArray, shapedTextOrientations.vertical ? WritingMode.horizontal : WritingMode.horizontalOnly, placedTextSymbolIndices, glyphPositionMap, sizes); if (shapedTextOrientations.vertical) { - numVerticalGlyphVertices += addTextVertices(bucket, anchor, shapedTextOrientations.vertical, layer, textAlongLine, globalProperties, feature, textOffset, lineArray, WritingMode.vertical, placedTextSymbolIndices, glyphPositionMap); + numVerticalGlyphVertices += addTextVertices(bucket, anchor, shapedTextOrientations.vertical, layer, textAlongLine, globalProperties, feature, textOffset, lineArray, WritingMode.vertical, placedTextSymbolIndices, glyphPositionMap, sizes); } } @@ -319,8 +370,8 @@ function addSymbol(bucket: SymbolBucket, ]; } else if (sizeData.functionType === 'composite') { iconSizeData = [ - 10 * bucket.compositeIconSizes[0].evaluate(feature), - 10 * bucket.compositeIconSizes[1].evaluate(feature) + 10 * sizes.compositeIconSizes[0].evaluate(feature), + 10 * sizes.compositeIconSizes[1].evaluate(feature) ]; } diff --git a/src/ui/map.js b/src/ui/map.js index a5214fde9ac..db970383bc5 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -8,6 +8,7 @@ const DOM = require('../util/dom'); const ajax = require('../util/ajax'); const Style = require('../style/style'); +const EvaluationParameters = require('../style/evaluation_parameters'); const Painter = require('../render/painter'); const Transform = require('../geo/transform'); @@ -242,6 +243,7 @@ class Map extends Camera { _hash: Hash; _delegatedListeners: any; _fadeDuration: number; + _crossFadingFactor: number; scrollZoom: ScrollZoomHandler; boxZoom: BoxZoomHandler; @@ -269,6 +271,7 @@ class Map extends Camera { this._bearingSnap = options.bearingSnap; this._refreshExpiredTiles = options.refreshExpiredTiles; this._fadeDuration = options.fadeDuration; + this._crossFadingFactor = 1; const transformRequestFn = options.transformRequest; this._transformRequest = transformRequestFn ? (url, type) => transformRequestFn(url, type) || ({ url }) : (url) => ({ url }); @@ -1419,7 +1422,7 @@ class Map extends Camera { * @returns {boolean} A Boolean indicating whether the map is fully loaded. */ loaded() { - if (this._styleDirty || this._sourcesDirty || this._placementDirty) + if (this._styleDirty || this._sourcesDirty) return false; if (!this.style || !this.style.loaded()) return false; @@ -1458,14 +1461,32 @@ class Map extends Camera { this._updateEase(); } - // If the style has changed, the map is being zoomed, or a transition - // is in progress: + let crossFading = false; + + // If the style has changed, the map is being zoomed, or a transition or fade is in progress: // - Apply style changes (in a batch) - // - Recalculate zoom-dependent paint properties. + // - Recalculate paint properties. if (this.style && this._styleDirty) { this._styleDirty = false; - this.style.update(); - this.style._recalculate(this.transform.zoom, this._fadeDuration); + + const zoom = this.transform.zoom; + const now = browser.now(); + this.style.zoomHistory.update(zoom, now); + + const parameters = new EvaluationParameters(zoom, { + now, + fadeDuration: this._fadeDuration, + zoomHistory: this.style.zoomHistory, + transition: util.extend({ duration: 300, delay: 0 }, this.style.stylesheet.transition) + }); + + const factor = parameters.crossFadingFactor(); + if (factor !== 1 || factor !== this._crossFadingFactor) { + crossFading = true; + this._crossFadingFactor = factor; + } + + this.style.update(parameters); } // If we are in _render for any reason other than an in-progress paint @@ -1494,7 +1515,7 @@ class Map extends Camera { this.fire('load'); } - if (this.style && this.style.hasTransitions()) { + if (this.style && (this.style.hasTransitions() || crossFading)) { this._styleDirty = true; } diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5740/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#5740/expected.png new file mode 100644 index 00000000000..c86291d37b3 Binary files /dev/null and b/test/integration/render-tests/regressions/mapbox-gl-js#5740/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5740/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#5740/style.json new file mode 100644 index 00000000000..2ab02ada1c6 --- /dev/null +++ b/test/integration/render-tests/regressions/mapbox-gl-js#5740/style.json @@ -0,0 +1,72 @@ +{ + "version": 8, + "metadata": { + "test": { + "fadeDuration": 1000, + "width": 64, + "height": 64, + "description": "Tests that fill-pattern cross-fading completes, by checking the rendering after the fade duration has elapsed.", + "operations": [ + [ + "wait" + ], + [ + "setZoom", + 1.1 + ], + [ + "wait", + 0 + ], + [ + "wait", + 1000 + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Polygon", + "coordinates": [ + [ + [ + -10, + -10 + ], + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + 10, + -10 + ], + [ + -10, + -10 + ] + ] + ] + } + } + }, + "sprite": "local://sprites/emerald", + "layers": [ + { + "id": "fill", + "type": "fill", + "source": "geojson", + "paint": { + "fill-antialias": false, + "fill-pattern": "generic_icon" + } + } + ] +} diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 1ca5a62dbe5..91e52088fa0 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -38,7 +38,7 @@ module.exports = function(style, options, _callback) { preserveDrawingBuffer: true, axonometric: options.axonometric || false, skew: options.skew || [0, 0], - fadeDuration: 0 + fadeDuration: options.fadeDuration || 0 }); // Configure the map to never stop the render loop diff --git a/test/unit/style/style.test.js b/test/unit/style/style.test.js index a8daf1d3896..6ff76232a3b 100644 --- a/test/unit/style/style.test.js +++ b/test/unit/style/style.test.js @@ -329,7 +329,7 @@ test('Style#loadJSON', (t) => { 'source': '-source-id-', 'source-layer': '-source-layer-' }); - style.update(); + style.update({}); }); style.on('error', (event) => { @@ -431,7 +431,7 @@ test('Style#update', (t) => { t.end(); }; - style.update(); + style.update({}); }); }); @@ -581,7 +581,7 @@ test('Style#addSource', (t) => { style.once('data', t.end); style.on('style.load', () => { style.addSource('source-id', source); - style.update(); + style.update({}); }); }); @@ -664,7 +664,7 @@ test('Style#removeSource', (t) => { style.on('style.load', () => { style.addSource('source-id', source); style.removeSource('source-id'); - style.update(); + style.update({}); }); }); @@ -708,8 +708,7 @@ test('Style#removeSource', (t) => { }] })); style.on('style.load', () => { - style.update(); - style._recalculate(1); + style.update(1, 0); callback(style); }); return style; @@ -901,7 +900,7 @@ test('Style#addLayer', (t) => { if (e.dataType === 'source' && e.sourceDataType === 'content') { style.sourceCaches['mapbox'].reload = t.end; style.addLayer(layer); - style.update(); + style.update({}); } }); }); @@ -937,7 +936,7 @@ test('Style#addLayer', (t) => { style.sourceCaches['mapbox'].clearTiles = t.fail; style.removeLayer('my-layer'); style.addLayer(layer); - style.update(); + style.update({}); } }); @@ -973,7 +972,7 @@ test('Style#addLayer', (t) => { style.sourceCaches['mapbox'].clearTiles = t.end; style.removeLayer('my-layer'); style.addLayer(layer); - style.update(); + style.update({}); } }); @@ -988,7 +987,7 @@ test('Style#addLayer', (t) => { style.on('style.load', () => { style.addLayer(layer); - style.update(); + style.update({}); }); }); @@ -1120,7 +1119,7 @@ test('Style#removeLayer', (t) => { style.on('style.load', () => { style.addLayer(layer); style.removeLayer('background'); - style.update(); + style.update({}); }); }); @@ -1221,7 +1220,7 @@ test('Style#moveLayer', (t) => { style.on('style.load', () => { style.addLayer(layer); style.moveLayer('background'); - style.update(); + style.update({}); }); }); @@ -1281,8 +1280,7 @@ test('Style#setPaintProperty', (t) => { tr.resize(512, 512); style.once('style.load', () => { - style.update(); - style._recalculate(tr.zoom); + style.update(tr.zoom, 0); const sourceCache = style.sourceCaches['geojson']; const source = style.getSource('geojson'); @@ -1308,7 +1306,7 @@ test('Style#setPaintProperty', (t) => { // after the next Style#update() setTimeout(() => { styleUpdateCalled = true; - style.update(); + style.update({}); }, 50); } })); @@ -1352,7 +1350,7 @@ test('Style#setFilter', (t) => { style.setFilter('symbol', ['==', 'id', 1]); t.deepEqual(style.getFilter('symbol'), ['==', 'id', 1]); - style.update(); // trigger dispatcher broadcast + style.update({}); // trigger dispatcher broadcast }); }); @@ -1379,7 +1377,7 @@ test('Style#setFilter', (t) => { style.on('style.load', () => { const filter = ['==', 'id', 1]; style.setFilter('symbol', filter); - style.update(); // flush pending operations + style.update({}); // flush pending operations style.dispatcher.broadcast = function(key, value) { t.equal(key, 'updateLayers'); @@ -1389,7 +1387,7 @@ test('Style#setFilter', (t) => { }; filter[2] = 2; style.setFilter('symbol', filter); - style.update(); // trigger dispatcher broadcast + style.update({}); // trigger dispatcher broadcast }); }); @@ -1577,8 +1575,7 @@ test('Style#queryRenderedFeatures', (t) => { }); style.on('style.load', () => { - style.update(); - style._recalculate(0); + style.update(0, 0); t.test('returns feature type', (t) => { const results = style.queryRenderedFeatures([{column: 1, row: 1, zoom: 1}], {}, 0, 0); @@ -1661,7 +1658,7 @@ test('Style defers expensive methods', (t) => { })); style.on('style.load', () => { - style.update(); + style.update({}); // spies to track defered methods t.spy(style, 'fire'); @@ -1679,7 +1676,7 @@ test('Style defers expensive methods', (t) => { t.notOk(style._reloadSource.called, '_reloadSource is deferred'); t.notOk(style._updateWorkerLayers.called, '_updateWorkerLayers is deferred'); - style.update(); + style.update({}); t.ok(style.fire.calledWith('data'), 'a data event was fired'); @@ -1814,7 +1811,7 @@ test('Style#hasTransitions', (t) => { style.on('style.load', () => { style.setPaintProperty("background", "background-color", "blue"); - style.update(); + style.update({transition: { duration: 300, delay: 0 }}); t.equal(style.hasTransitions(), true); t.end(); }); @@ -1825,9 +1822,6 @@ test('Style#hasTransitions', (t) => { style.loadJSON({ "version": 8, "sources": {}, - "transition": { - "duration": 0 - }, "layers": [{ "id": "background", "type": "background" @@ -1836,7 +1830,7 @@ test('Style#hasTransitions', (t) => { style.on('style.load', () => { style.setPaintProperty("background", "background-color", "blue"); - style.update(); + style.update({transition: { duration: 0, delay: 0 }}); t.equal(style.hasTransitions(), false); t.end(); }); diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index fd41dca2d5e..44999e5b09a 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -919,7 +919,7 @@ test('Map', (t) => { }; map.setLayoutProperty('symbol', 'text-transform', 'lowercase'); - map.style.update(); + map.style.update({}); t.deepEqual(map.getLayoutProperty('symbol', 'text-transform'), 'lowercase'); t.end(); });