Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property refactor #5682

Merged
merged 13 commits into from
Nov 17, 2017
Merged
130 changes: 130 additions & 0 deletions build/generate-style-code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

require('flow-remove-types/register');

const fs = require('fs');
const ejs = require('ejs');
const spec = require('../src/style-spec/reference/v8');
const Color = require('../src/style-spec/util/color');

global.camelize = function (str) {
return str.replace(/(?:^|-)(.)/g, function (_, x) {
return x.toUpperCase();
});
};

global.isDataDriven = function (property) {
return property['property-function'] === true;
};

global.flowType = function (property) {
switch (property.type) {
case 'boolean':
return 'boolean';
case 'number':
return 'number';
case 'string':
return 'string';
case 'enum':
return Object.keys(property.values).map(JSON.stringify).join(' | ');
case 'color':
return `Color`;
case 'array':
if (property.length) {
return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`;
} else {
return `Array<${flowType({type: property.value})}>`;
}
default: throw new Error(`unknown type for ${property.name}`)
}
};

global.propertyType = function (property) {
if (isDataDriven(property)) {
return `DataDrivenProperty<${flowType(property)}>`;
} else if (/-pattern$/.test(property.name) || property.name === 'line-dasharray') {
return `CrossFadedProperty<${flowType(property)}>`;
} else if (property.name === 'heatmap-color') {
return `HeatmapColorProperty`;
} else {
return `DataConstantProperty<${flowType(property)}>`;
}
};

global.runtimeType = function (property) {
switch (property.type) {
case 'boolean':
return 'BooleanType';
case 'number':
return 'NumberType';
case 'string':
case 'enum':
return 'StringType';
case 'color':
return `ColorType`;
case 'array':
if (property.length) {
return `array(${runtimeType({type: property.value})}, ${property.length})`;
} else {
return `array(${runtimeType({type: property.value})})`;
}
default: throw new Error(`unknown type for ${property.name}`)
}
};

global.defaultValue = function (property) {
switch (property.type) {
case 'boolean':
case 'number':
case 'string':
case 'array':
case 'enum':
return JSON.stringify(property.default);
case 'color':
if (typeof property.default !== 'string') {
return JSON.stringify(property.default);
} else {
const {r, g, b, a} = Color.parse(property.default);
return `new Color(${r}, ${g}, ${b}, ${a})`;
}
default: throw new Error(`unknown type for ${property.name}`)
}
};

global.propertyValue = function (property, type) {
if (isDataDriven(property)) {
return `new DataDrivenProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`;
} else if (/-pattern$/.test(property.name) || property.name === 'line-dasharray') {
return `new CrossFadedProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`;
} else if (property.name === 'heatmap-color') {
return `new HeatmapColorProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`;
} else {
return `new DataConstantProperty(styleSpec["${type}_${property.layerType}"]["${property.name}"])`;
}
};

const propertiesJs = ejs.compile(fs.readFileSync('src/style/style_layer/layer_properties.js.ejs', 'utf8'), {strict: true});

const layers = Object.keys(spec.layer.type.values).map((type) => {
const layoutProperties = Object.keys(spec[`layout_${type}`]).reduce((memo, name) => {
if (name !== 'visibility') {
spec[`layout_${type}`][name].name = name;
spec[`layout_${type}`][name].layerType = type;
memo.push(spec[`layout_${type}`][name]);
}
return memo;
}, []);

const paintProperties = Object.keys(spec[`paint_${type}`]).reduce((memo, name) => {
spec[`paint_${type}`][name].name = name;
spec[`paint_${type}`][name].layerType = type;
memo.push(spec[`paint_${type}`][name]);
return memo;
}, []);

return { type, layoutProperties, paintProperties };
});

for (const layer of layers) {
fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.js`, propertiesJs(layer))
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"coveralls": "^2.11.8",
"derequire": "^2.0.6",
"documentation": "5.3.3",
"ejs": "^2.5.7",
"envify": "^4.0.0",
"eslint": "4.1.1",
"eslint-config-mourner": "^2.0.0",
Expand Down
14 changes: 7 additions & 7 deletions src/data/bucket/line_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const vectorTileFeatureTypes = require('@mapbox/vector-tile').VectorTileFeature.

import type {Bucket, IndexedFeature, PopulateParameters, SerializedBucket} from '../bucket';
import type {ProgramInterface} from '../program_configuration';
import type StyleLayer from '../../style/style_layer';
import type LineStyleLayer from '../../style/style_layer/line_style_layer';
import type Point from '@mapbox/point-geometry';
import type {Segment} from '../segment';
import type {StructArray} from '../../util/struct_array';
Expand Down Expand Up @@ -62,7 +62,7 @@ const lineInterface = {
{property: 'line-gap-width', name: 'gapwidth'},
{property: 'line-offset'},
{property: 'line-width'},
{property: 'line-width', name: 'floorwidth', useIntegerZoom: true},
{property: 'line-floorwidth'},
],
indexArrayType: TriangleIndexArray
};
Expand Down Expand Up @@ -103,7 +103,7 @@ class LineBucket implements Bucket {
index: number;
zoom: number;
overscaling: number;
layers: Array<StyleLayer>;
layers: Array<LineStyleLayer>;

layoutVertexArray: StructArray;
layoutVertexBuffer: VertexBuffer;
Expand Down Expand Up @@ -168,10 +168,10 @@ class LineBucket implements Bucket {

addFeature(feature: VectorTileFeature, geometry: Array<Array<Point>>) {
const layout = this.layers[0].layout;
const join = this.layers[0].getLayoutValue('line-join', {zoom: this.zoom}, feature);
const cap = layout['line-cap'];
const miterLimit = layout['line-miter-limit'];
const roundLimit = layout['line-round-limit'];
const join = layout.get('line-join').evaluate(feature);
const cap = layout.get('line-cap');
const miterLimit = layout.get('line-miter-limit');
const roundLimit = layout.get('line-round-limit');

for (const line of geometry) {
this.addLine(line, feature, join, cap, miterLimit, roundLimit);
Expand Down
79 changes: 63 additions & 16 deletions src/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow

const Point = require('@mapbox/point-geometry');
const {SegmentVector} = require('../segment');
const VertexBuffer = require('../../gl/vertex_buffer');
Expand Down Expand Up @@ -27,6 +28,8 @@ import type {
} from '../../util/struct_array';
import type SymbolStyleLayer from '../../style/style_layer/symbol_style_layer';
import type {SymbolQuad} from '../../symbol/quads';
import type {SizeData} from '../../symbol/symbol_size';
import type {PossiblyEvaluatedPropertyValue} from '../../style/properties';

export type SingleCollisionBox = {
x1: number;
Expand Down Expand Up @@ -365,8 +368,29 @@ class SymbolBucket implements Bucket {
index: number;
sdfIcons: boolean;
iconsNeedLinear: boolean;
textSizeData: any;
iconSizeData: any;

// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add more detail to why we need the max-size for line-symbol placement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, but nobody seems remember the details! Tracking this in #5683.

// 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<number>; // (3)
layoutIconSize: PossiblyEvaluatedPropertyValue<number>; // (3)
textMaxSize: PossiblyEvaluatedPropertyValue<number>; // (4)
compositeTextSizes: [PossiblyEvaluatedPropertyValue<number>, PossiblyEvaluatedPropertyValue<number>]; // (5)
compositeIconSizes: [PossiblyEvaluatedPropertyValue<number>, PossiblyEvaluatedPropertyValue<number>]; // (5)

placedGlyphArray: StructArray;
placedIconArray: StructArray;
glyphOffsetArray: StructArray;
Expand Down Expand Up @@ -414,13 +438,34 @@ class SymbolBucket implements Bucket {
this.symbolInstances = options.symbolInstances;

const layout = options.layers[0].layout;
this.sortFeaturesByY = layout['text-allow-overlap'] || layout['icon-allow-overlap'] ||
layout['text-ignore-placement'] || layout['icon-ignore-placement'];
this.sortFeaturesByY = layout.get('text-allow-overlap') || layout.get('icon-allow-overlap') ||
layout.get('text-ignore-placement') || layout.get('icon-ignore-placement');

} else {
const layer = this.layers[0];
this.textSizeData = getSizeData(this.zoom, layer, 'text-size');
this.iconSizeData = getSizeData(this.zoom, layer, 'icon-size');
const layer: SymbolStyleLayer = this.layers[0];
const unevaluatedLayoutValues = layer._unevaluatedLayout._values;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use

const textSize = layer.getLayoutProperty('text-size');
const iconSize = layer.getLayoutProperty('icon-size');

instead of accessing _unevaluatedValues directly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is this because getLayoutProperty's return type isn't parameterized on the property name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because layer.getLayoutProperty returns the raw style spec value (as needed for the public runtime styling API), and we need the PropertyValue.

Copy link
Contributor

@anandthakker anandthakker Nov 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I guess we could add SymbolStyleLayer#get{Text,Icon}Size()... but treating *_bucket as a 'friend' of *_style_layer is probably reasonable, too.


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

Expand All @@ -437,12 +482,14 @@ class SymbolBucket implements Bucket {
}

populate(features: Array<IndexedFeature>, options: PopulateParameters) {
const layer: SymbolStyleLayer = this.layers[0];
const layer = this.layers[0];
const layout = layer.layout;
const textFont = layout['text-font'];

const hasText = (!layer.isLayoutValueFeatureConstant('text-field') || layout['text-field']) && textFont;
const hasIcon = (!layer.isLayoutValueFeatureConstant('icon-image') || layout['icon-image']);
const textFont = layout.get('text-font').join(',');
const textField = layout.get('text-field');
const iconImage = layout.get('icon-image');
const hasText = textField.value.kind !== 'constant' || textField.value.value.length > 0 && textFont.length > 0;
const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0;

this.features = [];

Expand All @@ -462,13 +509,13 @@ class SymbolBucket implements Bucket {

let text;
if (hasText) {
text = layer.getValueAndResolveTokens('text-field', globalProperties, feature);
text = transformText(text, layer, globalProperties, feature);
text = layer.getValueAndResolveTokens('text-field', feature);
text = transformText(text, layer, feature);
}

let icon;
if (hasIcon) {
icon = layer.getValueAndResolveTokens('icon-image', globalProperties, feature);
icon = layer.getValueAndResolveTokens('icon-image', feature);
}

if (!text && !icon) {
Expand All @@ -494,7 +541,7 @@ class SymbolBucket implements Bucket {
}

if (text) {
const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line';
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') === 'line';
const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(text);
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = true;
Expand All @@ -508,7 +555,7 @@ class SymbolBucket implements Bucket {
}
}

if (layout['symbol-placement'] === 'line') {
if (layout.get('symbol-placement') === 'line') {
// Merge adjacent lines with the same text to improve labelling.
// It's better to place labels on one long line than on many short segments.
this.features = mergeLines(this.features);
Expand Down
Loading