diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js
index 8238f98d5cb..6a3e8cf86ea 100644
--- a/build/generate-struct-arrays.js
+++ b/build/generate-struct-arrays.js
@@ -128,6 +128,7 @@ import fillAttributes from '../src/data/bucket/fill_attributes.js';
import lineAttributes from '../src/data/bucket/line_attributes.js';
import lineAttributesExt from '../src/data/bucket/line_attributes_ext.js';
import patternAttributes from '../src/data/bucket/pattern_attributes.js';
+import dashAttributes from '../src/data/bucket/dash_attributes.js';
import skyboxAttributes from '../src/render/skybox_attributes.js';
import {fillExtrusionAttributes, centroidAttributes} from '../src/data/bucket/fill_extrusion_attributes.js';
@@ -139,7 +140,8 @@ const layoutAttributes = {
heatmap: circleAttributes,
line: lineAttributes,
lineExt: lineAttributesExt,
- pattern: patternAttributes
+ pattern: patternAttributes,
+ dash: dashAttributes
};
for (const name in layoutAttributes) {
createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]);
diff --git a/debug/dasharray.html b/debug/dasharray.html
new file mode 100644
index 00000000000..be1b7d2590d
--- /dev/null
+++ b/debug/dasharray.html
@@ -0,0 +1,72 @@
+
+
+
+ Mapbox GL JS debug page
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/array_types.js b/src/data/array_types.js
index 220dae362c5..3e673069f71 100644
--- a/src/data/array_types.js
+++ b/src/data/array_types.js
@@ -188,6 +188,44 @@ class StructArrayLayout10ui20 extends StructArray {
StructArrayLayout10ui20.prototype.bytesPerElement = 20;
register('StructArrayLayout10ui20', StructArrayLayout10ui20);
+/**
+ * Implementation of the StructArray layout:
+ * [0]: Uint16[8]
+ *
+ * @private
+ */
+class StructArrayLayout8ui16 extends StructArray {
+ uint8: Uint8Array;
+ uint16: Uint16Array;
+
+ _refreshViews() {
+ this.uint8 = new Uint8Array(this.arrayBuffer);
+ this.uint16 = new Uint16Array(this.arrayBuffer);
+ }
+
+ emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
+ const i = this.length;
+ this.resize(i + 1);
+ return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7);
+ }
+
+ emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
+ const o2 = i * 8;
+ this.uint16[o2 + 0] = v0;
+ this.uint16[o2 + 1] = v1;
+ this.uint16[o2 + 2] = v2;
+ this.uint16[o2 + 3] = v3;
+ this.uint16[o2 + 4] = v4;
+ this.uint16[o2 + 5] = v5;
+ this.uint16[o2 + 6] = v6;
+ this.uint16[o2 + 7] = v7;
+ return i;
+ }
+}
+
+StructArrayLayout8ui16.prototype.bytesPerElement = 16;
+register('StructArrayLayout8ui16', StructArrayLayout8ui16);
+
/**
* Implementation of the StructArray layout:
* [0]: Int16[4]
@@ -1104,6 +1142,7 @@ export {
StructArrayLayout2i4ub1f12,
StructArrayLayout2f8,
StructArrayLayout10ui20,
+ StructArrayLayout8ui16,
StructArrayLayout4i4ui4i24,
StructArrayLayout3f12,
StructArrayLayout1ul4,
@@ -1129,6 +1168,7 @@ export {
StructArrayLayout2i4ub1f12 as LineLayoutArray,
StructArrayLayout2f8 as LineExtLayoutArray,
StructArrayLayout10ui20 as PatternLayoutArray,
+ StructArrayLayout8ui16 as DashLayoutArray,
StructArrayLayout4i4ui4i24 as SymbolLayoutArray,
StructArrayLayout3f12 as SymbolDynamicLayoutArray,
StructArrayLayout1ul4 as SymbolOpacityArray,
diff --git a/src/data/bucket.js b/src/data/bucket.js
index 332f635da8d..b2c5c1a47d0 100644
--- a/src/data/bucket.js
+++ b/src/data/bucket.js
@@ -7,6 +7,7 @@ import type FeatureIndex from './feature_index.js';
import type Context from '../gl/context.js';
import type {FeatureStates} from '../source/source_state.js';
import type {ImagePosition} from '../render/image_atlas.js';
+import type LineAtlas from '../render/line_atlas.js';
import type {CanonicalTileID} from '../source/tile_id.js';
export type BucketParameters = {
@@ -26,7 +27,8 @@ export type PopulateParameters = {
iconDependencies: {},
patternDependencies: {},
glyphDependencies: {},
- availableImages: Array
+ availableImages: Array,
+ lineAtlas: LineAtlas
}
export type IndexedFeature = {
diff --git a/src/data/bucket/dash_attributes.js b/src/data/bucket/dash_attributes.js
new file mode 100644
index 00000000000..1b0520cf47a
--- /dev/null
+++ b/src/data/bucket/dash_attributes.js
@@ -0,0 +1,9 @@
+// @flow
+import {createLayout} from '../../util/struct_array.js';
+
+const dashAttributes = createLayout([
+ {name: 'a_dash_to', components: 4, type: 'Uint16'}, // [x, y, width, unused]
+ {name: 'a_dash_from', components: 4, type: 'Uint16'}
+]);
+
+export default dashAttributes;
diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js
index 5389cbe1dd5..0c620c747dc 100644
--- a/src/data/bucket/line_bucket.js
+++ b/src/data/bucket/line_bucket.js
@@ -34,6 +34,7 @@ import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type {FeatureStates} from '../../source/source_state.js';
import type {ImagePosition} from '../../render/image_atlas.js';
+import type LineAtlas from '../../render/line_atlas.js';
// NOTE ON EXTRUDE SCALE:
// scale the extrusion vector so that the normal length is this value.
@@ -169,21 +170,100 @@ class LineBucket implements Bucket {
});
}
+ const {lineAtlas, featureIndex} = options;
+ const hasFeatureDashes = this.addConstantDashes(lineAtlas);
+
for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature;
+ if (hasFeatureDashes) {
+ this.addFeatureDashes(bucketFeature, lineAtlas);
+ }
+
if (this.hasPattern) {
const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, this.zoom, options);
// pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(patternBucketFeature);
+
} else {
- this.addFeature(bucketFeature, geometry, index, canonical, {});
+ this.addFeature(bucketFeature, geometry, index, canonical, lineAtlas.positions);
}
const feature = features[index].feature;
- options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
+ featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
+ }
+ }
+
+ addConstantDashes(lineAtlas: LineAtlas) {
+ let hasFeatureDashes = false;
+
+ for (const layer of this.layers) {
+ const dashPropertyValue = layer.paint.get('line-dasharray').value;
+ const capPropertyValue = layer.layout.get('line-cap').value;
+
+ if (dashPropertyValue.kind !== 'constant' || capPropertyValue.kind !== 'constant') {
+ hasFeatureDashes = true;
+
+ } else {
+ const round = capPropertyValue.value === 'round';
+ const constDash = dashPropertyValue.value;
+ if (!constDash) continue;
+ lineAtlas.addDash(constDash.from, round);
+ lineAtlas.addDash(constDash.to, round);
+ if (constDash.other) lineAtlas.addDash(constDash.other, round);
+ }
+ }
+
+ return hasFeatureDashes;
+ }
+
+ addFeatureDashes(feature: BucketFeature, lineAtlas: LineAtlas) {
+
+ const zoom = this.zoom;
+
+ for (const layer of this.layers) {
+ const dashPropertyValue = layer.paint.get('line-dasharray').value;
+ const capPropertyValue = layer.layout.get('line-cap').value;
+
+ if (dashPropertyValue.kind === 'constant' && capPropertyValue.kind === 'constant') continue;
+
+ let minDashArray, midDashArray, maxDashArray, minRound, midRound, maxRound;
+
+ if (dashPropertyValue.kind === 'constant') {
+ const constDash = dashPropertyValue.value;
+ if (!constDash) continue;
+ minDashArray = constDash.other || constDash.to;
+ midDashArray = constDash.to;
+ maxDashArray = constDash.from;
+
+ } else {
+ minDashArray = dashPropertyValue.evaluate({zoom: zoom - 1}, feature);
+ midDashArray = dashPropertyValue.evaluate({zoom}, feature);
+ maxDashArray = dashPropertyValue.evaluate({zoom: zoom + 1}, feature);
+ }
+
+ if (capPropertyValue.kind === 'constant') {
+ minRound = midRound = maxRound = capPropertyValue.value === 'round';
+
+ } else {
+ minRound = capPropertyValue.evaluate({zoom: zoom - 1}, feature) === 'round';
+ midRound = capPropertyValue.evaluate({zoom}, feature) === 'round';
+ maxRound = capPropertyValue.evaluate({zoom: zoom + 1}, feature) === 'round';
+ }
+
+ lineAtlas.addDash(minDashArray, minRound);
+ lineAtlas.addDash(midDashArray, midRound);
+ lineAtlas.addDash(maxDashArray, maxRound);
+
+ const min = lineAtlas.getKey(minDashArray, minRound);
+ const mid = lineAtlas.getKey(midDashArray, midRound);
+ const max = lineAtlas.getKey(maxDashArray, maxRound);
+
+ // save positions for paint array
+ feature.patterns[layer.id] = {min, mid, max};
}
+
}
update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) {
@@ -236,7 +316,7 @@ class LineBucket implements Bucket {
addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
const layout = this.layers[0].layout;
const join = layout.get('line-join').evaluate(feature, {});
- const cap = layout.get('line-cap');
+ const cap = layout.get('line-cap').evaluate(feature, {});
const miterLimit = layout.get('line-miter-limit');
const roundLimit = layout.get('line-round-limit');
this.lineClips = this.lineFeatureClips(feature);
diff --git a/src/data/program_configuration.js b/src/data/program_configuration.js
index 30638af16d1..dc8c0602ee2 100644
--- a/src/data/program_configuration.js
+++ b/src/data/program_configuration.js
@@ -5,9 +5,10 @@ import Color from '../style-spec/util/color.js';
import {supportsPropertyExpression} from '../style-spec/util/properties.js';
import {register} from '../util/web_worker_transfer.js';
import {PossiblyEvaluatedPropertyValue} from '../style/properties.js';
-import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray} from './array_types.js';
+import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray, DashLayoutArray} from './array_types.js';
import {clamp} from '../util/util.js';
import patternAttributes from './bucket/pattern_attributes.js';
+import dashAttributes from './bucket/dash_attributes.js';
import EvaluationParameters from '../style/evaluation_parameters.js';
import FeaturePositionMap from './feature_position_map.js';
import {
@@ -131,21 +132,21 @@ class CrossFadedConstantBinder implements UniformBinder {
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) {
this.pixelRatioFrom = posFrom.pixelRatio;
this.pixelRatioTo = posTo.pixelRatio;
- this.patternFrom = posFrom.tlbr;
- this.patternTo = posTo.tlbr;
+ this.patternFrom = posFrom.tl.concat(posFrom.br);
+ this.patternTo = posTo.tl.concat(posTo.br);
}
setUniform(uniform: Uniform<*>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue, uniformName: string) {
const pos =
- uniformName === 'u_pattern_to' ? this.patternTo :
- uniformName === 'u_pattern_from' ? this.patternFrom :
+ uniformName === 'u_pattern_to' || uniformName === 'u_dash_to' ? this.patternTo :
+ uniformName === 'u_pattern_from' || uniformName === 'u_dash_from' ? this.patternFrom :
uniformName === 'u_pixel_ratio_to' ? this.pixelRatioTo :
uniformName === 'u_pixel_ratio_from' ? this.pixelRatioFrom : null;
if (pos) uniform.set(pos);
}
getBinding(context: Context, location: WebGLUniformLocation, name: string): $Shape> {
- return name.substr(0, 9) === 'u_pattern' ?
+ return name === 'u_pattern_from' || name === 'u_pattern_to' || name === 'u_dash_from' || name === 'u_dash_to' ?
new Uniform4f(context, location) :
new Uniform1f(context, location);
}
@@ -320,8 +321,9 @@ class CrossFadedCompositeBinder implements AttributeBinder {
this.zoom = zoom;
this.layerId = layerId;
+ this.paintVertexAttributes = (type === 'array' ? dashAttributes : patternAttributes).members;
for (let i = 0; i < names.length; ++i) {
- assert(`a_${names[i]}` === patternAttributes.members[i].name);
+ assert(`a_${names[i]}` === this.paintVertexAttributes[i].name);
}
this.zoomInPaintVertexArray = new PaintVertexArray();
@@ -352,25 +354,23 @@ class CrossFadedCompositeBinder implements AttributeBinder {
// we're cross-fading to at layout time. In order to keep vertex attributes to a minimum and not pass
// unnecessary vertex data to the shaders, we determine which to upload at draw time.
for (let i = start; i < end; i++) {
- this.zoomInPaintVertexArray.emplace(i,
- imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1],
- imageMin.tl[0], imageMin.tl[1], imageMin.br[0], imageMin.br[1],
- imageMid.pixelRatio,
- imageMin.pixelRatio,
- );
- this.zoomOutPaintVertexArray.emplace(i,
- imageMid.tl[0], imageMid.tl[1], imageMid.br[0], imageMid.br[1],
- imageMax.tl[0], imageMax.tl[1], imageMax.br[0], imageMax.br[1],
- imageMid.pixelRatio,
- imageMax.pixelRatio,
- );
+ this._setPaintValue(this.zoomInPaintVertexArray, i, imageMid, imageMin);
+ this._setPaintValue(this.zoomOutPaintVertexArray, i, imageMid, imageMax);
}
}
+ _setPaintValue(array, i, posA, posB) {
+ array.emplace(i,
+ posA.tl[0], posA.tl[1], posA.br[0], posA.br[1],
+ posB.tl[0], posB.tl[1], posB.br[0], posB.br[1],
+ posA.pixelRatio, posB.pixelRatio
+ );
+ }
+
upload(context: Context) {
if (this.zoomInPaintVertexArray && this.zoomInPaintVertexArray.arrayBuffer && this.zoomOutPaintVertexArray && this.zoomOutPaintVertexArray.arrayBuffer) {
- this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, patternAttributes.members, this.expression.isStateDependent);
- this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, patternAttributes.members, this.expression.isStateDependent);
+ this.zoomInPaintVertexBuffer = context.createVertexBuffer(this.zoomInPaintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent);
+ this.zoomOutPaintVertexBuffer = context.createVertexBuffer(this.zoomOutPaintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent);
}
}
@@ -425,13 +425,15 @@ export default class ProgramConfiguration {
const propType = value.property.specification['property-type'];
const isCrossFaded = propType === 'cross-faded' || propType === 'cross-faded-data-driven';
- if (expression.kind === 'constant') {
+ const sourceException = String(property) === 'line-dasharray' && (layer.layout: any).get('line-cap').value.kind !== 'constant';
+
+ if (expression.kind === 'constant' && !sourceException) {
this.binders[property] = isCrossFaded ?
new CrossFadedConstantBinder(expression.value, names) :
new ConstantBinder(expression.value, names, type);
keys.push(`/u_${property}`);
- } else if (expression.kind === 'source' || isCrossFaded) {
+ } else if (expression.kind === 'source' || sourceException || isCrossFaded) {
const StructArrayLayout = layoutType(property, type, 'source');
this.binders[property] = isCrossFaded ?
new CrossFadedCompositeBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout, layer.id) :
@@ -507,14 +509,10 @@ export default class ProgramConfiguration {
const result = [];
for (const property in this.binders) {
const binder = this.binders[property];
- if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder) {
+ if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof CrossFadedCompositeBinder) {
for (let i = 0; i < binder.paintVertexAttributes.length; i++) {
result.push(binder.paintVertexAttributes[i].name);
}
- } else if (binder instanceof CrossFadedCompositeBinder) {
- for (let i = 0; i < patternAttributes.members.length; i++) {
- result.push(patternAttributes.members[i].name);
- }
}
}
return result;
@@ -648,59 +646,60 @@ export class ProgramConfigurationSet {
}
}
-function paintAttributeNames(property, type) {
- const attributeNameExceptions = {
- 'text-opacity': ['opacity'],
- 'icon-opacity': ['opacity'],
- 'text-color': ['fill_color'],
- 'icon-color': ['fill_color'],
- 'text-halo-color': ['halo_color'],
- 'icon-halo-color': ['halo_color'],
- 'text-halo-blur': ['halo_blur'],
- 'icon-halo-blur': ['halo_blur'],
- 'text-halo-width': ['halo_width'],
- 'icon-halo-width': ['halo_width'],
- 'line-gap-width': ['gapwidth'],
- 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
- 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
- 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
- };
+const attributeNameExceptions = {
+ 'text-opacity': ['opacity'],
+ 'icon-opacity': ['opacity'],
+ 'text-color': ['fill_color'],
+ 'icon-color': ['fill_color'],
+ 'text-halo-color': ['halo_color'],
+ 'icon-halo-color': ['halo_color'],
+ 'text-halo-blur': ['halo_blur'],
+ 'icon-halo-blur': ['halo_blur'],
+ 'text-halo-width': ['halo_width'],
+ 'icon-halo-width': ['halo_width'],
+ 'line-gap-width': ['gapwidth'],
+ 'line-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
+ 'fill-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
+ 'fill-extrusion-pattern': ['pattern_to', 'pattern_from', 'pixel_ratio_to', 'pixel_ratio_from'],
+ 'line-dasharray': ['dash_to', 'dash_from']
+};
+function paintAttributeNames(property, type) {
return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')];
}
-function getLayoutException(property) {
- const propertyExceptions = {
- 'line-pattern':{
- 'source': PatternLayoutArray,
- 'composite': PatternLayoutArray
- },
- 'fill-pattern': {
- 'source': PatternLayoutArray,
- 'composite': PatternLayoutArray
- },
- 'fill-extrusion-pattern':{
- 'source': PatternLayoutArray,
- 'composite': PatternLayoutArray
- }
- };
+const propertyExceptions = {
+ 'line-pattern': {
+ 'source': PatternLayoutArray,
+ 'composite': PatternLayoutArray
+ },
+ 'fill-pattern': {
+ 'source': PatternLayoutArray,
+ 'composite': PatternLayoutArray
+ },
+ 'fill-extrusion-pattern':{
+ 'source': PatternLayoutArray,
+ 'composite': PatternLayoutArray
+ },
+ 'line-dasharray': { // temporary layout
+ 'source': DashLayoutArray,
+ 'composite': DashLayoutArray
+ }
+};
- return propertyExceptions[property];
-}
+const defaultLayouts = {
+ 'color': {
+ 'source': StructArrayLayout2f8,
+ 'composite': StructArrayLayout4f16
+ },
+ 'number': {
+ 'source': StructArrayLayout1f4,
+ 'composite': StructArrayLayout2f8
+ }
+};
function layoutType(property, type, binderType) {
- const defaultLayouts = {
- 'color': {
- 'source': StructArrayLayout2f8,
- 'composite': StructArrayLayout4f16
- },
- 'number': {
- 'source': StructArrayLayout1f4,
- 'composite': StructArrayLayout2f8
- }
- };
-
- const layoutException = getLayoutException(property);
+ const layoutException = propertyExceptions[property];
return layoutException && layoutException[binderType] || defaultLayouts[type][binderType];
}
diff --git a/src/render/draw_line.js b/src/render/draw_line.js
index 9a605ae4417..a8a95a52256 100644
--- a/src/render/draw_line.js
+++ b/src/render/draw_line.js
@@ -29,7 +29,9 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly);
const colorMode = painter.colorModeForRenderPass();
- const dasharray = layer.paint.get('line-dasharray');
+ const dasharrayProperty = layer.paint.get('line-dasharray');
+ const dasharray = dasharrayProperty.constantOr((1: any));
+ const capProperty = layer.layout.get('line-cap');
const patternProperty = layer.paint.get('line-pattern');
const image = patternProperty.constantOr((1: any));
@@ -44,8 +46,6 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
const context = painter.context;
const gl = context.gl;
- let firstTile = true;
-
for (const coord of coords) {
const tile = sourceCache.getTile(coord);
if (image && !tile.patternsLoaded()) continue;
@@ -55,9 +55,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
painter.prepareDrawTile(coord);
const programConfiguration = bucket.programConfigurations.get(layer.id);
- const prevProgram = painter.context.program.get();
const program = painter.useProgram(programId, programConfiguration);
- const programChanged = firstTile || program.program !== prevProgram;
const constantPattern = patternProperty.constantOr(null);
if (constantPattern && tile.imageAtlas) {
@@ -67,9 +65,20 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
}
+ const constantDash = dasharrayProperty.constantOr(null);
+ const constantCap = capProperty.constantOr((null: any));
+
+ if (!image && constantDash && constantCap && tile.lineAtlas) {
+ const atlas = tile.lineAtlas;
+ const round = constantCap === 'round';
+ const posTo = atlas.getDash(constantDash.to, round);
+ const posFrom = atlas.getDash(constantDash.from, round);
+ if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
+ }
+
const matrix = painter.terrain ? coord.posMatrix : null;
const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade, matrix) :
- dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade, matrix) :
+ dasharray ? lineSDFUniformValues(painter, tile, layer, crossfade, matrix) :
gradient ? lineGradientUniformValues(painter, tile, layer, matrix, bucket.lineClipsArray.length) :
lineUniformValues(painter, tile, layer, matrix);
@@ -77,9 +86,10 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
context.activeTexture.set(gl.TEXTURE0);
tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
programConfiguration.updatePaintBuffers(crossfade);
- } else if (dasharray && (programChanged || painter.lineAtlas.dirty)) {
+ } else if (dasharray) {
context.activeTexture.set(gl.TEXTURE0);
- painter.lineAtlas.bind(context);
+ tile.lineAtlasTexture.bind(gl.LINEAR, gl.REPEAT);
+ programConfiguration.updatePaintBuffers(crossfade);
} else if (gradient) {
const layerGradient = bucket.gradients[layer.id];
let gradientTexture = layerGradient.texture;
@@ -119,8 +129,5 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues,
layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments,
layer.paint, painter.transform.zoom, programConfiguration, bucket.layoutVertexBuffer2);
-
- firstTile = false;
- // once refactored so that bound texture state is managed, we'll also be able to remove this firstTile/programChanged logic
}
}
diff --git a/src/render/image_atlas.js b/src/render/image_atlas.js
index e1e2165a385..1ac51570034 100644
--- a/src/render/image_atlas.js
+++ b/src/render/image_atlas.js
@@ -49,10 +49,6 @@ export class ImagePosition {
];
}
- get tlbr(): Array {
- return this.tl.concat(this.br);
- }
-
get displaySize(): [number, number] {
return [
(this.paddedRect.w - IMAGE_PADDING * 2) / this.pixelRatio,
diff --git a/src/render/line_atlas.js b/src/render/line_atlas.js
index c869141d587..f8d0fb620e1 100644
--- a/src/render/line_atlas.js
+++ b/src/render/line_atlas.js
@@ -1,8 +1,8 @@
// @flow
-import {warnOnce} from '../util/util.js';
-
-import type Context from '../gl/context.js';
+import {warnOnce, nextPowerOfTwo} from '../util/util.js';
+import {AlphaImage} from '../util/image.js';
+import {register} from '../util/web_worker_transfer.js';
/**
* A LineAtlas lets us reuse rendered dashed lines
@@ -17,24 +17,21 @@ class LineAtlas {
width: number;
height: number;
nextRow: number;
- bytes: number;
- data: Uint8Array;
- dashEntry: {[_: string]: any};
- dirty: boolean;
- texture: WebGLTexture;
+ image: AlphaImage;
+ positions: {[_: string]: any};
+ uploaded: boolean;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.nextRow = 0;
-
- this.data = new Uint8Array(this.width * this.height);
-
- this.dashEntry = {};
+ this.image = new AlphaImage({width, height});
+ this.positions = {};
+ this.uploaded = false;
}
/**
- * Get or create a dash line pattern.
+ * Get a dash line pattern.
*
* @param {Array} dasharray
* @param {boolean} round whether to add circle caps in between dash segments
@@ -42,12 +39,18 @@ class LineAtlas {
* @private
*/
getDash(dasharray: Array, round: boolean) {
- const key = dasharray.join(",") + String(round);
+ const key = this.getKey(dasharray, round);
+ return this.positions[key];
+ }
- if (!this.dashEntry[key]) {
- this.dashEntry[key] = this.addDash(dasharray, round);
- }
- return this.dashEntry[key];
+ trim() {
+ const width = this.width;
+ const height = this.height = nextPowerOfTwo(this.nextRow);
+ this.image.resize({width, height});
+ }
+
+ getKey(dasharray: Array, round: boolean): string {
+ return dasharray.join(',') + String(round);
}
getDashRanges(dasharray: Array, lineAtlasWidth: number, stretch: number) {
@@ -103,7 +106,7 @@ class LineAtlas {
signedDistance = halfStretch - Math.sqrt(minDist * minDist + distMiddle * distMiddle);
}
- this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
+ this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
}
}
}
@@ -146,11 +149,12 @@ class LineAtlas {
const minDist = Math.min(distLeft, distRight);
const signedDistance = range.isDash ? minDist : -minDist;
- this.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
+ this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128));
}
}
addDash(dasharray: Array, round: boolean) {
+ const key = this.getKey(dasharray, round);
const n = round ? 7 : 0;
const height = 2 * n + 1;
@@ -185,38 +189,19 @@ class LineAtlas {
}
}
- const dashEntry = {
- y: (this.nextRow + n + 0.5) / this.height,
- height: 2 * n / this.height,
- width: length
- };
+ const y = this.nextRow + n;
this.nextRow += height;
- this.dirty = true;
-
- return dashEntry;
- }
- bind(context: Context) {
- const gl = context.gl;
- if (!this.texture) {
- this.texture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.width, this.height, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.data);
-
- } else {
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
-
- if (this.dirty) {
- this.dirty = false;
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, this.height, gl.ALPHA, gl.UNSIGNED_BYTE, this.data);
- }
- }
+ const pos = {
+ tl: [y, n],
+ br: [length, 0]
+ };
+ this.positions[key] = pos;
+ return pos;
}
}
+register('LineAtlas', LineAtlas);
+
export default LineAtlas;
diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js
index 12163a675af..caa9fe3dbad 100644
--- a/src/render/program/line_program.js
+++ b/src/render/program/line_program.js
@@ -15,7 +15,6 @@ import type Context from '../../gl/context.js';
import type {UniformValues, UniformLocations} from '../uniform_binding.js';
import type Transform from '../../geo/transform.js';
import type Tile from '../../source/tile.js';
-import type {CrossFaded} from '../../style/properties.js';
import type LineStyleLayer from '../../style/style_layer/line_style_layer.js';
import type Painter from '../painter.js';
import type {CrossfadeParameters} from '../../style/evaluation_parameters.js';
@@ -49,15 +48,12 @@ export type LinePatternUniformsType = {|
export type LineSDFUniformsType = {|
'u_matrix': UniformMatrix4f,
+ 'u_texsize': Uniform2f,
'u_ratio': Uniform1f,
'u_device_pixel_ratio': Uniform1f,
'u_units_to_pixels': Uniform2f,
- 'u_patternscale_a': Uniform2f,
- 'u_patternscale_b': Uniform2f,
- 'u_sdfgamma': Uniform1f,
+ 'u_scale': Uniform3f,
'u_image': Uniform1i,
- 'u_tex_y_a': Uniform1f,
- 'u_tex_y_b': Uniform1f,
'u_mix': Uniform1f
|};
@@ -90,15 +86,12 @@ const linePatternUniforms = (context: Context, locations: UniformLocations): Lin
const lineSDFUniforms = (context: Context, locations: UniformLocations): LineSDFUniformsType => ({
'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
+ 'u_texsize': new Uniform2f(context, locations.u_texsize),
'u_ratio': new Uniform1f(context, locations.u_ratio),
'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio),
'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels),
- 'u_patternscale_a': new Uniform2f(context, locations.u_patternscale_a),
- 'u_patternscale_b': new Uniform2f(context, locations.u_patternscale_b),
- 'u_sdfgamma': new Uniform1f(context, locations.u_sdfgamma),
'u_image': new Uniform1i(context, locations.u_image),
- 'u_tex_y_a': new Uniform1f(context, locations.u_tex_y_a),
- 'u_tex_y_b': new Uniform1f(context, locations.u_tex_y_b),
+ 'u_scale': new Uniform3f(context, locations.u_scale),
'u_mix': new Uniform1f(context, locations.u_mix)
});
@@ -163,29 +156,14 @@ const lineSDFUniformValues = (
painter: Painter,
tile: Tile,
layer: LineStyleLayer,
- dasharray: CrossFaded>,
crossfade: CrossfadeParameters,
matrix: ?Float32Array
): UniformValues => {
- const transform = painter.transform;
- const lineAtlas = painter.lineAtlas;
- const tileRatio = calculateTileRatio(tile, transform);
-
- const round = layer.layout.get('line-cap') === 'round';
-
- const posA = lineAtlas.getDash(dasharray.from, round);
- const posB = lineAtlas.getDash(dasharray.to, round);
-
- const widthA = posA.width * crossfade.fromScale;
- const widthB = posB.width * crossfade.toScale;
-
+ const tileZoomRatio = calculateTileRatio(tile, painter.transform);
return extend(lineUniformValues(painter, tile, layer, matrix), {
- 'u_patternscale_a': [tileRatio / widthA, -posA.height / 2],
- 'u_patternscale_b': [tileRatio / widthB, -posB.height / 2],
- 'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * browser.devicePixelRatio) / 2,
+ 'u_texsize': tile.lineAtlasTexture.size,
+ 'u_scale': [tileZoomRatio, crossfade.fromScale, crossfade.toScale],
'u_image': 0,
- 'u_tex_y_a': posA.y,
- 'u_tex_y_b': posB.y,
'u_mix': crossfade.t
});
};
diff --git a/src/shaders/line_sdf.fragment.glsl b/src/shaders/line_sdf.fragment.glsl
index 70a74996b34..6abbeb3a562 100644
--- a/src/shaders/line_sdf.fragment.glsl
+++ b/src/shaders/line_sdf.fragment.glsl
@@ -1,8 +1,8 @@
uniform lowp float u_device_pixel_ratio;
uniform sampler2D u_image;
-uniform float u_sdfgamma;
uniform float u_mix;
+uniform vec3 u_scale;
varying vec2 v_normal;
varying vec2 v_width2;
@@ -15,6 +15,8 @@ varying float v_gamma_scale;
#pragma mapbox: define lowp float opacity
#pragma mapbox: define mediump float width
#pragma mapbox: define lowp float floorwidth
+#pragma mapbox: define lowp vec4 dash_from
+#pragma mapbox: define lowp vec4 dash_to
void main() {
#pragma mapbox: initialize highp vec4 color
@@ -22,6 +24,8 @@ void main() {
#pragma mapbox: initialize lowp float opacity
#pragma mapbox: initialize mediump float width
#pragma mapbox: initialize lowp float floorwidth
+ #pragma mapbox: initialize mediump vec4 dash_from
+ #pragma mapbox: initialize mediump vec4 dash_to
// Calculate the distance of the pixel from the line in pixels.
float dist = length(v_normal) * v_width2.s;
@@ -35,7 +39,9 @@ void main() {
float sdfdist_a = texture2D(u_image, v_tex_a).a;
float sdfdist_b = texture2D(u_image, v_tex_b).a;
float sdfdist = mix(sdfdist_a, sdfdist_b, u_mix);
- alpha *= smoothstep(0.5 - u_sdfgamma / floorwidth, 0.5 + u_sdfgamma / floorwidth, sdfdist);
+ float sdfwidth = min(dash_from.z * u_scale.y, dash_to.z * u_scale.z);
+ float sdfgamma = 1.0 / (2.0 * u_device_pixel_ratio) / sdfwidth;
+ alpha *= smoothstep(0.5 - sdfgamma / floorwidth, 0.5 + sdfgamma / floorwidth, sdfdist);
gl_FragColor = color * (alpha * opacity);
diff --git a/src/shaders/line_sdf.vertex.glsl b/src/shaders/line_sdf.vertex.glsl
index 7dc1d739c00..941c8111e4a 100644
--- a/src/shaders/line_sdf.vertex.glsl
+++ b/src/shaders/line_sdf.vertex.glsl
@@ -4,7 +4,7 @@
// there are also "special" normals that have a bigger length (of up to 126 in
// this case).
// #define scale 63.0
-#define scale 0.015873016
+#define EXTRUDE_SCALE 0.015873016
attribute vec2 a_pos_normal;
attribute vec4 a_data;
@@ -13,12 +13,11 @@ attribute float a_linesofar;
uniform mat4 u_matrix;
uniform mediump float u_ratio;
uniform lowp float u_device_pixel_ratio;
-uniform vec2 u_patternscale_a;
-uniform float u_tex_y_a;
-uniform vec2 u_patternscale_b;
-uniform float u_tex_y_b;
uniform vec2 u_units_to_pixels;
+uniform vec2 u_texsize;
+uniform mediump vec3 u_scale;
+
varying vec2 v_normal;
varying vec2 v_width2;
varying vec2 v_tex_a;
@@ -32,6 +31,8 @@ varying float v_gamma_scale;
#pragma mapbox: define lowp float offset
#pragma mapbox: define mediump float width
#pragma mapbox: define lowp float floorwidth
+#pragma mapbox: define lowp vec4 dash_from
+#pragma mapbox: define lowp vec4 dash_to
void main() {
#pragma mapbox: initialize highp vec4 color
@@ -41,6 +42,8 @@ void main() {
#pragma mapbox: initialize lowp float offset
#pragma mapbox: initialize mediump float width
#pragma mapbox: initialize lowp float floorwidth
+ #pragma mapbox: initialize mediump vec4 dash_from
+ #pragma mapbox: initialize mediump vec4 dash_to
// the distance over which the line edge fades out.
// Retina devices need a smaller distance to avoid aliasing.
@@ -69,7 +72,7 @@ void main() {
// Scale the extrusion vector down to a normal and then up by the line width
// of this vertex.
- mediump vec2 dist = outset * a_extrude * scale;
+ mediump vec2 dist = outset * a_extrude * EXTRUDE_SCALE;
// Calculate the offset when drawing a line that is to the side of the actual line.
// We do this by creating a vector that points towards the extrude, but rotate
@@ -77,7 +80,7 @@ void main() {
// extrude vector points in another direction.
mediump float u = 0.5 * a_direction;
mediump float t = 1.0 - abs(u);
- mediump vec2 offset2 = offset * a_extrude * scale * normal.y * mat2(t, -u, u, t);
+ mediump vec2 offset2 = offset * a_extrude * EXTRUDE_SCALE * normal.y * mat2(t, -u, u, t);
vec4 projected_extrude = u_matrix * vec4(dist / u_ratio, 0.0, 0.0);
gl_Position = u_matrix * vec4(pos + offset2 / u_ratio, 0.0, 1.0) + projected_extrude;
@@ -91,8 +94,17 @@ void main() {
v_gamma_scale = 1.0;
#endif
- v_tex_a = vec2(a_linesofar * u_patternscale_a.x / floorwidth, normal.y * u_patternscale_a.y + u_tex_y_a);
- v_tex_b = vec2(a_linesofar * u_patternscale_b.x / floorwidth, normal.y * u_patternscale_b.y + u_tex_y_b);
+ float tileZoomRatio = u_scale.x;
+ float fromScale = u_scale.y;
+ float toScale = u_scale.z;
+
+ float widthA = dash_from.z * fromScale;
+ float widthB = dash_to.z * toScale;
+ float heightA = dash_from.y;
+ float heightB = dash_to.y;
+
+ v_tex_a = vec2(a_linesofar * (tileZoomRatio / widthA) / floorwidth, (-normal.y * heightA + dash_from.x + 0.5) / u_texsize.y);
+ v_tex_b = vec2(a_linesofar * (tileZoomRatio / widthB) / floorwidth, (-normal.y * heightB + dash_to.x + 0.5) / u_texsize.y);
v_width2 = vec2(outset, inset);
}
diff --git a/src/source/tile.js b/src/source/tile.js
index eb36fb2e23a..a29dc1629ee 100644
--- a/src/source/tile.js
+++ b/src/source/tile.js
@@ -26,6 +26,7 @@ import type Actor from '../util/actor.js';
import type DEMData from '../data/dem_data.js';
import type {AlphaImage} from '../util/image.js';
import type ImageAtlas from '../render/image_atlas.js';
+import type LineAtlas from '../render/line_atlas.js';
import type ImageManager from '../render/image_manager.js';
import type Context from '../gl/context.js';
import type {OverscaledTileID} from './tile_id.js';
@@ -62,6 +63,8 @@ class Tile {
latestRawTileData: ?ArrayBuffer;
imageAtlas: ?ImageAtlas;
imageAtlasTexture: Texture;
+ lineAtlas: ?LineAtlas;
+ lineAtlasTexture: Texture;
glyphAtlasImage: ?AlphaImage;
glyphAtlasTexture: Texture;
expirationTime: any;
@@ -215,6 +218,9 @@ class Tile {
if (data.glyphAtlasImage) {
this.glyphAtlasImage = data.glyphAtlasImage;
}
+ if (data.lineAtlas) {
+ this.lineAtlas = data.lineAtlas;
+ }
}
/**
@@ -228,17 +234,26 @@ class Tile {
}
this.buckets = {};
- if (this.imageAtlasTexture) {
- this.imageAtlasTexture.destroy();
- }
-
if (this.imageAtlas) {
this.imageAtlas = null;
}
+ if (this.lineAtlas) {
+ this.lineAtlas = null;
+ }
+
+ if (this.imageAtlasTexture) {
+ this.imageAtlasTexture.destroy();
+ }
+
if (this.glyphAtlasTexture) {
this.glyphAtlasTexture.destroy();
}
+
+ if (this.lineAtlasTexture) {
+ this.lineAtlasTexture.destroy();
+ }
+
Debug.run(() => {
if (this.queryGeometryDebugViz) {
this.queryGeometryDebugViz.unload();
@@ -275,6 +290,11 @@ class Tile {
this.glyphAtlasTexture = new Texture(context, this.glyphAtlasImage, gl.ALPHA);
this.glyphAtlasImage = null;
}
+
+ if (this.lineAtlas && !this.lineAtlas.uploaded) {
+ this.lineAtlasTexture = new Texture(context, this.lineAtlas.image, gl.ALPHA);
+ this.lineAtlas.uploaded = true;
+ }
}
prepare(imageManager: ImageManager) {
diff --git a/src/source/worker_source.js b/src/source/worker_source.js
index d9c37b37630..9c33bf574db 100644
--- a/src/source/worker_source.js
+++ b/src/source/worker_source.js
@@ -4,6 +4,7 @@ import type {RequestParameters} from '../util/ajax.js';
import type {RGBAImage, AlphaImage} from '../util/image.js';
import type {GlyphPositions} from '../render/glyph_atlas.js';
import type ImageAtlas from '../render/image_atlas.js';
+import type LineAtlas from '../render/line_atlas.js';
import type {OverscaledTileID} from './tile_id.js';
import type {Bucket} from '../data/bucket.js';
import type FeatureIndex from '../data/feature_index.js';
@@ -52,6 +53,7 @@ export type WorkerTileResult = {
buckets: Array,
imageAtlas: ImageAtlas,
glyphAtlasImage: AlphaImage,
+ lineAtlas: LineAtlas,
featureIndex: FeatureIndex,
collisionBoxArray: CollisionBoxArray,
rawTileData?: ArrayBuffer,
diff --git a/src/source/worker_tile.js b/src/source/worker_tile.js
index 9356cf11076..6aa2fd0312b 100644
--- a/src/source/worker_tile.js
+++ b/src/source/worker_tile.js
@@ -11,6 +11,7 @@ import FillBucket from '../data/bucket/fill_bucket.js';
import FillExtrusionBucket from '../data/bucket/fill_extrusion_bucket.js';
import {warnOnce, mapObject, values} from '../util/util.js';
import assert from 'assert';
+import LineAtlas from '../render/line_atlas.js';
import ImageAtlas from '../render/image_atlas.js';
import GlyphAtlas from '../render/glyph_atlas.js';
import EvaluationParameters from '../style/evaluation_parameters.js';
@@ -83,11 +84,15 @@ class WorkerTile {
const buckets: {[_: string]: Bucket} = {};
+ // we initially reserve space for a 256x256 atlas, but trim it after processing all line features
+ const lineAtlas = new LineAtlas(256, 256);
+
const options = {
featureIndex,
iconDependencies: {},
patternDependencies: {},
glyphDependencies: {},
+ lineAtlas,
availableImages
};
@@ -155,6 +160,8 @@ class WorkerTile {
}
}
+ lineAtlas.trim();
+
let error: ?Error;
let glyphMap: ?{[_: string]: {[_: number]: ?StyleGlyph}};
let iconMap: ?{[_: string]: StyleImage};
@@ -239,6 +246,7 @@ class WorkerTile {
featureIndex,
collisionBoxArray: this.collisionBoxArray,
glyphAtlasImage: glyphAtlas.image,
+ lineAtlas,
imageAtlas,
// Only used for benchmarking:
glyphMap: this.returnDependencies ? glyphMap : null,
diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json
index fa43a896a55..b31fbe23031 100644
--- a/src/style-spec/reference/v8.json
+++ b/src/style-spec/reference/v8.json
@@ -884,15 +884,19 @@
"android": "2.0.1",
"ios": "2.0.0",
"macos": "0.1.0"
+ },
+ "data-driven styling": {
+ "js": "2.3.0"
}
},
"expression": {
"interpolated": false,
"parameters": [
- "zoom"
+ "zoom",
+ "feature"
]
},
- "property-type": "data-constant"
+ "property-type": "data-driven"
},
"line-join": {
"type": "enum",
@@ -4532,15 +4536,18 @@
"ios": "2.0.0",
"macos": "0.1.0"
},
- "data-driven styling": {}
+ "data-driven styling": {
+ "js": "2.3.0"
+ }
},
"expression": {
"interpolated": false,
"parameters": [
- "zoom"
+ "zoom",
+ "feature"
]
},
- "property-type": "cross-faded"
+ "property-type": "cross-faded-data-driven"
},
"line-pattern": {
"type": "resolvedImage",
diff --git a/src/style-spec/types.js b/src/style-spec/types.js
index 1186be1c05e..0269f8c7819 100644
--- a/src/style-spec/types.js
+++ b/src/style-spec/types.js
@@ -197,7 +197,7 @@ export type LineLayerSpecification = {|
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
- "line-cap"?: PropertyValueSpecification<"butt" | "round" | "square">,
+ "line-cap"?: DataDrivenPropertyValueSpecification<"butt" | "round" | "square">,
"line-join"?: DataDrivenPropertyValueSpecification<"bevel" | "round" | "miter">,
"line-miter-limit"?: PropertyValueSpecification,
"line-round-limit"?: PropertyValueSpecification,
@@ -213,7 +213,7 @@ export type LineLayerSpecification = {|
"line-gap-width"?: DataDrivenPropertyValueSpecification,
"line-offset"?: DataDrivenPropertyValueSpecification,
"line-blur"?: DataDrivenPropertyValueSpecification,
- "line-dasharray"?: PropertyValueSpecification>,
+ "line-dasharray"?: DataDrivenPropertyValueSpecification>,
"line-pattern"?: DataDrivenPropertyValueSpecification,
"line-gradient"?: ExpressionSpecification
|}
diff --git a/src/style/properties.js b/src/style/properties.js
index 1ebf0e850d5..71084fe9de9 100644
--- a/src/style/properties.js
+++ b/src/style/properties.js
@@ -27,7 +27,8 @@ type TimePoint = number;
export type CrossFaded = {
to: T,
- from: T
+ from: T,
+ other?: T
};
/**
@@ -634,7 +635,12 @@ export class CrossFadedDataDrivenProperty extends DataDrivenProperty {
const z = parameters.zoom;
- return z > parameters.zoomHistory.lastIntegerZoom ? {from: min, to: mid} : {from: max, to: mid};
+ // ugly hack alert: when evaluating non-constant dashes on the worker side,
+ // we need all three values to pack into the atlas; the if condition is always false there;
+ // will be removed after removing cross-fading
+ return z > parameters.zoomHistory.lastIntegerZoom ?
+ {from: min, to: mid, other: max} :
+ {from: max, to: mid, other: min};
}
interpolate(a: PossiblyEvaluatedPropertyValue>): PossiblyEvaluatedPropertyValue> {
diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js
index d70e97d076e..729725d70ce 100644
--- a/src/style/style_layer/line_style_layer_properties.js
+++ b/src/style/style_layer/line_style_layer_properties.js
@@ -20,7 +20,7 @@ import type Formatted from '../../style-spec/expression/types/formatted.js';
import type ResolvedImage from '../../style-spec/expression/types/resolved_image.js';
export type LayoutProps = {|
- "line-cap": DataConstantProperty<"butt" | "round" | "square">,
+ "line-cap": DataDrivenProperty<"butt" | "round" | "square">,
"line-join": DataDrivenProperty<"bevel" | "round" | "miter">,
"line-miter-limit": DataConstantProperty,
"line-round-limit": DataConstantProperty,
@@ -28,7 +28,7 @@ export type LayoutProps = {|
|};
const layout: Properties = new Properties({
- "line-cap": new DataConstantProperty(styleSpec["layout_line"]["line-cap"]),
+ "line-cap": new DataDrivenProperty(styleSpec["layout_line"]["line-cap"]),
"line-join": new DataDrivenProperty(styleSpec["layout_line"]["line-join"]),
"line-miter-limit": new DataConstantProperty(styleSpec["layout_line"]["line-miter-limit"]),
"line-round-limit": new DataConstantProperty(styleSpec["layout_line"]["line-round-limit"]),
@@ -44,7 +44,7 @@ export type PaintProps = {|
"line-gap-width": DataDrivenProperty,
"line-offset": DataDrivenProperty,
"line-blur": DataDrivenProperty,
- "line-dasharray": CrossFadedProperty>,
+ "line-dasharray": CrossFadedDataDrivenProperty>,
"line-pattern": CrossFadedDataDrivenProperty,
"line-gradient": ColorRampProperty,
|};
@@ -58,7 +58,7 @@ const paint: Properties = new Properties({
"line-gap-width": new DataDrivenProperty(styleSpec["paint_line"]["line-gap-width"]),
"line-offset": new DataDrivenProperty(styleSpec["paint_line"]["line-offset"]),
"line-blur": new DataDrivenProperty(styleSpec["paint_line"]["line-blur"]),
- "line-dasharray": new CrossFadedProperty(styleSpec["paint_line"]["line-dasharray"]),
+ "line-dasharray": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-dasharray"]),
"line-pattern": new CrossFadedDataDrivenProperty(styleSpec["paint_line"]["line-pattern"]),
"line-gradient": new ColorRampProperty(styleSpec["paint_line"]["line-gradient"]),
});
diff --git a/test/integration/render-tests/line-cap/data-driven/expected.png b/test/integration/render-tests/line-cap/data-driven/expected.png
new file mode 100644
index 00000000000..5d1599bbf27
Binary files /dev/null and b/test/integration/render-tests/line-cap/data-driven/expected.png differ
diff --git a/test/integration/render-tests/line-cap/data-driven/style.json b/test/integration/render-tests/line-cap/data-driven/style.json
new file mode 100644
index 00000000000..8de29dc2c24
--- /dev/null
+++ b/test/integration/render-tests/line-cap/data-driven/style.json
@@ -0,0 +1,62 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 128,
+ "height": 48
+ }
+ },
+ "zoom": 0,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"property": 2},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -10], [40, -10]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 4},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 0], [40, 0]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 6},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 10], [40, 10]]
+ }
+ }
+ ]
+ }
+ }
+ },
+ "layers": [
+ {
+ "id": "road",
+ "type": "line",
+ "source": "geojson",
+ "layout": {
+ "line-cap": [
+ "match", ["get", "property"],
+ 2, "square",
+ 4, "butt",
+ "round"
+ ]
+ },
+ "paint": {
+ "line-width": 6
+ }
+ }
+ ]
+}
diff --git a/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png
new file mode 100644
index 00000000000..ef16adf79d4
Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/expected.png differ
diff --git a/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json
new file mode 100644
index 00000000000..bc95e002e7e
--- /dev/null
+++ b/test/integration/render-tests/line-dasharray/data-driven/composite-dash-composite-cap/style.json
@@ -0,0 +1,89 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 128,
+ "height": 64
+ }
+ },
+ "zoom": 0.1,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"property": 2},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -18], [40, -18]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 3},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -10], [40, -10]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 4},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -2], [40, -2]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 5},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 7], [40, 7]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 6},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 18], [40, 18]]
+ }
+ }
+ ]
+ }
+ }
+ },
+ "layers": [
+ {
+ "id": "road",
+ "type": "line",
+ "source": "geojson",
+ "layout": {
+ "line-cap": [
+ "step", ["zoom"],
+ ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"],
+ 1, "butt"
+ ]
+ },
+ "paint": {
+ "line-width": ["get", "property"],
+ "line-dasharray": [
+ "step", ["zoom"],
+ [
+ "match", ["get", "property"],
+ 2, ["literal", [2, 2]],
+ 3, ["literal", [2, 3]],
+ 4, ["literal", [2, 4]],
+ 5, ["literal", [2, 5]],
+ ["literal", [2, 6]]
+ ],
+ 1, ["literal", [1, 1]]
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png
new file mode 100644
index 00000000000..53b0a629d3b
Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/expected.png differ
diff --git a/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json
new file mode 100644
index 00000000000..01db0779b87
--- /dev/null
+++ b/test/integration/render-tests/line-dasharray/data-driven/const-dash-feature-cap/style.json
@@ -0,0 +1,74 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 128,
+ "height": 64
+ }
+ },
+ "zoom": 0,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"property": 2},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -18], [40, -18]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 3},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -10], [40, -10]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 4},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -2], [40, -2]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 5},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 7], [40, 7]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 6},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 18], [40, 18]]
+ }
+ }
+ ]
+ }
+ }
+ },
+ "layers": [
+ {
+ "id": "road",
+ "type": "line",
+ "source": "geojson",
+ "layout": {
+ "line-cap": ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"]
+ },
+ "paint": {
+ "line-width": ["get", "property"],
+ "line-dasharray": [2, 2]
+ }
+ }
+ ]
+}
diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png
new file mode 100644
index 00000000000..7465f4abfab
Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/expected.png differ
diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json
new file mode 100644
index 00000000000..41cfe23f681
--- /dev/null
+++ b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-const-cap/style.json
@@ -0,0 +1,81 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 128,
+ "height": 64
+ }
+ },
+ "zoom": 0,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"property": 2},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -18], [40, -18]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 3},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -10], [40, -10]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 4},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -2], [40, -2]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 5},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 7], [40, 7]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 6},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 18], [40, 18]]
+ }
+ }
+ ]
+ }
+ }
+ },
+ "layers": [
+ {
+ "id": "road",
+ "type": "line",
+ "source": "geojson",
+ "layout": {
+ "line-cap": "butt"
+ },
+ "paint": {
+ "line-width": ["get", "property"],
+ "line-dasharray": [
+ "match", ["get", "property"],
+ 2, ["literal", [2, 2]],
+ 3, ["literal", [2, 3]],
+ 4, ["literal", [2, 4]],
+ 5, ["literal", [2, 5]],
+ ["literal", [2, 6]]
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png
new file mode 100644
index 00000000000..093c5005351
Binary files /dev/null and b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/expected.png differ
diff --git a/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json
new file mode 100644
index 00000000000..a356b05a3f1
--- /dev/null
+++ b/test/integration/render-tests/line-dasharray/data-driven/feature-dash-feature-cap/style.json
@@ -0,0 +1,81 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 128,
+ "height": 64
+ }
+ },
+ "zoom": 0,
+ "sources": {
+ "geojson": {
+ "type": "geojson",
+ "data": {
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {"property": 2},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -18], [40, -18]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 3},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -10], [40, -10]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 4},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, -2], [40, -2]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 5},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 7], [40, 7]]
+ }
+ },
+ {
+ "type": "Feature",
+ "properties": {"property": 6},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [[-40, 18], [40, 18]]
+ }
+ }
+ ]
+ }
+ }
+ },
+ "layers": [
+ {
+ "id": "road",
+ "type": "line",
+ "source": "geojson",
+ "layout": {
+ "line-cap": ["case", ["==", ["%", ["get", "property"], 2], 0], "butt", "round"]
+ },
+ "paint": {
+ "line-width": ["get", "property"],
+ "line-dasharray": [
+ "match", ["get", "property"],
+ 2, ["literal", [2, 2]],
+ 3, ["literal", [2, 3]],
+ 4, ["literal", [2, 4]],
+ 5, ["literal", [2, 5]],
+ ["literal", [2, 6]]
+ ]
+ }
+ }
+ ]
+}
diff --git a/test/unit/render/line_atlas.test.js b/test/unit/render/line_atlas.test.js
index ef607b93ab6..1f2f2e4abb7 100644
--- a/test/unit/render/line_atlas.test.js
+++ b/test/unit/render/line_atlas.test.js
@@ -5,43 +5,43 @@ test('LineAtlas', (t) => {
const lineAtlas = new LineAtlas(64, 64);
t.test('round [0, 0]', (t) => {
const entry = lineAtlas.addDash([0, 0], true);
- t.equal(entry.width, 0);
+ t.equal(entry.br[0], 0);
t.end();
});
t.test('round [1, 0]', (t) => {
const entry = lineAtlas.addDash([1, 0], true);
- t.equal(entry.width, 1);
+ t.equal(entry.br[0], 1);
t.end();
});
t.test('round [0, 1]', (t) => {
const entry = lineAtlas.addDash([0, 1], true);
- t.equal(entry.width, 1);
+ t.equal(entry.br[0], 1);
t.end();
});
t.test('odd round [1, 2, 1]', (t) => {
const entry = lineAtlas.addDash([1, 2, 1], true);
- t.equal(entry.width, 4);
+ t.equal(entry.br[0], 4);
t.end();
});
t.test('regular [0, 0]', (t) => {
const entry = lineAtlas.addDash([0, 0], false);
- t.equal(entry.width, 0);
+ t.equal(entry.br[0], 0);
t.end();
});
t.test('regular [1, 0]', (t) => {
const entry = lineAtlas.addDash([1, 0], false);
- t.equal(entry.width, 1);
+ t.equal(entry.br[0], 1);
t.end();
});
t.test('regular [0, 1]', (t) => {
const entry = lineAtlas.addDash([0, 1], false);
- t.equal(entry.width, 1);
+ t.equal(entry.br[0], 1);
t.end();
});
t.test('odd regular [1, 2, 1]', (t) => {
const entry = lineAtlas.addDash([1, 2, 1], false);
- t.equal(entry.width, 4);
+ t.equal(entry.br[0], 4);
t.end();
});
t.end();
diff --git a/test/unit/style-spec/fixture/functions.output-api-supported.json b/test/unit/style-spec/fixture/functions.output-api-supported.json
index 580f30573a8..9461099bd21 100644
--- a/test/unit/style-spec/fixture/functions.output-api-supported.json
+++ b/test/unit/style-spec/fixture/functions.output-api-supported.json
@@ -28,7 +28,7 @@
"line": 75
},
{
- "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found",
+ "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found\nIf you intended to use a categorical function, specify `\"type\": \"categorical\"`.",
"line": 89
},
{
diff --git a/test/unit/style-spec/fixture/functions.output.json b/test/unit/style-spec/fixture/functions.output.json
index 580f30573a8..9461099bd21 100644
--- a/test/unit/style-spec/fixture/functions.output.json
+++ b/test/unit/style-spec/fixture/functions.output.json
@@ -28,7 +28,7 @@
"line": 75
},
{
- "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found",
+ "message": "layers[6].paint.line-dasharray.stops[0][0]: number expected, string found\nIf you intended to use a categorical function, specify `\"type\": \"categorical\"`.",
"line": 89
},
{