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

Multiple text formats for symbols via "format" expression #6994

Merged
merged 5 commits into from Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/generate-flow-typed-style-spec.js
Expand Up @@ -118,6 +118,8 @@ fs.writeFileSync('src/style-spec/types.js', `// @flow

export type ColorSpecification = string;

export type FormattedSpecification = string;

export type FilterSpecification =
| ['has', string]
| ['!has', string]
Expand Down
5 changes: 4 additions & 1 deletion build/generate-style-code.js
Expand Up @@ -24,6 +24,8 @@ global.flowType = function (property) {
return Object.keys(property.values).map(JSON.stringify).join(' | ');
case 'color':
return `Color`;
case 'formatted':
return `string | Formatted`;
case 'array':
if (property.length) {
return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`;
Expand Down Expand Up @@ -61,6 +63,8 @@ global.runtimeType = function (property) {
return 'StringType';
case 'color':
return `ColorType`;
case 'formatted':
return `FormattedType`;
case 'array':
if (property.length) {
return `array(${runtimeType({type: property.value})}, ${property.length})`;
Expand Down Expand Up @@ -131,4 +135,3 @@ const layers = Object.keys(spec.layer.type.values).map((type) => {
for (const layer of layers) {
fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.js`, propertiesJs(layer))
}

2 changes: 1 addition & 1 deletion debug/csp.html
Expand Up @@ -27,7 +27,7 @@
hash: true
});

mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.2/mapbox-gl-rtl-text.js');
mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');

</script>
</body>
Expand Down
8 changes: 8 additions & 0 deletions docs/components/expression-metadata.js
Expand Up @@ -134,6 +134,14 @@ const types = {
collator: [{
type: 'collator',
parameters: [ '{ "case-sensitive": boolean, "diacritic-sensitive": boolean, "locale": string }' ]
}],
format: [{
type: 'formatted',
parameters: [
'input_1: string, options_1: { "font-scale": number, "text-font": array<string> }',
'...',
'input_n: string, options_n: { "font-scale": number, "text-font": array<string> }'
]
}]
};

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/example/mapbox-gl-rtl-text.html
@@ -1,7 +1,7 @@
<div id='map'></div>

<script>
mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.2/mapbox-gl-rtl-text.js');
mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');

var map = new mapboxgl.Map({
container: 'map',
Expand Down
16 changes: 16 additions & 0 deletions docs/pages/style-spec.js
Expand Up @@ -159,6 +159,9 @@ const navigation = [
{
"title": "String"
},
{
"title": "Formatted"
},
{
"title": "Boolean"
},
Expand Down Expand Up @@ -948,6 +951,19 @@ export default class extends React.Component {
<p>Especially of note is the support for hsl, which can be <a href='http://mothereffinghsl.com/'>easier to reason about than rgb()</a>.</p>
</div>

<div className='pad2 keyline-bottom'>
<a id='types-formatted' className='anchor'/>
<h3 className='space-bottom1'><a href='#types-formatted' title='link to formatted'>Formatted</a></h3>
<p>The <code>formatted</code> type represents a string broken into sections annotated with separate formatting options.</p>
{highlightJSON(`
{
"text-field": ["format",
"foo", { "font-scale": 1.2 },
"bar", { "font-scale": 0.8 }
]
}`)}
</div>

<div className='pad2 keyline-bottom'>
<a id='types-string' className='anchor'/>
<h3 className='space-bottom1'><a href='#types-string' title='link to string'>String</a></h3>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -40,7 +40,7 @@
"devDependencies": {
"@mapbox/batfish": "^1.0.4",
"@mapbox/flow-remove-types": "^1.3.0-await.upstream.1",
"@mapbox/mapbox-gl-rtl-text": "^0.1.2",
"@mapbox/mapbox-gl-rtl-text": "^0.2.0",
"@mapbox/mapbox-gl-test-suite": "file:test/integration",
"babel-eslint": "^7.0.0",
"benchmark": "~2.1.0",
Expand Down
35 changes: 25 additions & 10 deletions src/data/bucket/symbol_bucket.js
Expand Up @@ -18,6 +18,7 @@ import Anchor from '../../symbol/anchor';
import { getSizeData } from '../../symbol/symbol_size';
import { register } from '../../util/web_worker_transfer';
import EvaluationParameters from '../../style/evaluation_parameters';
import {Formatted} from '../../style-spec/expression/definitions/formatted';

import type {
Bucket,
Expand Down Expand Up @@ -53,7 +54,7 @@ export type CollisionArrays = {
};

export type SymbolFeature = {|
text: string | void,
text: string | Formatted | void,
icon: string | void,
index: number,
sourceLayerIndex: number,
Expand Down Expand Up @@ -328,6 +329,18 @@ class SymbolBucket implements Bucket {
this.lineVertexArray = new SymbolLineVertexArray();
}

calculateGlyphDependencies(text: string, stack: {[number]: boolean}, textAlongLine: boolean, doesAllowVerticalWritingMode: boolean) {
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = true;
if (textAlongLine && doesAllowVerticalWritingMode) {
const verticalChar = verticalizedCharacterMap[text.charAt(i)];
if (verticalChar) {
stack[verticalChar.charCodeAt(0)] = true;
}
}
}
}

populate(features: Array<IndexedFeature>, options: PopulateParameters) {
const layer = this.layers[0];
const layout = layer.layout;
Expand All @@ -336,7 +349,7 @@ class SymbolBucket implements Bucket {
const textField = layout.get('text-field');
const iconImage = layout.get('icon-image');
const hasText =
(textField.value.kind !== 'constant' || textField.value.value.length > 0) &&
(textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) &&
(textFont.value.kind !== 'constant' || textFont.value.value.length > 0);
const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0;

Expand Down Expand Up @@ -392,16 +405,18 @@ class SymbolBucket implements Bucket {
const fontStack = textFont.evaluate(feature, {}).join(',');
const stack = stacks[fontStack] = stacks[fontStack] || {};
const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
for (let i = 0; i < text.length; i++) {
stack[text.charCodeAt(i)] = true;
if (textAlongLine && doesAllowVerticalWritingMode) {
const verticalChar = verticalizedCharacterMap[text.charAt(i)];
if (verticalChar) {
stack[verticalChar.charCodeAt(0)] = true;
}
if (text instanceof Formatted) {
for (const section of text.sections) {
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
const sectionFont = section.fontStack || fontStack;
const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {};
this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode);
}
} else {
const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
this.calculateGlyphDependencies(text, stack, textAlongLine, doesAllowVerticalWritingMode);
}

}
}

Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Expand Up @@ -96,7 +96,7 @@ const exported = {
* @param {string} pluginURL URL pointing to the Mapbox RTL text plugin source.
* @param {Function} callback Called with an error argument if there is an error.
* @example
* mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.2/mapbox-gl-rtl-text.js');
* mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js');
* @see [Add support for right-to-left scripts](https://www.mapbox.com/mapbox-gl-js/example/mapbox-gl-rtl-text/)
*/

Expand Down
2 changes: 2 additions & 0 deletions src/source/rtl_text_plugin.js
Expand Up @@ -54,10 +54,12 @@ export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) {
export const plugin: {
applyArabicShaping: ?Function,
processBidirectionalText: ?(string, Array<number>) => Array<string>,
processStyledBidirectionalText: ?(string, Array<number>, Array<number>) => Array<[string, Array<number>]>,
isLoaded: () => boolean
} = {
applyArabicShaping: null,
processBidirectionalText: null,
processStyledBidirectionalText: null,
isLoaded: function() {
return foregroundLoadComplete || // Foreground: loaded if the completion callback returned successfully
plugin.applyArabicShaping != null; // Background: loaded if the plugin functions have been compiled
Expand Down
4 changes: 2 additions & 2 deletions src/source/worker.js
Expand Up @@ -55,12 +55,13 @@ export default class Worker {
this.workerSourceTypes[name] = WorkerSource;
};

this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function}) => {
this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => {
if (globalRTLTextPlugin.isLoaded()) {
throw new Error('RTL text plugin already registered.');
}
globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping;
globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText;
globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText;
};
}

Expand Down Expand Up @@ -197,4 +198,3 @@ if (typeof WorkerGlobalScope !== 'undefined' &&
self instanceof WorkerGlobalScope) {
new Worker(self);
}

53 changes: 33 additions & 20 deletions src/style-spec/expression/compound_expression.js
Expand Up @@ -67,29 +67,36 @@ class CompoundExpression implements Expression {
signature.length === args.length - 1 // correct param count
));

// First parse all the args
const parsedArgs: Array<Expression> = [];
for (let i = 1; i < args.length; i++) {
const arg = args[i];
let expected;
if (overloads.length === 1) {
const params = overloads[0][0];
expected = Array.isArray(params) ?
params[i - 1] :
params.type;
}
const parsed = context.parse(arg, 1 + parsedArgs.length, expected);
if (!parsed) return null;
parsedArgs.push(parsed);
}

let signatureContext: ParsingContext = (null: any);

for (const [params, evaluate] of overloads) {
// Use a fresh context for each attempted signature so that, if
// we eventually succeed, we haven't polluted `context.errors`.
signatureContext = new ParsingContext(context.registry, context.path, null, context.scope);

// First parse all the args, potentially coercing to the
// types expected by this overload.
const parsedArgs: Array<Expression> = [];
let argParseFailed = false;
for (let i = 1; i < args.length; i++) {
const arg = args[i];
const expectedType = Array.isArray(params) ?
params[i - 1] :
params.type;

const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType);
if (!parsed) {
argParseFailed = true;
break;
}
parsedArgs.push(parsed);
}
if (argParseFailed) {
// Couldn't coerce args of this overload to expected type, move
// on to next one.
continue;
}

if (Array.isArray(params)) {
if (params.length !== parsedArgs.length) {
signatureContext.error(`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`);
Expand Down Expand Up @@ -117,10 +124,16 @@ class CompoundExpression implements Expression {
const signatures = expected
.map(([params]) => stringifySignature(params))
.join(' | ');
const actualTypes = parsedArgs
.map(arg => toString(arg.type))
.join(', ');
context.error(`Expected arguments of type ${signatures}, but found (${actualTypes}) instead.`);

const actualTypes = [];
// For error message, re-parse arguments without trying to
// apply any coercions
for (let i = 1; i < args.length; i++) {
const parsed = context.parse(args[i], 1 + actualTypes.length);
if (!parsed) return null;
actualTypes.push(toString(parsed.type));
}
context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`);
}

return null;
Expand Down
10 changes: 10 additions & 0 deletions src/style-spec/expression/definitions/coercion.js
Expand Up @@ -10,6 +10,7 @@ import type { Expression } from '../expression';
import type ParsingContext from '../parsing_context';
import type EvaluationContext from '../evaluation_context';
import type { Type } from '../types';
import { Formatted, FormattedSection } from './formatted';

const types = {
'to-number': NumberType,
Expand Down Expand Up @@ -73,6 +74,15 @@ class Coercion implements Expression {
}
}
throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
} else if (this.type.kind === 'formatted') {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
if (typeof input === 'string') {
return new Formatted([new FormattedSection(input, null, null)]);
}
}
throw new RuntimeError(`Could not parse formatted text from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
} else {
let value = null;
for (const arg of this.args) {
Expand Down