From ee1d53e2701c450ada7a468c63470a3d4d5cdd88 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sat, 29 Apr 2023 12:22:07 +0200 Subject: [PATCH 01/15] Use strings for unicode numbers --- src/data/bucket/symbol_bucket.ts | 4 ++-- src/render/glyph_manager.ts | 4 ++++ src/source/worker_tile.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index f20f5838c1..0df961551b 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -417,11 +417,11 @@ class SymbolBucket implements Bucket { calculateGlyphDependencies(text: string, stack: {[_: number]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { for (let i = 0; i < text.length; i++) { - stack[text.charCodeAt(i)] = true; + stack[text.charCodeAt(i).toString()] = true; if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { const verticalChar = verticalizedCharacterMap[text.charAt(i)]; if (verticalChar) { - stack[verticalChar.charCodeAt(0)] = true; + stack[verticalChar.charCodeAt(0).toString()] = true; } } } diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 4e927aab48..0e9bb3e3f2 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -83,6 +83,9 @@ export default class GlyphManager { } glyph = this._tinySDF(entry, stack, id); + + console.log('glyph_manager', glyph); + if (glyph) { entry.glyphs[id] = glyph; callback(null, {stack, id, glyph}); @@ -161,6 +164,7 @@ export default class GlyphManager { } _doesCharSupportLocalGlyph(id: number): boolean { + return true; /* eslint-disable new-cap */ return !!this.localIdeographFontFamily && (isChar['CJK Unified Ideographs'](id) || diff --git a/src/source/worker_tile.ts b/src/source/worker_tile.ts index 3675a97a64..35d3629968 100644 --- a/src/source/worker_tile.ts +++ b/src/source/worker_tile.ts @@ -138,7 +138,7 @@ class WorkerTile { let iconMap: {[_: string]: StyleImage}; let patternMap: {[_: string]: StyleImage}; - const stacks = mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(Number)); + const stacks = mapObject(options.glyphDependencies, (glyphs) => Object.keys(glyphs).map(String)); if (Object.keys(stacks).length) { actor.send('getGlyphs', {uid: this.uid, stacks, source: this.source, tileID: this.tileID, type: 'glyphs'}, (err, result) => { if (!error) { From a5d4fcc803ff85e6fc33526ac4de2057660805c6 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sat, 29 Apr 2023 13:02:29 +0200 Subject: [PATCH 02/15] Add comments --- src/data/bucket/symbol_bucket.ts | 5 +- src/render/glyph_manager.ts | 4 +- src/symbol/shaping.ts | 108 +++++++++++++++------------- src/util/script_detection.ts | 5 ++ src/util/util.ts | 2 + src/util/verticalize_punctuation.ts | 2 + 6 files changed, 74 insertions(+), 52 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 0df961551b..cba56695a2 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -415,12 +415,15 @@ class SymbolBucket implements Bucket { this.symbolInstances = new SymbolInstanceArray(); } - calculateGlyphDependencies(text: string, stack: {[_: number]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { + calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { for (let i = 0; i < text.length; i++) { + // OLIVER stack[text.charCodeAt(i).toString()] = true; if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { + // OLIVER const verticalChar = verticalizedCharacterMap[text.charAt(i)]; if (verticalChar) { + // OLIVER stack[verticalChar.charCodeAt(0).toString()] = true; } } diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 0e9bb3e3f2..688d51243f 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -84,7 +84,7 @@ export default class GlyphManager { glyph = this._tinySDF(entry, stack, id); - console.log('glyph_manager', glyph); + // console.log('glyph_manager', glyph); if (glyph) { entry.glyphs[id] = glyph; @@ -204,6 +204,8 @@ export default class GlyphManager { }); } + //console.log('tinysdf', id, typeof id); + // OLIVER const char = tinySDF.draw(String.fromCharCode(id)); /** diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index dc87f31522..622e06d4f0 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -141,6 +141,7 @@ class TaggedString { } getCharCode(index: number): number { + // OLIVER return this.text.charCodeAt(index); } @@ -151,12 +152,14 @@ class TaggedString { trim() { let beginningWhitespace = 0; for (let i = 0; + // OLIVER i < this.text.length && whitespace[this.text.charCodeAt(i)]; i++) { beginningWhitespace++; } let trailingWhitespace = this.text.length; for (let i = this.text.length - 1; + // OLIVER i >= 0 && i >= beginningWhitespace && whitespace[this.text.charCodeAt(i)]; i--) { trailingWhitespace--; @@ -203,6 +206,7 @@ class TaggedString { return; } + // OLIVER this.text += String.fromCharCode(nextImageSectionCharCode); this.sections.push(SectionOptions.forImage(imageName)); this.sectionIndex.push(this.sections.length - 1); @@ -382,6 +386,7 @@ function getGlyphAdvance( } } +// safe to ignore this fucntion function determineAverageLineWidth(logicalInput: TaggedString, spacing: number, maxWidth: number, @@ -505,50 +510,52 @@ function determineLineBreaks( symbolPlacement: string, layoutTextSize: number ): Array { - if (symbolPlacement !== 'point') - return []; - - if (!logicalInput) - return []; - - const potentialLineBreaks = []; - const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); - - const hasServerSuggestedBreakpoints = logicalInput.text.indexOf('\u200b') >= 0; - - let currentX = 0; - - for (let i = 0; i < logicalInput.length(); i++) { - const section = logicalInput.getSection(i); - const codePoint = logicalInput.getCharCode(i); - if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint, section, glyphMap, imagePositions, spacing, layoutTextSize); - - // Ideographic characters, spaces, and word-breaking punctuation that often appear without - // surrounding spaces. - if ((i < logicalInput.length() - 1)) { - const ideographicBreak = charAllowsIdeographicBreaking(codePoint); - if (breakable[codePoint] || ideographicBreak || section.imageName) { - - potentialLineBreaks.push( - evaluateBreak( - i + 1, - currentX, - targetWidth, - potentialLineBreaks, - calculatePenalty(codePoint, logicalInput.getCharCode(i + 1), ideographicBreak && hasServerSuggestedBreakpoints), - false)); - } - } - } - - return leastBadBreaks( - evaluateBreak( - logicalInput.length(), - currentX, - targetWidth, - potentialLineBreaks, - 0, - true)); + return []; + + // if (symbolPlacement !== 'point') + // return []; + + // if (!logicalInput) + // return []; + + // const potentialLineBreaks = []; + // const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); + + // const hasServerSuggestedBreakpoints = logicalInput.text.indexOf('\u200b') >= 0; + + // let currentX = 0; + + // for (let i = 0; i < logicalInput.length(); i++) { + // const section = logicalInput.getSection(i); + // const codePoint = logicalInput.getCharCode(i); + // if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint, section, glyphMap, imagePositions, spacing, layoutTextSize); + + // // Ideographic characters, spaces, and word-breaking punctuation that often appear without + // // surrounding spaces. + // if ((i < logicalInput.length() - 1)) { + // const ideographicBreak = charAllowsIdeographicBreaking(codePoint); + // if (breakable[codePoint] || ideographicBreak || section.imageName) { + + // potentialLineBreaks.push( + // evaluateBreak( + // i + 1, + // currentX, + // targetWidth, + // potentialLineBreaks, + // calculatePenalty(codePoint, logicalInput.getCharCode(i + 1), ideographicBreak && hasServerSuggestedBreakpoints), + // false)); + // } + // } + // } + + // return leastBadBreaks( + // evaluateBreak( + // logicalInput.length(), + // currentX, + // targetWidth, + // potentialLineBreaks, + // 0, + // true)); } function getAnchorAlignment(anchor: SymbolAnchor) { @@ -640,12 +647,13 @@ function shapeLines(shaping: Shaping, let rect = null; let imageName = null; let verticalAdvance = ONE_EM; - const vertical = !(writingMode === WritingMode.horizontal || - // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. - (!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) || - // If vertical placement is enabled, don't verticalize glyphs that - // are from complex text layout script, or whitespaces. - (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); + // const vertical = !(writingMode === WritingMode.horizontal || + // // Don't verticalize glyphs that have no upright orientation if vertical placement is disabled. + // (!allowVerticalPlacement && !charHasUprightVerticalOrientation(codePoint)) || + // // If vertical placement is enabled, don't verticalize glyphs that + // // are from complex text layout script, or whitespaces. + // (allowVerticalPlacement && (whitespace[codePoint] || charInComplexShapingScript(codePoint)))); + const vertical = false; if (!section.imageName) { const positions = glyphPositions[section.fontStack]; diff --git a/src/util/script_detection.ts b/src/util/script_detection.ts index a1c143f9b2..25faf19765 100644 --- a/src/util/script_detection.ts +++ b/src/util/script_detection.ts @@ -4,6 +4,7 @@ import isChar from './is_char_in_unicode_block'; export function allowsIdeographicBreaking(chars: string) { for (const char of chars) { + // OLIVER if (!charAllowsIdeographicBreaking(char.charCodeAt(0))) return false; } return true; @@ -11,6 +12,7 @@ export function allowsIdeographicBreaking(chars: string) { export function allowsVerticalWritingMode(chars: string) { for (const char of chars) { + // OLIVER if (charHasUprightVerticalOrientation(char.charCodeAt(0))) return true; } return false; @@ -18,6 +20,7 @@ export function allowsVerticalWritingMode(chars: string) { export function allowsLetterSpacing(chars: string) { for (const char of chars) { + // OLIVER if (!charAllowsLetterSpacing(char.charCodeAt(0))) return false; } return true; @@ -309,6 +312,7 @@ export function charInSupportedScript(char: number, canRenderRTL: boolean) { export function stringContainsRTLText(chars: string): boolean { for (const char of chars) { + // OLIVER if (charInRTLScript(char.charCodeAt(0))) { return true; } @@ -318,6 +322,7 @@ export function stringContainsRTLText(chars: string): boolean { export function isStringInSupportedScript(chars: string, canRenderRTL: boolean) { for (const char of chars) { + // OLIVER if (!charInSupportedScript(char.charCodeAt(0), canRenderRTL)) { return false; } diff --git a/src/util/util.ts b/src/util/util.ts index dc7d3aed53..bb839e175a 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -517,6 +517,7 @@ export function b64EncodeUnicode(str: string) { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { + // OLIVER return String.fromCharCode(Number('0x' + p1)); //eslint-disable-line } ) @@ -526,6 +527,7 @@ export function b64EncodeUnicode(str: string) { // Unicode compliant decoder for base64-encoded strings export function b64DecodeUnicode(str: string) { return decodeURIComponent(atob(str).split('').map((c) => { + // OLIVER return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); //eslint-disable-line }).join('')); } diff --git a/src/util/verticalize_punctuation.ts b/src/util/verticalize_punctuation.ts index f7d05d69c1..9dc48416f9 100644 --- a/src/util/verticalize_punctuation.ts +++ b/src/util/verticalize_punctuation.ts @@ -90,7 +90,9 @@ export default function verticalizePunctuation(input: string) { let output = ''; for (let i = 0; i < input.length; i++) { + // OLIVER const nextCharCode = input.charCodeAt(i + 1) || null; + // OLIVER const prevCharCode = input.charCodeAt(i - 1) || null; const canReplacePunctuation = ( From fc05d73ca4442fe094e35dca9bc7f49db9b0dff2 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sat, 29 Apr 2023 13:52:39 +0200 Subject: [PATCH 03/15] Step 1: Use strings as indices --- src/data/bucket/symbol_bucket.ts | 6 ++-- src/render/glyph_atlas.ts | 7 +++-- src/render/glyph_manager.ts | 5 ++-- src/symbol/shaping.ts | 50 +++++++++++++++++--------------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index cba56695a2..400839363b 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -418,13 +418,15 @@ class SymbolBucket implements Bucket { calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { for (let i = 0; i < text.length; i++) { // OLIVER - stack[text.charCodeAt(i).toString()] = true; + // stack[text.charCodeAt(i).toString()] = true; + stack[text[i]] = true; if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { // OLIVER const verticalChar = verticalizedCharacterMap[text.charAt(i)]; if (verticalChar) { // OLIVER - stack[verticalChar.charCodeAt(0).toString()] = true; + // stack[verticalChar.charCodeAt(0).toString()] = true; + stack[verticalChar[i]] = true; } } } diff --git a/src/render/glyph_atlas.ts b/src/render/glyph_atlas.ts index a0afc79641..efbf71f5a2 100644 --- a/src/render/glyph_atlas.ts +++ b/src/render/glyph_atlas.ts @@ -36,12 +36,14 @@ export default class GlyphAtlas { const positions = {}; const bins = []; + console.log('glyph_atlas', stacks); for (const stack in stacks) { const glyphs = stacks[stack]; const stackPositions = positions[stack] = {}; for (const id in glyphs) { - const src = glyphs[+id]; + //const src = glyphs[+id]; + const src = glyphs[id]; if (!src || src.bitmap.width === 0 || src.bitmap.height === 0) continue; const bin = { @@ -62,7 +64,8 @@ export default class GlyphAtlas { const glyphs = stacks[stack]; for (const id in glyphs) { - const src = glyphs[+id]; + // const src = glyphs[+id]; + const src = glyphs[id]; if (!src || src.bitmap.width === 0 || src.bitmap.height === 0) continue; const bin = positions[stack][id].rect; AlphaImage.copy(src.bitmap, image, {x: 0, y: 0}, {x: bin.x + padding, y: bin.y + padding}, src.bitmap); diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 688d51243f..3c7ba60be9 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -204,9 +204,10 @@ export default class GlyphManager { }); } - //console.log('tinysdf', id, typeof id); + console.log('tinysdf', id, typeof id); // OLIVER - const char = tinySDF.draw(String.fromCharCode(id)); + // const char = tinySDF.draw(String.fromCharCode(id)); + const char = tinySDF.draw(id as any); /** * TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph. diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 622e06d4f0..a9a97384a6 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -140,9 +140,10 @@ class TaggedString { return this.sectionIndex[index]; } - getCharCode(index: number): number { + getCharCode(index: number): string { // OLIVER - return this.text.charCodeAt(index); + // return this.text.charCodeAt(index); + return this.text[index]; } verticalizePunctuation() { @@ -320,6 +321,7 @@ function shapeText( verticalizable: false }; + // console.log('shaping', 'glyphMap', glyphMap); shapeLines(shaping, glyphMap, glyphPositions, imagePositions, lines, lineHeight, textAnchor, textJustify, writingMode, spacing, allowVerticalPlacement, layoutTextSizeThisZoom); if (isEmpty(positionedLines)) return false; @@ -387,26 +389,26 @@ function getGlyphAdvance( } // safe to ignore this fucntion -function determineAverageLineWidth(logicalInput: TaggedString, - spacing: number, - maxWidth: number, - glyphMap: { - [_: string]: { - [_: number]: StyleGlyph; - }; - }, - imagePositions: {[_: string]: ImagePosition}, - layoutTextSize: number) { - let totalWidth = 0; - - for (let index = 0; index < logicalInput.length(); index++) { - const section = logicalInput.getSection(index); - totalWidth += getGlyphAdvance(logicalInput.getCharCode(index), section, glyphMap, imagePositions, spacing, layoutTextSize); - } - - const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); - return totalWidth / lineCount; -} +// function determineAverageLineWidth(logicalInput: TaggedString, +// spacing: number, +// maxWidth: number, +// glyphMap: { +// [_: string]: { +// [_: number]: StyleGlyph; +// }; +// }, +// imagePositions: {[_: string]: ImagePosition}, +// layoutTextSize: number) { +// let totalWidth = 0; + +// for (let index = 0; index < logicalInput.length(); index++) { +// const section = logicalInput.getSection(index); +// totalWidth += getGlyphAdvance(logicalInput.getCharCode(index), section, glyphMap, imagePositions, spacing, layoutTextSize); +// } + +// const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); +// return totalWidth / lineCount; +// } function calculateBadness(lineWidth: number, targetWidth: number, @@ -641,7 +643,7 @@ function shapeLines(shaping: Shaping, for (let i = 0; i < line.length(); i++) { const section = line.getSection(i); const sectionIndex = line.getSectionIndex(i); - const codePoint = line.getCharCode(i); + const codePoint = line.getCharCode(i) as any; let baselineOffset = 0.0; let metrics = null; let rect = null; @@ -722,6 +724,8 @@ function shapeLines(shaping: Shaping, justifyLine(positionedGlyphs, 0, positionedGlyphs.length - 1, justify, lineOffset); } + // console.log('shaping', positionedGlyphs); + x = 0; const currentLineHeight = lineHeight * lineMaxScale + lineOffset; positionedLine.lineOffset = Math.max(lineOffset, maxLineOffset); From 94a059758ab44f829ebd3ae9fb9632e62dc881ee Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sat, 29 Apr 2023 22:29:58 +0200 Subject: [PATCH 04/15] Add example --- src/data/bucket/symbol_bucket.ts | 26 +++--- src/render/glyph_atlas.ts | 2 +- src/render/glyph_manager.ts | 2 +- src/symbol/shaping.ts | 150 ++++++++++++++++--------------- test/debug-pages/debug2.html | 87 ++++++++++++++++++ 5 files changed, 183 insertions(+), 84 deletions(-) create mode 100644 test/debug-pages/debug2.html diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 400839363b..d09ccc1f4d 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -416,19 +416,23 @@ class SymbolBucket implements Bucket { } calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { - for (let i = 0; i < text.length; i++) { + const segmenter = new Intl.Segmenter( + 'my', {granularity: 'grapheme'} + ); + const graphemes = Array.from(segmenter.segment(text), s => s.segment); + for (let i = 0; i < graphemes.length; i++) { // OLIVER // stack[text.charCodeAt(i).toString()] = true; - stack[text[i]] = true; - if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { - // OLIVER - const verticalChar = verticalizedCharacterMap[text.charAt(i)]; - if (verticalChar) { - // OLIVER - // stack[verticalChar.charCodeAt(0).toString()] = true; - stack[verticalChar[i]] = true; - } - } + stack[graphemes[i]] = true; + // if ((textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode) { + // // OLIVER + // const verticalChar = verticalizedCharacterMap[text.charAt(i)]; + // if (verticalChar) { + // // OLIVER + // // stack[verticalChar.charCodeAt(0).toString()] = true; + // stack[verticalChar[i]] = true; + // } + // } } } diff --git a/src/render/glyph_atlas.ts b/src/render/glyph_atlas.ts index efbf71f5a2..93315756df 100644 --- a/src/render/glyph_atlas.ts +++ b/src/render/glyph_atlas.ts @@ -36,7 +36,7 @@ export default class GlyphAtlas { const positions = {}; const bins = []; - console.log('glyph_atlas', stacks); + // console.log('glyph_atlas', stacks); for (const stack in stacks) { const glyphs = stacks[stack]; const stackPositions = positions[stack] = {}; diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 3c7ba60be9..4efad31565 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -204,7 +204,7 @@ export default class GlyphManager { }); } - console.log('tinysdf', id, typeof id); + // console.log('tinysdf', id, typeof id); // OLIVER // const char = tinySDF.draw(String.fromCharCode(id)); const char = tinySDF.draw(id as any); diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index a9a97384a6..e5aaa7d81d 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -343,8 +343,9 @@ const whitespace: { }; const breakable: { - [_: number]: boolean; + [_: number | string]: boolean; } = { + ['\n']: true, [0x0a]: true, // newline [0x20]: true, // space [0x26]: true, // ampersand @@ -389,26 +390,26 @@ function getGlyphAdvance( } // safe to ignore this fucntion -// function determineAverageLineWidth(logicalInput: TaggedString, -// spacing: number, -// maxWidth: number, -// glyphMap: { -// [_: string]: { -// [_: number]: StyleGlyph; -// }; -// }, -// imagePositions: {[_: string]: ImagePosition}, -// layoutTextSize: number) { -// let totalWidth = 0; - -// for (let index = 0; index < logicalInput.length(); index++) { -// const section = logicalInput.getSection(index); -// totalWidth += getGlyphAdvance(logicalInput.getCharCode(index), section, glyphMap, imagePositions, spacing, layoutTextSize); -// } - -// const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); -// return totalWidth / lineCount; -// } +function determineAverageLineWidth(logicalInput: TaggedString, + spacing: number, + maxWidth: number, + glyphMap: { + [_: string]: { + [_: number]: StyleGlyph; + }; + }, + imagePositions: {[_: string]: ImagePosition}, + layoutTextSize: number) { + let totalWidth = 0; + + for (let index = 0; index < logicalInput.length(); index++) { + const section = logicalInput.getSection(index); + totalWidth += getGlyphAdvance(logicalInput.getCharCode(index) as any, section, glyphMap, imagePositions, spacing, layoutTextSize); + } + + const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); + return totalWidth / lineCount; +} function calculateBadness(lineWidth: number, targetWidth: number, @@ -512,52 +513,51 @@ function determineLineBreaks( symbolPlacement: string, layoutTextSize: number ): Array { - return []; - - // if (symbolPlacement !== 'point') - // return []; - - // if (!logicalInput) - // return []; - - // const potentialLineBreaks = []; - // const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); - - // const hasServerSuggestedBreakpoints = logicalInput.text.indexOf('\u200b') >= 0; - - // let currentX = 0; - - // for (let i = 0; i < logicalInput.length(); i++) { - // const section = logicalInput.getSection(i); - // const codePoint = logicalInput.getCharCode(i); - // if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint, section, glyphMap, imagePositions, spacing, layoutTextSize); - - // // Ideographic characters, spaces, and word-breaking punctuation that often appear without - // // surrounding spaces. - // if ((i < logicalInput.length() - 1)) { - // const ideographicBreak = charAllowsIdeographicBreaking(codePoint); - // if (breakable[codePoint] || ideographicBreak || section.imageName) { - - // potentialLineBreaks.push( - // evaluateBreak( - // i + 1, - // currentX, - // targetWidth, - // potentialLineBreaks, - // calculatePenalty(codePoint, logicalInput.getCharCode(i + 1), ideographicBreak && hasServerSuggestedBreakpoints), - // false)); - // } - // } - // } - - // return leastBadBreaks( - // evaluateBreak( - // logicalInput.length(), - // currentX, - // targetWidth, - // potentialLineBreaks, - // 0, - // true)); + + if (symbolPlacement !== 'point') + return []; + + if (!logicalInput) + return []; + + const potentialLineBreaks = []; + const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap, imagePositions, layoutTextSize); + + const hasServerSuggestedBreakpoints = logicalInput.text.indexOf('\u200b') >= 0; + + let currentX = 0; + + for (let i = 0; i < logicalInput.length(); i++) { + const section = logicalInput.getSection(i); + const codePoint = logicalInput.getCharCode(i); + if (!whitespace[codePoint]) currentX += getGlyphAdvance(codePoint as any, section, glyphMap, imagePositions, spacing, layoutTextSize); + + // Ideographic characters, spaces, and word-breaking punctuation that often appear without + // surrounding spaces. + if ((i < logicalInput.length() - 1)) { + const ideographicBreak = charAllowsIdeographicBreaking(codePoint as any); + if (breakable[codePoint] || ideographicBreak || section.imageName) { + + potentialLineBreaks.push( + evaluateBreak( + i + 1, + currentX, + targetWidth, + potentialLineBreaks, + calculatePenalty(codePoint as any, logicalInput.getCharCode(i + 1) as any, ideographicBreak && hasServerSuggestedBreakpoints), + false)); + } + } + } + + return leastBadBreaks( + evaluateBreak( + logicalInput.length(), + currentX, + targetWidth, + potentialLineBreaks, + 0, + true)); } function getAnchorAlignment(anchor: SymbolAnchor) { @@ -640,10 +640,18 @@ function shapeLines(shaping: Shaping, continue; } - for (let i = 0; i < line.length(); i++) { - const section = line.getSection(i); - const sectionIndex = line.getSectionIndex(i); - const codePoint = line.getCharCode(i) as any; + const segmenter = new Intl.Segmenter( + 'en', {granularity: 'grapheme'} + ); + const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); + + for (let i = 0; i < graphemes.length; i++) { + // const section = line.getSection(i); + const section = line.getSection(0); + // const sectionIndex = line.getSectionIndex(i); + const sectionIndex = line.getSectionIndex(0); + // const codePoint = line.getCharCode(i) as any; + const codePoint = graphemes[i] as any; let baselineOffset = 0.0; let metrics = null; let rect = null; diff --git a/test/debug-pages/debug2.html b/test/debug-pages/debug2.html new file mode 100644 index 0000000000..6843e905fe --- /dev/null +++ b/test/debug-pages/debug2.html @@ -0,0 +1,87 @@ + + + + + MapLibre GL JS debug page + + + + + + + +
+ + + + + + \ No newline at end of file From fa63d4629b28008d9a1bb1b1fc6859824d87eb2b Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 09:55:28 +0200 Subject: [PATCH 05/15] Index by word --- src/data/bucket/symbol_bucket.ts | 2 +- src/render/glyph_manager.ts | 2 +- src/symbol/shaping.ts | 2 +- test/debug-pages/debug2.html | 80 +++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index d09ccc1f4d..d922478e91 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -417,7 +417,7 @@ class SymbolBucket implements Bucket { calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { const segmenter = new Intl.Segmenter( - 'my', {granularity: 'grapheme'} + 'en', {granularity: 'word'} ); const graphemes = Array.from(segmenter.segment(text), s => s.segment); for (let i = 0; i < graphemes.length; i++) { diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 4efad31565..3c7ba60be9 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -204,7 +204,7 @@ export default class GlyphManager { }); } - // console.log('tinysdf', id, typeof id); + console.log('tinysdf', id, typeof id); // OLIVER // const char = tinySDF.draw(String.fromCharCode(id)); const char = tinySDF.draw(id as any); diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index e5aaa7d81d..3c4b637518 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -641,7 +641,7 @@ function shapeLines(shaping: Shaping, } const segmenter = new Intl.Segmenter( - 'en', {granularity: 'grapheme'} + 'en', {granularity: 'word'} ); const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); diff --git a/test/debug-pages/debug2.html b/test/debug-pages/debug2.html index 6843e905fe..164167a64a 100644 --- a/test/debug-pages/debug2.html +++ b/test/debug-pages/debug2.html @@ -2,7 +2,7 @@ - MapLibre GL JS debug page + Index by Graheme @@ -36,25 +36,13 @@ var map = window.map = new maplibregl.Map({ container: 'map', - zoom: 12.5, - center: [-77.01866, 38.888], + zoom: 14.5, + center: [90.39561, 23.74923], style: 'style2.json', hash: 'map' }); map.addControl(new maplibregl.NavigationControl()); - map.addControl(new maplibregl.GeolocateControl({ - positionOptions: { - enableHighAccuracy: true - }, - trackUserLocation: true, - showUserLocation: true, - fitBoundsOptions: { - maxZoom: 20 - } - })); - map.addControl(new maplibregl.ScaleControl()); - map.addControl(new maplibregl.FullscreenControl()); map.on('load', function () { var hash = window.location.hash.substr(1); @@ -65,14 +53,74 @@ return res; }, {}); + map.addLayer({ + "id": "line-labels-swissmap", + "type": "symbol", + "source": "swissmap", + "source-layer": "highway", + "layout": { + "text-field": [ + "get", + "name" + ], + "text-font": [ + "Fira Sans Regular" + ], + "symbol-placement": "line", + "text-keep-upright": true, + }, + "paint": { + "text-halo-color": "white", + "text-color": "#888", + "text-halo-width": 1.5 + } + }); + map.addLayer({ 'id': 'points', 'type': 'symbol', 'source': 'qrank', 'source-layer': 'qrank', 'layout': { - 'text-max-width': 4, + 'text-max-width': 0, 'text-field': ['concat', ['get', 'name:en'], '\n', ['get', `name${result.language ? (':' + result.language) : ''}`]], + "text-size": [ + "interpolate", + ["linear"], + ["zoom"], + 7, + ["*", + 1.1, + [ + "case", + [">", ["to-number", ["get", "@qrank"]], 10000000], + 17, + [">", ["to-number", ["get", "@qrank"]], 1000000], + 16, + [">", ["to-number", ["get", "@qrank"]], 100000], + 14, + [">", ["to-number", ["get", "@qrank"]], 10000], + 12, + 11 + ] + ], + 12, + ["*", + 1.5, + [ + "case", + [">", ["to-number", ["get", "@qrank"]], 10000000], + 17, + [">", ["to-number", ["get", "@qrank"]], 1000000], + 16, + [">", ["to-number", ["get", "@qrank"]], 100000], + 14, + [">", ["to-number", ["get", "@qrank"]], 10000], + 12, + 11 + ] + ] + ], }, 'paint': { 'text-halo-color': 'white', From 04b6751d750ecd389a545569b651acaa32ef86e6 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 18:18:57 +0200 Subject: [PATCH 06/15] Add canvas comparer --- src/data/bucket/symbol_bucket.ts | 9 ++++- src/symbol/shaping.ts | 9 ++++- src/util/canvas_comparer.ts | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/util/canvas_comparer.ts diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index d922478e91..3e17408a79 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -56,6 +56,7 @@ import type {SizeData} from '../../symbol/symbol_size'; import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; import type {VectorTileLayer} from '@mapbox/vector-tile'; +import CanvasComparer from '../../util/canvas_comparer'; export type SingleCollisionBox = { x1: number; @@ -417,9 +418,15 @@ class SymbolBucket implements Bucket { calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { const segmenter = new Intl.Segmenter( - 'en', {granularity: 'word'} + 'en', {granularity: 'grapheme'} ); const graphemes = Array.from(segmenter.segment(text), s => s.segment); + + const canvasComparer = new CanvasComparer(); + + // const graphemes = [...text]; + canvasComparer.mergeStrings(graphemes); + for (let i = 0; i < graphemes.length; i++) { // OLIVER // stack[text.charCodeAt(i).toString()] = true; diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 3c4b637518..ee869ffc3f 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -14,6 +14,7 @@ import type {ImagePosition} from '../render/image_atlas'; import {IMAGE_PADDING} from '../render/image_atlas'; import type {Rect, GlyphPosition} from '../render/glyph_atlas'; import {Formatted, FormattedSection} from '@maplibre/maplibre-gl-style-spec'; +import CanvasComparer from '../util/canvas_comparer'; enum WritingMode { none = 0, @@ -624,6 +625,9 @@ function shapeLines(shaping: Shaping, textJustify === 'left' ? 0 : 0.5; let lineIndex = 0; + + const canvasComparer = new CanvasComparer(); + for (const line of lines) { line.trim(); @@ -641,10 +645,13 @@ function shapeLines(shaping: Shaping, } const segmenter = new Intl.Segmenter( - 'en', {granularity: 'word'} + 'en', {granularity: 'grapheme'} ); const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); + // const graphemes = [...line.text]; + canvasComparer.mergeStrings(graphemes); + for (let i = 0; i < graphemes.length; i++) { // const section = line.getSection(i); const section = line.getSection(0); diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts new file mode 100644 index 0000000000..587fc8587a --- /dev/null +++ b/src/util/canvas_comparer.ts @@ -0,0 +1,57 @@ +export default class CanvasComparer { + private canvas1: OffscreenCanvas; + private canvas2: OffscreenCanvas; + private ctx1: any; + private ctx2: any; + + constructor() { + this.canvas1 = new OffscreenCanvas(100, 100); + this.canvas2 = new OffscreenCanvas(100, 100); + this.ctx1 = this.canvas1.getContext('2d', { willReadFrequently: true }); + this.ctx2 = this.canvas2.getContext('2d', { willReadFrequently: true }); + + this.ctx1.font = '24px Arial'; + this.ctx2.font = '24px Arial'; + } + + private compareCanvases(string1: string, string2: string): boolean { + this.ctx1.clearRect(0, 0, this.canvas1.width, this.canvas1.height); + this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height); + + this.ctx1.fillText(`${string1}${string2}`, 10, 40); + + const parts = [string1, string2]; + let offset = 0; + for (const part of parts) { + this.ctx2.fillText(part, 10 + offset, 40); + offset += this.ctx2.measureText(part).width; + } + + const imageData1 = this.ctx1.getImageData(0, 0, this.canvas1.width, this.canvas1.height); + const imageData2 = this.ctx2.getImageData(0, 0, this.canvas2.width, this.canvas2.height); + + if (imageData1.data.length !== imageData2.data.length) { + return false; + } else { + for (let i = 0; i < imageData1.data.length; i++) { + if (imageData1.data[i] !== imageData2.data[i]) { + return false; + } + } + return true; + } + } + + public mergeStrings(parts: string[]): string[] { + let i = 0; + while (i < parts.length - 1) { + if (!this.compareCanvases(parts[i], parts[i + 1])) { + parts.splice(i, 2, parts[i] + parts[i + 1]); + i = 0; + continue; + } + i++; + } + return parts; + } +} From c68cde0d7ea25e9fcdc0b1867159388c5f1f2c32 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 20:02:47 +0200 Subject: [PATCH 07/15] Optimize --- src/util/canvas_comparer.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts index 587fc8587a..85ae0816e9 100644 --- a/src/util/canvas_comparer.ts +++ b/src/util/canvas_comparer.ts @@ -10,25 +10,30 @@ export default class CanvasComparer { this.ctx1 = this.canvas1.getContext('2d', { willReadFrequently: true }); this.ctx2 = this.canvas2.getContext('2d', { willReadFrequently: true }); - this.ctx1.font = '24px Arial'; - this.ctx2.font = '24px Arial'; + this.ctx1.font = '12px Arial'; + this.ctx2.font = '12px Arial'; } private compareCanvases(string1: string, string2: string): boolean { this.ctx1.clearRect(0, 0, this.canvas1.width, this.canvas1.height); this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height); - this.ctx1.fillText(`${string1}${string2}`, 10, 40); + this.ctx1.fillText(`${string1}${string2}`, 0, 20); + const offset1 = this.ctx1.measureText(`${string1}${string2}`).width; const parts = [string1, string2]; - let offset = 0; + let offset2 = 0; for (const part of parts) { - this.ctx2.fillText(part, 10 + offset, 40); - offset += this.ctx2.measureText(part).width; + this.ctx2.fillText(part, offset2, 20); + offset2 += this.ctx2.measureText(part).width; } - const imageData1 = this.ctx1.getImageData(0, 0, this.canvas1.width, this.canvas1.height); - const imageData2 = this.ctx2.getImageData(0, 0, this.canvas2.width, this.canvas2.height); + if (offset1 !== offset2) { + return false; + } + + const imageData1 = this.ctx1.getImageData(0, 0, offset2, 30); + const imageData2 = this.ctx2.getImageData(0, 0, offset2, 30); if (imageData1.data.length !== imageData2.data.length) { return false; From aff6cf4505b1b0f6610470214ff537381df5d9d1 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 20:32:27 +0200 Subject: [PATCH 08/15] Optimize more by single pass joining --- src/data/bucket/symbol_bucket.ts | 1 + src/symbol/shaping.ts | 1 + src/util/canvas_comparer.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 3e17408a79..e0dba121eb 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -425,6 +425,7 @@ class SymbolBucket implements Bucket { const canvasComparer = new CanvasComparer(); // const graphemes = [...text]; + canvasComparer.mergeStrings(graphemes); for (let i = 0; i < graphemes.length; i++) { diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index ee869ffc3f..a5df5e5ef3 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -650,6 +650,7 @@ function shapeLines(shaping: Shaping, const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); // const graphemes = [...line.text]; + canvasComparer.mergeStrings(graphemes); for (let i = 0; i < graphemes.length; i++) { diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts index 85ae0816e9..307681871e 100644 --- a/src/util/canvas_comparer.ts +++ b/src/util/canvas_comparer.ts @@ -52,7 +52,7 @@ export default class CanvasComparer { while (i < parts.length - 1) { if (!this.compareCanvases(parts[i], parts[i + 1])) { parts.splice(i, 2, parts[i] + parts[i + 1]); - i = 0; + //i = 0; continue; } i++; From cf1606ba3434a7fb72f3aaad4648cad9a68497c9 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 20:51:55 +0200 Subject: [PATCH 09/15] Optimize with static canvas --- src/util/canvas_comparer.ts | 42 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts index 307681871e..6a97beb7f2 100644 --- a/src/util/canvas_comparer.ts +++ b/src/util/canvas_comparer.ts @@ -1,39 +1,43 @@ export default class CanvasComparer { - private canvas1: OffscreenCanvas; - private canvas2: OffscreenCanvas; - private ctx1: any; - private ctx2: any; + private static canvas1: OffscreenCanvas = new OffscreenCanvas(50, 15); + private static canvas2: OffscreenCanvas = new OffscreenCanvas(50, 15); + private static ctx1: any = CanvasComparer.canvas1.getContext('2d', { willReadFrequently: true }); + private static ctx2: any = CanvasComparer.canvas2.getContext('2d', { willReadFrequently: true }); constructor() { - this.canvas1 = new OffscreenCanvas(100, 100); - this.canvas2 = new OffscreenCanvas(100, 100); - this.ctx1 = this.canvas1.getContext('2d', { willReadFrequently: true }); - this.ctx2 = this.canvas2.getContext('2d', { willReadFrequently: true }); + // this.canvas1 = new OffscreenCanvas(50, 15); + // this.canvas2 = new OffscreenCanvas(50, 15); + // this.ctx1 = this.canvas1.getContext('2d', { willReadFrequently: true }); + // this.ctx2 = this.canvas2.getContext('2d', { willReadFrequently: true }); - this.ctx1.font = '12px Arial'; - this.ctx2.font = '12px Arial'; + CanvasComparer.ctx1.font = '6px Arial'; + CanvasComparer.ctx2.font = '6px Arial'; } private compareCanvases(string1: string, string2: string): boolean { - this.ctx1.clearRect(0, 0, this.canvas1.width, this.canvas1.height); - this.ctx2.clearRect(0, 0, this.canvas2.width, this.canvas2.height); + CanvasComparer.ctx1.clearRect(0, 0, CanvasComparer.canvas1.width, CanvasComparer.canvas1.height); + CanvasComparer.ctx2.clearRect(0, 0, CanvasComparer.canvas2.width, CanvasComparer.canvas2.height); - this.ctx1.fillText(`${string1}${string2}`, 0, 20); - const offset1 = this.ctx1.measureText(`${string1}${string2}`).width; + CanvasComparer.ctx1.fillText(`${string1}${string2}`, 0, 10); + const offset1 = CanvasComparer.ctx1.measureText(`${string1}${string2}`).width; const parts = [string1, string2]; let offset2 = 0; for (const part of parts) { - this.ctx2.fillText(part, offset2, 20); - offset2 += this.ctx2.measureText(part).width; + CanvasComparer.ctx2.fillText(part, offset2, 10); + offset2 += CanvasComparer.ctx2.measureText(part).width; } if (offset1 !== offset2) { return false; } - const imageData1 = this.ctx1.getImageData(0, 0, offset2, 30); - const imageData2 = this.ctx2.getImageData(0, 0, offset2, 30); + if (offset1 === 0) { + return false; + } + + const imageData1 = CanvasComparer.ctx1.getImageData(0, 0, offset2, 15); + const imageData2 = CanvasComparer.ctx2.getImageData(0, 0, offset2, 15); if (imageData1.data.length !== imageData2.data.length) { return false; @@ -52,7 +56,7 @@ export default class CanvasComparer { while (i < parts.length - 1) { if (!this.compareCanvases(parts[i], parts[i + 1])) { parts.splice(i, 2, parts[i] + parts[i + 1]); - //i = 0; + //i = 0; // commenting out this might require grapheme clusters in the first place continue; } i++; From af9662373318cc02db95ef104a9302791d5e8cc2 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 20:54:44 +0200 Subject: [PATCH 10/15] Optimize with 3 px font size --- src/util/canvas_comparer.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts index 6a97beb7f2..691f0b1327 100644 --- a/src/util/canvas_comparer.ts +++ b/src/util/canvas_comparer.ts @@ -1,6 +1,6 @@ export default class CanvasComparer { - private static canvas1: OffscreenCanvas = new OffscreenCanvas(50, 15); - private static canvas2: OffscreenCanvas = new OffscreenCanvas(50, 15); + private static canvas1: OffscreenCanvas = new OffscreenCanvas(50, 8); + private static canvas2: OffscreenCanvas = new OffscreenCanvas(50, 8); private static ctx1: any = CanvasComparer.canvas1.getContext('2d', { willReadFrequently: true }); private static ctx2: any = CanvasComparer.canvas2.getContext('2d', { willReadFrequently: true }); @@ -10,21 +10,21 @@ export default class CanvasComparer { // this.ctx1 = this.canvas1.getContext('2d', { willReadFrequently: true }); // this.ctx2 = this.canvas2.getContext('2d', { willReadFrequently: true }); - CanvasComparer.ctx1.font = '6px Arial'; - CanvasComparer.ctx2.font = '6px Arial'; + CanvasComparer.ctx1.font = '3px Arial'; + CanvasComparer.ctx2.font = '3px Arial'; } private compareCanvases(string1: string, string2: string): boolean { CanvasComparer.ctx1.clearRect(0, 0, CanvasComparer.canvas1.width, CanvasComparer.canvas1.height); CanvasComparer.ctx2.clearRect(0, 0, CanvasComparer.canvas2.width, CanvasComparer.canvas2.height); - CanvasComparer.ctx1.fillText(`${string1}${string2}`, 0, 10); + CanvasComparer.ctx1.fillText(`${string1}${string2}`, 0, 5); const offset1 = CanvasComparer.ctx1.measureText(`${string1}${string2}`).width; const parts = [string1, string2]; let offset2 = 0; for (const part of parts) { - CanvasComparer.ctx2.fillText(part, offset2, 10); + CanvasComparer.ctx2.fillText(part, offset2, 5); offset2 += CanvasComparer.ctx2.measureText(part).width; } @@ -36,8 +36,8 @@ export default class CanvasComparer { return false; } - const imageData1 = CanvasComparer.ctx1.getImageData(0, 0, offset2, 15); - const imageData2 = CanvasComparer.ctx2.getImageData(0, 0, offset2, 15); + const imageData1 = CanvasComparer.ctx1.getImageData(0, 0, offset2, 8); + const imageData2 = CanvasComparer.ctx2.getImageData(0, 0, offset2, 8); if (imageData1.data.length !== imageData2.data.length) { return false; From 47e1a196c2829cdd34b6f54bb2e721d218c41592 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 22:55:05 +0200 Subject: [PATCH 11/15] Only merge graphemes on non-latin text --- src/data/bucket/symbol_bucket.ts | 8 ++++++-- src/symbol/shaping.ts | 6 ++++-- src/util/canvas_comparer.ts | 17 ++++++++++------- test/debug-pages/debug2.html | 2 +- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index e0dba121eb..1b7da1f87c 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -425,8 +425,12 @@ class SymbolBucket implements Bucket { const canvasComparer = new CanvasComparer(); // const graphemes = [...text]; - - canvasComparer.mergeStrings(graphemes); + + console.log('isLatin', text, canvasComparer.isLatin(text)); + + if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { + canvasComparer.mergeStrings(graphemes); + } for (let i = 0; i < graphemes.length; i++) { // OLIVER diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index a5df5e5ef3..7233e1f8f8 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -650,8 +650,10 @@ function shapeLines(shaping: Shaping, const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); // const graphemes = [...line.text]; - - canvasComparer.mergeStrings(graphemes); + + if (!canvasComparer.isLatin(line.text) && !canvasComparer.compareCanvases(line.text, graphemes)) { + canvasComparer.mergeStrings(graphemes); + } for (let i = 0; i < graphemes.length; i++) { // const section = line.getSection(i); diff --git a/src/util/canvas_comparer.ts b/src/util/canvas_comparer.ts index 691f0b1327..8944d74863 100644 --- a/src/util/canvas_comparer.ts +++ b/src/util/canvas_comparer.ts @@ -1,6 +1,6 @@ export default class CanvasComparer { - private static canvas1: OffscreenCanvas = new OffscreenCanvas(50, 8); - private static canvas2: OffscreenCanvas = new OffscreenCanvas(50, 8); + private static canvas1: OffscreenCanvas = new OffscreenCanvas(100, 8); + private static canvas2: OffscreenCanvas = new OffscreenCanvas(100, 8); private static ctx1: any = CanvasComparer.canvas1.getContext('2d', { willReadFrequently: true }); private static ctx2: any = CanvasComparer.canvas2.getContext('2d', { willReadFrequently: true }); @@ -14,14 +14,13 @@ export default class CanvasComparer { CanvasComparer.ctx2.font = '3px Arial'; } - private compareCanvases(string1: string, string2: string): boolean { + public compareCanvases(total: string, parts: string[]): boolean { CanvasComparer.ctx1.clearRect(0, 0, CanvasComparer.canvas1.width, CanvasComparer.canvas1.height); CanvasComparer.ctx2.clearRect(0, 0, CanvasComparer.canvas2.width, CanvasComparer.canvas2.height); - CanvasComparer.ctx1.fillText(`${string1}${string2}`, 0, 5); - const offset1 = CanvasComparer.ctx1.measureText(`${string1}${string2}`).width; + CanvasComparer.ctx1.fillText(total, 0, 5); + const offset1 = CanvasComparer.ctx1.measureText(total).width; - const parts = [string1, string2]; let offset2 = 0; for (const part of parts) { CanvasComparer.ctx2.fillText(part, offset2, 5); @@ -54,7 +53,7 @@ export default class CanvasComparer { public mergeStrings(parts: string[]): string[] { let i = 0; while (i < parts.length - 1) { - if (!this.compareCanvases(parts[i], parts[i + 1])) { + if (!this.compareCanvases(`${parts[i]}${parts[i + 1]}`, [parts[i], parts[i + 1]])) { parts.splice(i, 2, parts[i] + parts[i + 1]); //i = 0; // commenting out this might require grapheme clusters in the first place continue; @@ -63,4 +62,8 @@ export default class CanvasComparer { } return parts; } + + public isLatin(text) { + return /^[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF\s\-'",.;:()!?]+$/u.test(text); + } } diff --git a/test/debug-pages/debug2.html b/test/debug-pages/debug2.html index 164167a64a..7eb45d89b4 100644 --- a/test/debug-pages/debug2.html +++ b/test/debug-pages/debug2.html @@ -83,7 +83,7 @@ 'source-layer': 'qrank', 'layout': { 'text-max-width': 0, - 'text-field': ['concat', ['get', 'name:en'], '\n', ['get', `name${result.language ? (':' + result.language) : ''}`]], + 'text-field': ['format', ['get', 'name:en'], {}, '\n', {}, ['get', `name${result.language ? (':' + result.language) : ''}`], {}], "text-size": [ "interpolate", ["linear"], From fdd319578245b06659a7cc2f0c066d4513ff038f Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 30 Apr 2023 22:58:45 +0200 Subject: [PATCH 12/15] Comment print --- src/data/bucket/symbol_bucket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 1b7da1f87c..9f878078dc 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -426,7 +426,7 @@ class SymbolBucket implements Bucket { // const graphemes = [...text]; - console.log('isLatin', text, canvasComparer.isLatin(text)); + // console.log('isLatin', text, canvasComparer.isLatin(text)); if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { canvasComparer.mergeStrings(graphemes); From 2edcd4b6d025b6e3fc0b76da53f9331fcfb2b00f Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Mon, 1 May 2023 11:24:48 +0200 Subject: [PATCH 13/15] Add cache --- src/data/bucket/symbol_bucket.ts | 34 +++++++++++++++++++++++--------- src/symbol/shaping.ts | 30 +++++++++++++++++++++------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 9f878078dc..4393fac375 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -363,6 +363,8 @@ class SymbolBucket implements Bucket { allowVerticalPlacement: boolean; hasRTLText: boolean; + textCache: {[key: string]: string[]}; + constructor(options: BucketParameters) { this.collisionBoxArray = options.collisionBoxArray; this.zoom = options.zoom; @@ -405,6 +407,8 @@ class SymbolBucket implements Bucket { this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); this.sourceID = options.sourceID; + + this.textCache = {}; } createArrays() { @@ -417,19 +421,31 @@ class SymbolBucket implements Bucket { } calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { - const segmenter = new Intl.Segmenter( - 'en', {granularity: 'grapheme'} - ); - const graphemes = Array.from(segmenter.segment(text), s => s.segment); + let graphemes = []; - const canvasComparer = new CanvasComparer(); + if (text in this.textCache) { + // console.log('symbol_bucket cache hit', text); + graphemes = this.textCache[text]; + + } else { + // console.log('symbol bucket cache miss', text); - // const graphemes = [...text]; + const segmenter = new Intl.Segmenter( + 'en', {granularity: 'grapheme'} + ); + graphemes = Array.from(segmenter.segment(text), s => s.segment); - // console.log('isLatin', text, canvasComparer.isLatin(text)); + // const graphemes = [...line.text]; + + // console.log('shaping', line.text); + + const canvasComparer = new CanvasComparer(); + + if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { + canvasComparer.mergeStrings(graphemes); + } - if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { - canvasComparer.mergeStrings(graphemes); + this.textCache[text] = [...graphemes]; } for (let i = 0; i < graphemes.length; i++) { diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 7233e1f8f8..4ec90bd4b3 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -593,6 +593,8 @@ function getAnchorAlignment(anchor: SymbolAnchor) { return {horizontalAlign, verticalAlign}; } +const textCache: {[key: string]: string[]} = {}; + function shapeLines(shaping: Shaping, glyphMap: { [_: string]: { @@ -644,15 +646,29 @@ function shapeLines(shaping: Shaping, continue; } - const segmenter = new Intl.Segmenter( - 'en', {granularity: 'grapheme'} - ); - const graphemes = Array.from(segmenter.segment(line.text), s => s.segment); + let graphemes = []; + + if (line.text in textCache) { + // console.log('cache hit', line.text); + graphemes = textCache[line.text]; + + } else { + // console.log('cache miss', line.text); + + const segmenter = new Intl.Segmenter( + 'en', {granularity: 'grapheme'} + ); + graphemes = Array.from(segmenter.segment(line.text), s => s.segment); + + // const graphemes = [...line.text]; - // const graphemes = [...line.text]; + // console.log('shaping', line.text); + + if (!canvasComparer.isLatin(line.text) && !canvasComparer.compareCanvases(line.text, graphemes)) { + canvasComparer.mergeStrings(graphemes); + } - if (!canvasComparer.isLatin(line.text) && !canvasComparer.compareCanvases(line.text, graphemes)) { - canvasComparer.mergeStrings(graphemes); + textCache[line.text] = [...graphemes]; } for (let i = 0; i < graphemes.length; i++) { From 2349b59e23790b4c383bc9eb8b4544de4e0d0f03 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Sun, 7 May 2023 14:31:04 +0200 Subject: [PATCH 14/15] Use marked strings --- src/data/bucket/symbol_bucket.ts | 42 ++++++++++++++++++-------------- src/render/glyph_manager.ts | 17 +++++++++++++ src/symbol/shaping.ts | 42 +++++++++++++++++--------------- src/util/util.ts | 22 +++++++++++++++++ test/debug-pages/debug2.html | 24 ++++++++++++++---- 5 files changed, 105 insertions(+), 42 deletions(-) diff --git a/src/data/bucket/symbol_bucket.ts b/src/data/bucket/symbol_bucket.ts index 4393fac375..f074074347 100644 --- a/src/data/bucket/symbol_bucket.ts +++ b/src/data/bucket/symbol_bucket.ts @@ -57,6 +57,7 @@ import type {FeatureStates} from '../../source/source_state'; import type {ImagePosition} from '../../render/image_atlas'; import type {VectorTileLayer} from '@mapbox/vector-tile'; import CanvasComparer from '../../util/canvas_comparer'; +import { markedStringToParts } from '../../util/util'; export type SingleCollisionBox = { x1: number; @@ -421,32 +422,37 @@ class SymbolBucket implements Bucket { } calculateGlyphDependencies(text: string, stack: {[_: string]: boolean}, textAlongLine: boolean, allowVerticalPlacement: boolean, doesAllowVerticalWritingMode: boolean) { - let graphemes = []; + // let graphemes = []; - if (text in this.textCache) { - // console.log('symbol_bucket cache hit', text); - graphemes = this.textCache[text]; + // if (text in this.textCache) { + // // console.log('symbol_bucket cache hit', text); + // graphemes = this.textCache[text]; - } else { - // console.log('symbol bucket cache miss', text); + // } else { + // // console.log('symbol bucket cache miss', text); - const segmenter = new Intl.Segmenter( - 'en', {granularity: 'grapheme'} - ); - graphemes = Array.from(segmenter.segment(text), s => s.segment); + // // const segmenter = new Intl.Segmenter( + // // 'en', {granularity: 'grapheme'} + // // ); + // // graphemes = Array.from(segmenter.segment(text), s => s.segment); - // const graphemes = [...line.text]; + // const graphemes = [...text]; - // console.log('shaping', line.text); + // // console.log('shaping', line.text); - const canvasComparer = new CanvasComparer(); + // // const canvasComparer = new CanvasComparer(); - if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { - canvasComparer.mergeStrings(graphemes); - } + // // if (!canvasComparer.isLatin(text) && !canvasComparer.compareCanvases(text, graphemes)) { + // // canvasComparer.mergeStrings(graphemes); + // // } - this.textCache[text] = [...graphemes]; - } + // this.textCache[text] = [...graphemes]; + // } + + + // const graphemes = [...text]; + + const graphemes = markedStringToParts(text); for (let i = 0; i < graphemes.length; i++) { // OLIVER diff --git a/src/render/glyph_manager.ts b/src/render/glyph_manager.ts index 3c7ba60be9..3679f2b125 100644 --- a/src/render/glyph_manager.ts +++ b/src/render/glyph_manager.ts @@ -207,8 +207,25 @@ export default class GlyphManager { console.log('tinysdf', id, typeof id); // OLIVER // const char = tinySDF.draw(String.fromCharCode(id)); + + // let char; + + // const cachedChar = localStorage.getItem(id as any); + + // if (cachedChar) { + // console.log('cached'); + // char = JSON.parse(cachedChar); + // char.data = Object.values(char.data); + // } else { + // console.log('not cached'); + // char = tinySDF.draw(id as any); + // localStorage.setItem(id as any, JSON.stringify(char)); + // } + const char = tinySDF.draw(id as any); + // console.log('char', char, JSON.stringify(char)); + /** * TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph. * Server-generated fonts specify "top" relative to an origin above the em box (the origin diff --git a/src/symbol/shaping.ts b/src/symbol/shaping.ts index 4ec90bd4b3..a4fdd9220b 100644 --- a/src/symbol/shaping.ts +++ b/src/symbol/shaping.ts @@ -6,7 +6,7 @@ import { import verticalizePunctuation from '../util/verticalize_punctuation'; import {plugin as rtlTextPlugin} from '../source/rtl_text_plugin'; import ONE_EM from './one_em'; -import {warnOnce} from '../util/util'; +import {markedStringToParts, warnOnce} from '../util/util'; import type {StyleGlyph, GlyphMetrics} from '../style/style_glyph'; import {GLYPH_PBF_BORDER} from '../style/parse_glyph_pbf'; @@ -628,7 +628,7 @@ function shapeLines(shaping: Shaping, let lineIndex = 0; - const canvasComparer = new CanvasComparer(); + //const canvasComparer = new CanvasComparer(); for (const line of lines) { line.trim(); @@ -646,30 +646,34 @@ function shapeLines(shaping: Shaping, continue; } - let graphemes = []; + // let graphemes = []; - if (line.text in textCache) { - // console.log('cache hit', line.text); - graphemes = textCache[line.text]; + // if (line.text in textCache) { + // // console.log('cache hit', line.text); + // graphemes = textCache[line.text]; - } else { - // console.log('cache miss', line.text); + // } else { + // // console.log('cache miss', line.text); - const segmenter = new Intl.Segmenter( - 'en', {granularity: 'grapheme'} - ); - graphemes = Array.from(segmenter.segment(line.text), s => s.segment); + // // const segmenter = new Intl.Segmenter( + // // 'en', {granularity: 'grapheme'} + // // ); + // // graphemes = Array.from(segmenter.segment(line.text), s => s.segment); - // const graphemes = [...line.text]; + // const graphemes = [...line.text]; - // console.log('shaping', line.text); + // // console.log('shaping', line.text); - if (!canvasComparer.isLatin(line.text) && !canvasComparer.compareCanvases(line.text, graphemes)) { - canvasComparer.mergeStrings(graphemes); - } + // // if (!canvasComparer.isLatin(line.text) && !canvasComparer.compareCanvases(line.text, graphemes)) { + // // canvasComparer.mergeStrings(graphemes); + // // } - textCache[line.text] = [...graphemes]; - } + // textCache[line.text] = [...graphemes]; + // } + + // const graphemes = [...line.text]; + + const graphemes = markedStringToParts(line.text); for (let i = 0; i < graphemes.length; i++) { // const section = line.getSection(i); diff --git a/src/util/util.ts b/src/util/util.ts index bb839e175a..e8604cef5a 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -582,3 +582,25 @@ export function arrayBufferToImage(data: ArrayBuffer, callback: (err?: Error | n const blob: Blob = new Blob([new Uint8Array(data)], {type: 'image/png'}); img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; } + +export function markedStringToParts(markedString) { + const result = []; + const customJoiningCharacter = '@'; + if (markedString.length === 0 || markedString[0] === customJoiningCharacter) { + return result; + } + let currentPart = markedString[0]; + for (let i = 1; i < markedString.length; ++i) { + if (markedString[i] === customJoiningCharacter) { + continue; + } + if (markedString[i - 1] === customJoiningCharacter) { + currentPart += markedString[i]; + } else { + result.push(currentPart); + currentPart = markedString[i]; + } + } + result.push(currentPart); + return result; +} diff --git a/test/debug-pages/debug2.html b/test/debug-pages/debug2.html index 7eb45d89b4..4dfff21c18 100644 --- a/test/debug-pages/debug2.html +++ b/test/debug-pages/debug2.html @@ -59,10 +59,7 @@ "source": "swissmap", "source-layer": "highway", "layout": { - "text-field": [ - "get", - "name" - ], + "text-field": ['get', `name${result.language ? (':' + result.language) : ''}`], "text-font": [ "Fira Sans Regular" ], @@ -76,6 +73,23 @@ } }); + const pointLabelFirstLine = [ + 'coalesce', + ['get', '@name:en'], + ['get', 'name'] + ]; + + let pointLabelSecondLine = [ + 'case', + ['has', '@language'], + ['get', ['concat', '@name:', ['get', '@language']]], + ['get', `@name${result.language ? (':' + result.language) : ''}`] + ]; + + if (result.language) { + pointLabelSecondLine = ['get', `@name:${result.language}`]; + } + map.addLayer({ 'id': 'points', 'type': 'symbol', @@ -83,7 +97,7 @@ 'source-layer': 'qrank', 'layout': { 'text-max-width': 0, - 'text-field': ['format', ['get', 'name:en'], {}, '\n', {}, ['get', `name${result.language ? (':' + result.language) : ''}`], {}], + 'text-field': ['format', pointLabelFirstLine, {}, '\n', {}, pointLabelSecondLine, {}], "text-size": [ "interpolate", ["linear"], From f3fe44c73f244a1e83bc8a5e3e6345a1d6812068 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Mon, 8 May 2023 13:33:46 +0200 Subject: [PATCH 15/15] Use roadnames data source with marked strings --- test/debug-pages/debug2.html | 2 +- test/debug-pages/style2.json | 573 +++++++++++++++++++++++++++++++++++ 2 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 test/debug-pages/style2.json diff --git a/test/debug-pages/debug2.html b/test/debug-pages/debug2.html index 4dfff21c18..f98c07ea89 100644 --- a/test/debug-pages/debug2.html +++ b/test/debug-pages/debug2.html @@ -56,7 +56,7 @@ map.addLayer({ "id": "line-labels-swissmap", "type": "symbol", - "source": "swissmap", + "source": "roadnames", "source-layer": "highway", "layout": { "text-field": ['get', `name${result.language ? (':' + result.language) : ''}`], diff --git a/test/debug-pages/style2.json b/test/debug-pages/style2.json new file mode 100644 index 0000000000..a46c6f7449 --- /dev/null +++ b/test/debug-pages/style2.json @@ -0,0 +1,573 @@ +{ + "version": 8, + "name": "SwissMap style", + "sources": { + "swissmap": { + "type": "vector", + "url": "https://wipfli.github.io/swiss-map/tilejson-swissmap.json" + }, + "qrank": { + "type": "vector", + "url": "https://wipfli.github.io/swiss-map/tilejson-qrank.json" + }, + "roadnames": { + "type": "vector", + "url": "https://wipfli.github.io/swiss-map/tilejson-roadnames.json" + }, + "landcover": { + "type": "vector", + "url": "https://wipfli.github.io/swiss-map/tilejson-h3-landcover.json" + } + }, + "light": { + "intensity": 0.1 + }, + "glyphs": "https://wipfli.github.io/swiss-map/font/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "layout": { + "visibility": "visible" + }, + "paint": { + "background-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + "#a8dab5", + 11, + "#f8f9fa" + ] + } + }, + { + "id": "Grass-Bare-Snow", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "Grass-Bare-Snow" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#b6dcc1", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "Bare-Snow", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "Bare-Snow" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#f3ede0", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "Snow", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "Snow" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "white", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "Crops", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "Crops" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#bbe2c6", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "Tree", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "Tree" + ], + "paint": { + "fill-color": "#94d2a5", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "wood", + "type": "fill", + "source": "swissmap", + "source-layer": "wood", + "paint": { + "fill-color": "#ceead6", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 0, + 11, + 1 + ] + } + }, + { + "id": "BuiltUp", + "type": "fill", + "source": "landcover", + "source-layer": "landcover", + "filter": [ + "==", + [ + "get", + "kind" + ], + "BuiltUp" + ], + "paint": { + "fill-color": "#e8eaed", + "fill-opacity": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 1, + 11, + 0 + ] + } + }, + { + "id": "residential", + "type": "fill", + "source": "swissmap", + "source-layer": "residential", + "minzoom": 10, + "maxzoom": 15, + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "stops": [ + [ + 12, + "#e8eaed" + ], + [ + 14, + "#F1F2F4" + ] + ] + } + } + }, + { + "id": "waterway", + "type": "line", + "source": "swissmap", + "source-layer": "waterway", + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "#ABCDFB", + "line-width": { + "stops": [ + [ + 9, + 1 + ], + [ + 11, + 2 + ] + ] + } + } + }, + { + "id": "water", + "type": "fill", + "source": "swissmap", + "source-layer": "water", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + "#8ab4f8", + 11, + "#ABCDFB" + ] + } + }, + { + "id": "highway-path", + "type": "line", + "source": "swissmap", + "source-layer": "highway-path", + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#bdc3c7", + "line-dasharray": [ + 0.0, + 2.0 + ], + "line-width": { + "stops": [ + [ + 12, + 0 + ], + [ + 14, + 2 + ], + [ + 20, + 3 + ] + ] + } + } + }, + { + "id": "highway-footway", + "type": "line", + "source": "swissmap", + "source-layer": "highway-footway", + "minzoom": 15, + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "#bdc3c7", + "line-dasharray": [ + 0.0, + 2.0 + ], + "line-width": { + "stops": [ + [ + 14, + 2 + ], + [ + 20, + 3 + ] + ] + } + } + }, + { + "id": "highway-tracktype-2", + "type": "line", + "source": "swissmap", + "source-layer": "highway-tracktype-2", + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "#bdc3c7", + "line-width": { + "stops": [ + [ + 12, + 0 + ], + [ + 14, + 1.5 + ] + ] + } + } + }, + { + "id": "highway-tracktype-3-4-5", + "type": "line", + "source": "swissmap", + "source-layer": "highway-tracktype-3-4-5", + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "#bdc3c7", + "line-dasharray": [ + 10.0, + 2.0 + ], + "line-width": { + "stops": [ + [ + 12, + 0 + ], + [ + 14, + 1.5 + ], + [ + 20, + 3 + ] + ] + } + } + }, + { + "id": "highway", + "type": "line", + "source": "swissmap", + "source-layer": "highway", + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible", + "line-sort-key": [ + "to-number", + [ + "get", + "line-sort-key" + ] + ] + }, + "paint": { + "line-color": [ + "get", + "line-color" + ], + "line-width": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 14, + [ + "to-number", + [ + "get", + "line-width" + ] + ], + 20, + [ + "to-number", + [ + "get", + "line-width-z20" + ] + ] + ] + } + }, + { + "id": "boundary", + "type": "line", + "source": "swissmap", + "source-layer": "boundary", + "layout": { + "visibility": "visible" + }, + "paint": { + "line-width": 2, + "line-color": "#7f8c8d" + } + }, + { + "id": "building", + "type": "fill", + "source": "swissmap", + "minzoom": 15, + "source-layer": "building", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#f1f3f4" + } + }, + { + "id": "building_outline", + "type": "line", + "source": "swissmap", + "source-layer": "building", + "minzoom": 15, + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "#dee0e4", + "line-width": 1 + } + }, + { + "id": "building-extrusion", + "type": "fill-extrusion", + "source": "swissmap", + "minzoom": 15, + "source-layer": "building", + "filter": [ + "has", + "est_height" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-extrusion-color": "#e6e7e9", + "fill-extrusion-height": [ + "to-number", + [ + "get", + "est_height" + ] + ], + "fill-extrusion-opacity": 1.0 + } + } + ], + "id": "swissmap-style" +} \ No newline at end of file