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

Add support for rendering CJK in a vertical writing mode along line-placed features #3438

Merged
merged 4 commits into from
Nov 10, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 48 additions & 22 deletions js/data/bucket/symbol_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ const resolveText = require('../../symbol/resolve_text');
const mergeLines = require('../../symbol/mergelines');
const clipLine = require('../../symbol/clip_line');
const util = require('../../util/util');
const scriptDetection = require('../../util/script_detection');
const loadGeometry = require('../load_geometry');
const CollisionFeature = require('../../symbol/collision_feature');
const findPoleOfInaccessibility = require('../../util/find_pole_of_inaccessibility');
const classifyRings = require('../../util/classify_rings');

const shapeText = Shaping.shapeText;
const shapeIcon = Shaping.shapeIcon;
const WritingMode = Shaping.WritingMode;
const getGlyphQuads = Quads.getGlyphQuads;
const getIconQuads = Quads.getIconQuads;

Expand Down Expand Up @@ -271,12 +273,20 @@ class SymbolBucket {
const spacing = layout['text-letter-spacing'] * oneEm;
const textOffset = [layout['text-offset'][0] * oneEm, layout['text-offset'][1] * oneEm];
const fontstack = this.fontstack = layout['text-font'].join(',');
const textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line';

for (const feature of this.features) {
let shapedText;

let shapedTextOrientations;
if (feature.text) {
shapedText = shapeText(feature.text, stacks[fontstack], maxWidth,
lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset);
const allowsVerticalWritingMode = scriptDetection.allowsVerticalWritingMode(feature.text);

shapedTextOrientations = {
[WritingMode.horizontal]: shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.horizontal),
[WritingMode.vertical]: allowsVerticalWritingMode && textAlongLine && shapeText(feature.text, stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset, oneEm, WritingMode.vertical)
};
} else {
shapedTextOrientations = {};
}

let shapedIcon;
Expand All @@ -298,14 +308,14 @@ class SymbolBucket {
}
}

if (shapedText || shapedIcon) {
this.addFeature(feature, shapedText, shapedIcon);
if (shapedTextOrientations[WritingMode.horizontal] || shapedIcon) {
this.addFeature(feature, shapedTextOrientations, shapedIcon);
}
}
this.symbolInstancesEndIndex = this.symbolInstancesArray.length;
}

addFeature(feature, shapedText, shapedIcon) {
addFeature(feature, shapedTextOrientations, shapedIcon) {
const lines = feature.geometry;
const layout = this.layers[0].layout;

Expand Down Expand Up @@ -350,7 +360,7 @@ class SymbolBucket {
line,
symbolMinDistance,
textMaxAngle,
shapedText,
shapedTextOrientations[WritingMode.vertical] || shapedTextOrientations[WritingMode.horizontal],
shapedIcon,
glyphSize,
textMaxBoxScale,
Expand All @@ -369,8 +379,8 @@ class SymbolBucket {
for (let j = 0, len = anchors.length; j < len; j++) {
const anchor = anchors[j];

if (shapedText && isLine) {
if (this.anchorIsTooClose(shapedText.text, textRepeatDistance, anchor)) {
if (shapedTextOrientations[WritingMode.horizontal] && isLine) {
if (this.anchorIsTooClose(shapedTextOrientations[WritingMode.horizontal].text, textRepeatDistance, anchor)) {
continue;
}
}
Expand All @@ -389,7 +399,7 @@ class SymbolBucket {
// be drawn across tile boundaries. Instead they need to be included in
// the buffers for both tiles and clipped to tile boundaries at draw time.
const addToBuffers = inside || mayOverlap;
this.addSymbolInstance(anchor, line, shapedText, shapedIcon, this.layers[0],
this.addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, this.layers[0],
addToBuffers, this.symbolInstancesArray.length, this.collisionBoxArray, feature.index, feature.sourceLayerIndex, this.index,
textBoxScale, textPadding, textAlongLine,
iconBoxScale, iconPadding, iconAlongLine, {zoom: this.zoom}, feature.properties);
Expand Down Expand Up @@ -516,7 +526,7 @@ class SymbolBucket {
if (hasText) {
collisionTile.insertCollisionFeature(textCollisionFeature, glyphScale, layout['text-ignore-placement']);
if (glyphScale <= maxScale) {
this.addSymbols(this.arrays.glyph, symbolInstance.glyphQuadStartIndex, symbolInstance.glyphQuadEndIndex, glyphScale, layout['text-keep-upright'], textAlongLine, collisionTile.angle);
this.addSymbols(this.arrays.glyph, symbolInstance.glyphQuadStartIndex, symbolInstance.glyphQuadEndIndex, glyphScale, layout['text-keep-upright'], textAlongLine, collisionTile.angle, symbolInstance.writingModes);
}
}

Expand All @@ -532,7 +542,7 @@ class SymbolBucket {
if (showCollisionBoxes) this.addToDebugBuffers(collisionTile);
}

addSymbols(arrays, quadsStart, quadsEnd, scale, keepUpright, alongLine, placementAngle) {
addSymbols(arrays, quadsStart, quadsEnd, scale, keepUpright, alongLine, placementAngle, writingModes) {
const elementArray = arrays.elementArray;
const layoutVertexArray = arrays.layoutVertexArray;

Expand All @@ -543,9 +553,13 @@ class SymbolBucket {

const symbol = this.symbolQuadsArray.get(k).SymbolQuad;

// drop upside down versions of glyphs
// drop incorrectly oriented glyphs
const a = (symbol.anchorAngle + placementAngle + Math.PI) % (Math.PI * 2);
if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue;
if (writingModes & WritingMode.vertical) {
if (alongLine && symbol.writingMode === WritingMode.vertical) {
if (keepUpright && alongLine && a <= (Math.PI * 5 / 4) || a > (Math.PI * 7 / 4)) continue;
} else if (keepUpright && alongLine && a <= (Math.PI * 3 / 4) || a > (Math.PI * 5 / 4)) continue;
} else if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue;

const tl = symbol.tl,
tr = symbol.tr,
Expand Down Expand Up @@ -630,14 +644,17 @@ class SymbolBucket {
}
}

addSymbolInstance(anchor, line, shapedText, shapedIcon, layer, addToBuffers, index, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex,
addSymbolInstance(anchor, line, shapedTextOrientations, shapedIcon, layer, addToBuffers, index, collisionBoxArray, featureIndex, sourceLayerIndex, bucketIndex,
textBoxScale, textPadding, textAlongLine,
iconBoxScale, iconPadding, iconAlongLine, globalProperties, featureProperties) {

let textCollisionFeature, iconCollisionFeature, glyphQuads, iconQuads;
if (shapedText) {
glyphQuads = addToBuffers ? getGlyphQuads(anchor, shapedText, textBoxScale, line, layer, textAlongLine) : [];
textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedText, textBoxScale, textPadding, textAlongLine, false);
let textCollisionFeature, iconCollisionFeature, iconQuads;
let glyphQuads = [];
for (const writingModeString in shapedTextOrientations) {
const writingMode = parseInt(writingModeString, 10);
if (!shapedTextOrientations[writingMode]) continue;
glyphQuads = glyphQuads.concat(addToBuffers ? getGlyphQuads(anchor, shapedTextOrientations[writingMode], textBoxScale, line, layer, textAlongLine, writingMode) : []);
textCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedTextOrientations[writingMode], textBoxScale, textPadding, textAlongLine, false);
}

const glyphQuadStartIndex = this.symbolQuadsArray.length;
Expand All @@ -652,7 +669,7 @@ class SymbolBucket {
const textBoxEndIndex = textCollisionFeature ? textCollisionFeature.boxEndIndex : this.collisionBoxArray.length;

if (shapedIcon) {
iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, iconBoxScale, line, layer, iconAlongLine, shapedText, globalProperties, featureProperties) : [];
iconQuads = addToBuffers ? getIconQuads(anchor, shapedIcon, iconBoxScale, line, layer, iconAlongLine, shapedTextOrientations[WritingMode.horizontal], globalProperties, featureProperties) : [];
iconCollisionFeature = new CollisionFeature(collisionBoxArray, line, anchor, featureIndex, sourceLayerIndex, bucketIndex, shapedIcon, iconBoxScale, iconPadding, iconAlongLine, true);
}

Expand All @@ -667,6 +684,11 @@ class SymbolBucket {
if (iconQuadEndIndex > SymbolBucket.MAX_QUADS) util.warnOnce("Too many symbols being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907");
if (glyphQuadEndIndex > SymbolBucket.MAX_QUADS) util.warnOnce("Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907");

const writingModes = (
(shapedTextOrientations[WritingMode.vertical] ? WritingMode.vertical : 0) |
(shapedTextOrientations[WritingMode.horizontal] ? WritingMode.horizontal : 0)
);

return this.symbolInstancesArray.emplaceBack(
textBoxStartIndex,
textBoxEndIndex,
Expand All @@ -678,7 +700,9 @@ class SymbolBucket {
iconQuadEndIndex,
anchor.x,
anchor.y,
index);
index,
writingModes
);
}

addSymbolQuad(symbolQuad) {
Expand All @@ -705,7 +729,9 @@ class SymbolBucket {
symbolQuad.glyphAngle,
// scales
symbolQuad.maxScale,
symbolQuad.minScale);
symbolQuad.minScale,
// writing mode
symbolQuad.writingMode);
}
}

Expand Down
16 changes: 12 additions & 4 deletions js/symbol/glyph_source.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const normalizeURL = require('../util/mapbox').normalizeGlyphsURL;
const ajax = require('../util/ajax');
const verticalizePunctuation = require('../util/verticalize_punctuation');
const Glyphs = require('../util/glyphs');
const GlyphAtlas = require('../symbol/glyph_atlas');
const Protobuf = require('pbf');
Expand Down Expand Up @@ -52,11 +53,9 @@ class GlyphSource {

const missing = {};
let remaining = 0;
let range;

for (let i = 0; i < glyphIDs.length; i++) {
const glyphID = glyphIDs[i];
range = Math.floor(glyphID / 256);
const getGlyph = (glyphID) => {
const range = Math.floor(glyphID / 256);

if (stack[range]) {
const glyph = stack[range].glyphs[glyphID];
Expand All @@ -69,6 +68,15 @@ class GlyphSource {
}
missing[range].push(glyphID);
}
};

for (let i = 0; i < glyphIDs.length; i++) {
const glyphID = glyphIDs[i];
const string = String.fromCodePoint(glyphID);
getGlyph(glyphID);
if (verticalizePunctuation.lookup[string]) {
getGlyph(verticalizePunctuation.lookup[string].codePointAt(0));
}
}

if (!remaining) callback(undefined, glyphs, fontstack);
Expand Down
30 changes: 20 additions & 10 deletions js/symbol/quads.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const minScale = 0.5; // underscale by 1 zoom level
* @class SymbolQuad
* @private
*/
function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale) {
function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, minScale, maxScale, writingMode) {
this.anchorPoint = anchorPoint;
this.tl = tl;
this.tr = tr;
Expand All @@ -40,6 +40,7 @@ function SymbolQuad(anchorPoint, tl, tr, bl, br, tex, anchorAngle, glyphAngle, m
this.glyphAngle = glyphAngle;
this.minScale = minScale;
this.maxScale = maxScale;
this.writingMode = writingMode;
}

/**
Expand Down Expand Up @@ -171,15 +172,24 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine) {
}];
}

const x1 = positionedGlyph.x + glyph.left,
y1 = positionedGlyph.y - glyph.top,
x2 = x1 + rect.w,
y2 = y1 + rect.h,
const x1 = positionedGlyph.x + glyph.left;
const y1 = positionedGlyph.y - glyph.top;
const x2 = x1 + rect.w;
const y2 = y1 + rect.h;

otl = new Point(x1, y1),
otr = new Point(x2, y1),
obl = new Point(x1, y2),
obr = new Point(x2, y2);
const center = new Point(positionedGlyph.x, glyph.advance / 2);

const otl = new Point(x1, y1);
const otr = new Point(x2, y1);
const obl = new Point(x1, y2);
const obr = new Point(x2, y2);

if (positionedGlyph.angle !== 0) {
otl._sub(center)._rotate(positionedGlyph.angle)._add(center);
otr._sub(center)._rotate(positionedGlyph.angle)._add(center);
obl._sub(center)._rotate(positionedGlyph.angle)._add(center);
obr._sub(center)._rotate(positionedGlyph.angle)._add(center);
}

for (let i = 0; i < glyphInstances.length; i++) {

Expand All @@ -205,7 +215,7 @@ function getGlyphQuads(anchor, shaping, boxScale, line, layer, alongLine) {

const anchorAngle = (anchor.angle + instance.offset + 2 * Math.PI) % (2 * Math.PI);
const glyphAngle = (instance.angle + instance.offset + 2 * Math.PI) % (2 * Math.PI);
quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale));
quads.push(new SymbolQuad(instance.anchorPoint, tl, tr, bl, br, rect, anchorAngle, glyphAngle, glyphMinScale, instance.maxScale, shaping.writingMode));
}
}

Expand Down
41 changes: 27 additions & 14 deletions js/symbol/shaping.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,75 @@
'use strict';

const scriptDetection = require('../util/script_detection');
const verticalizePunctuation = require('../util/verticalize_punctuation');


const WritingMode = {
horizontal: 1,
vertical: 2
};

module.exports = {
shapeText: shapeText,
shapeIcon: shapeIcon
shapeIcon: shapeIcon,
WritingMode: WritingMode
};


// The position of a glyph relative to the text's anchor point.
function PositionedGlyph(codePoint, x, y, glyph) {
function PositionedGlyph(codePoint, x, y, glyph, angle) {
this.codePoint = codePoint;
this.x = x;
this.y = y;
this.glyph = glyph || null;
this.angle = angle;
}

// A collection of positioned glyphs and some metadata
function Shaping(positionedGlyphs, text, top, bottom, left, right) {
function Shaping(positionedGlyphs, text, top, bottom, left, right, writingMode) {
this.positionedGlyphs = positionedGlyphs;
this.text = text;
this.top = top;
this.bottom = bottom;
this.left = left;
this.right = right;
this.writingMode = writingMode;
}

const newLine = 0x0a;

function shapeText(text, glyphs, maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, translate) {
function shapeText(text, glyphs, maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, translate, verticalHeight, writingMode) {

text = text.trim();
if (writingMode === WritingMode.vertical) text = verticalizePunctuation(text);

const positionedGlyphs = [];
const shaping = new Shaping(positionedGlyphs, text, translate[1], translate[1], translate[0], translate[0]);
const shaping = new Shaping(positionedGlyphs, text, translate[1], translate[1], translate[0], translate[0], writingMode);

// the y offset *should* be part of the font metadata
const yOffset = -17;

let x = 0;
const y = yOffset;

text = text.trim();

for (let i = 0; i < text.length; i++) {
const codePoint = text.charCodeAt(i);
const glyph = glyphs[codePoint];

if (!glyph && codePoint !== newLine) continue;

positionedGlyphs.push(new PositionedGlyph(codePoint, x, y, glyph));
if (!scriptDetection.charAllowsVerticalWritingMode(codePoint) || writingMode === WritingMode.horizontal) {
positionedGlyphs.push(new PositionedGlyph(codePoint, x, yOffset, glyph, 0));
if (glyph) x += glyph.advance + spacing;

if (glyph) {
x += glyph.advance + spacing;
} else {
positionedGlyphs.push(new PositionedGlyph(codePoint, x, 0, glyph, -Math.PI / 2));
if (glyph) x += verticalHeight + spacing;
}
}

if (!positionedGlyphs.length) return false;

linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, scriptDetection.allowsIdeographicBreaking(text));
linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, scriptDetection.allowsIdeographicBreaking(text), writingMode);

return shaping;
}
Expand All @@ -81,7 +94,7 @@ const breakable = {

invisible[newLine] = breakable[newLine] = true;

function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, useBalancedIdeographicBreaking) {
function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, verticalAlign, justify, translate, useBalancedIdeographicBreaking, writingMode) {
let lastSafeBreak = null;
let lengthBeforeCurrentLine = 0;
let lineStartIndex = 0;
Expand All @@ -91,7 +104,7 @@ function linewrap(shaping, glyphs, lineHeight, maxWidth, horizontalAlign, vertic

const positionedGlyphs = shaping.positionedGlyphs;

if (maxWidth) {
if (writingMode === WritingMode.horizontal && maxWidth) {
if (useBalancedIdeographicBreaking) {
const lastPositionedGlyph = positionedGlyphs[positionedGlyphs.length - 1];
const estimatedLineCount = Math.max(1, Math.ceil(lastPositionedGlyph.x / maxWidth));
Expand Down
Loading