<commit_before>// Text rendering style

import Builders from '../builders';
import {StyleParser} from '../style_parser';
import Texture from '../../gl/texture';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';
import {Sprites} from '../sprites/sprites';
import LabelPoint from './label_point';
import LabelLine from './label_line';

import log from 'loglevel';

export let TextStyle = Object.create(Sprites);

Object.assign(TextStyle, {
    name: 'text',
    super: Sprites,
    built_in: true,
    selection: false,

    init() {

        this.super.init.apply(this, arguments);

        // Provide a hook for this object to be called from worker threads
        if (Utils.isMainThread) {
            WorkerBroker.addTarget('TextStyle', this);
        }

        this.texts = {}; // unique texts, keyed by tile
        this.texture = {};
        this.canvas = {};
        this.bboxes = {};
        this.maxPriority = 0;

        // default font style
        this.font_style = {
            typeface: 'Helvetica 12px',
            fill: 'white',
            capitalized: false,
            stroke: {
                color: 'black',
                width: 3
            }
        };

        // default label style
        this.label_style = {
            priorities: {
                administrative: 4,
                restaurant: 3,
                major_road: 2,
                minor_road: 1
            },
            lines: {
                exceed: 80,
                offset: 0
            },
            points: {
                max_width: 100
            }
        };
    },

    // Set font style params for canvas drawing
    setFont (tile, { font, fill, stroke, stroke_width, px_size }) {
        this.size = parseInt(px_size);
        this.buffer = 6 * this.device_pixel_ratio; // pixel padding around text
        let ctx = this.canvas[tile].context;

        ctx.font = font;
        if (stroke) {
            ctx.strokeStyle = stroke;
        }
        ctx.fillStyle = fill;
        ctx.lineWidth = stroke_width;
        ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text, tile, capitalized) {
        let str = capitalized ? text.toUpperCase() : text;
        let ctx = this.canvas[tile].context;
        let split = str.split(' ');
        let split_size = {
            " ": this.canvas[tile].context.measureText(" ").width
        };

        for (let i in split) {
            let word = split[i];
            split_size[word] = ctx.measureText(word).width;
        }

        let text_size = [
            this.canvas[tile].context.measureText(str).width,
            this.size
        ];
        let texture_text_size = [
            Math.ceil(text_size[0]) + this.buffer * 2,
            this.size + this.buffer * 2
        ];

        return { split_size, text_size, texture_text_size };
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y], tile, stroke, capitalized) {
        let str = capitalized ? text.toUpperCase() : text;
        if (stroke) {
            this.canvas[tile].context.strokeText(str, x + this.buffer, y + this.buffer + this.size);
        }
        this.canvas[tile].context.fillText(str, x + this.buffer, y + this.buffer + this.size);
    },

    setTextureTextPositions (texts) {
        // Find widest label and sum of all label heights
        let widest = 0, height = 0;

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_info = text_infos[text];
                let size = text_info.size.texture_text_size;

                text_info.position = [0, height];

                if (size[0] > widest) {
                    widest = size[0];
                }

                height += size[1];
            }
        }

        return [ widest, height ];
    },

    getTextSizes (tile, texts) {
        // create a canvas
        if(!this.canvas[tile]) {
            let canvas = document.createElement('canvas');
            this.canvas[tile] = {
                canvas: canvas,
                context: canvas.getContext('2d')
            };
        }

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_style = text_infos[text].text_style;
                // update text sizes
                this.setFont(tile, text_style);
                text_infos[text].size = this.textSize(text, tile, text_style.capitalized);
            }
        }

        return Promise.resolve(texts);
    },

    // Called on main thread to release tile-specific resources
    freeTile (tile) {
        delete this.canvas[tile];
        delete this.texture[tile];
    },

    rasterize (tile, texts, texture_size) {
        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let info = text_infos[text];

                this.setFont(tile, info.text_style);
                this.drawText(text, info.position, tile, info.text_style.stroke, info.text_style.capitalized);

                info.texcoords = Builders.getTexcoordsForSprite(
                    info.position,
                    info.size.texture_text_size,
                    texture_size
                );

                // sub-texts uv mapping
                for (let i in info.sub_texts) {
                    let sub_text = info.sub_texts[i];

                    if (!info.subtexcoords) {
                        info.subtexcoords = {}
                    }

                    // TODO : compute sub texture coordinates
                    // info.subtexcoords[sub_text] = Builders.getTexcoordsForSprite(
                    //    info.position,
                    //    info.size.texture_text_size,
                    //    texture_size
                    //);
                }
            }
        }
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        let texture_size = this.setTextureTextPositions(texts);
        let context = this.canvas[tile].context;

        log.trace(`text summary for tile ${tile}: fits in ${texture_size[0]}x${texture_size[1]}px`);

        // update the canvas "context"
        this.canvas[tile].canvas.width = texture_size[0];
        this.canvas[tile].canvas.height = texture_size[1];
        context.clearRect(0, 0, texture_size[0], texture_size[1]);

        // create a texture
        let texture = 'labels-' + tile + '-' + (TextStyle.texture_id++);
        this.texture[tile] = new Texture(this.gl, texture, { filtering: 'linear' });
        // this.texture[tile].owner = { tile };

        // ask for rasterization for the text set
        this.rasterize(tile, texts, texture_size);

        this.texture[tile].setCanvas(this.canvas[tile].canvas);
        delete this.texture[tile];
        delete this.canvas[tile]; // we don't need canvas once it has been copied to GPU texture

        return Promise.resolve({ texts: this.texts[tile], texture });
    },

    // Override
    startData () {
        let tile_data = this.super.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    labelsFromGeometry (geometry, { text, size }) {
        let labels = [];

        if (geometry.type === "LineString") {
            let lines = geometry.coordinates;
            labels.push(new LabelLine(text, size, lines, this.label_style.lines, true, true));
        } else if (geometry.type === "MultiLineString") {
            let lines = geometry.coordinates;
            for (let i = 0; i < lines.length; ++i) {
                let line = lines[i];
                labels.push(new LabelLine(text, size, line, this.label_style.lines, true, true));
            }
        } else if (geometry.type === "Point") {
            let width = this.label_style.points.max_width * this.device_pixel_ratio;
            if (width && size.text_size[0] > width) {
            //    let label = LabelPoint.explode(text, geometry.coordinates, size, width, Utils.pixelToMercator(24), false, true);
            //    labels.push(label);
            } else {
                labels.push(new LabelPoint(text, geometry.coordinates, size, null, false, true));
            }
        } else if (geometry.type === "MultiPoint") {
            let points = geometry.coordinates;
            for (let i = 0; i < points.length; ++i) {
                let point = points[i];
                labels.push(new LabelPoint(text, point, size, null, false, true));
            }
        } else if (geometry.type === "Polygon") {
            let centroid = Utils.centroid(geometry.coordinates[0]);
            let area = Utils.polygonArea(geometry.coordinates[0]);
            labels.push(new LabelPoint(text, centroid, size, area, false, false));
        } else if (geometry.type === "MultiPolygon") {
            let centroid = Utils.multiCentroid(geometry.coordinates);
            let area = Utils.multiPolygonArea(geometry.coordinates);
            labels.push(new LabelPoint(text, centroid, size, area, false, false));
        }

        return labels;
    },

    subTextInfos (label_composite, text_info) {
        if (!text_info.sub_texts) {
            text_info.sub_texts = [];
        }
<commit_msg>only apply device pixel ratio to canvas-related sizes culling happens in logical pixel space, which is not affected by pixel ratios<commit_after>// Text rendering style

import Builders from '../builders';
import {StyleParser} from '../style_parser';
import Texture from '../../gl/texture';
import WorkerBroker from '../../utils/worker_broker';
import Utils from '../../utils/utils';
import {Sprites} from '../sprites/sprites';
import LabelPoint from './label_point';
import LabelLine from './label_line';

import log from 'loglevel';

export let TextStyle = Object.create(Sprites);

Object.assign(TextStyle, {
    name: 'text',
    super: Sprites,
    built_in: true,
    selection: false,

    init() {

        this.super.init.apply(this, arguments);

        // Provide a hook for this object to be called from worker threads
        if (Utils.isMainThread) {
            WorkerBroker.addTarget('TextStyle', this);
        }

        this.texts = {}; // unique texts, keyed by tile
        this.texture = {};
        this.canvas = {};
        this.bboxes = {};
        this.maxPriority = 0;

        // default font style
        this.font_style = {
            typeface: 'Helvetica 12px',
            fill: 'white',
            capitalized: false,
            stroke: {
                color: 'black',
                width: 3
            }
        };

        // default label style
        this.label_style = {
            priorities: {
                administrative: 4,
                restaurant: 3,
                major_road: 2,
                minor_road: 1
            },
            lines: {
                exceed: 80,
                offset: 0
            },
            points: {
                max_width: 100
            }
        };
    },

    // Set font style params for canvas drawing
    setFont (tile, { font, fill, stroke, stroke_width, px_size }) {
        this.size = parseInt(px_size);
        this.buffer = 6 * this.device_pixel_ratio; // pixel padding around text
        let ctx = this.canvas[tile].context;

        ctx.font = font;
        if (stroke) {
            ctx.strokeStyle = stroke;
        }
        ctx.fillStyle = fill;
        ctx.lineWidth = stroke_width;
        ctx.miterLimit = 2;
    },

    // Width and height of text based on current font style
    textSize (text, tile, capitalized) {
        let str = capitalized ? text.toUpperCase() : text;
        let ctx = this.canvas[tile].context;
        let split = str.split(' ');
        let split_size = {
            " ": this.canvas[tile].context.measureText(" ").width / this.device_pixel_ratio
        };

        for (let i in split) {
            let word = split[i];
            split_size[word] = ctx.measureText(word).width / this.device_pixel_ratio;
        }

        let str_width = ctx.measureText(str).width;
        let text_size = [
            (str_width + this.buffer * 0) / this.device_pixel_ratio,
            (this.size + this.buffer * 0) / this.device_pixel_ratio
        ];

        let texture_text_size = [
            Math.ceil(str_width) + this.buffer * 2,
            this.size + this.buffer * 2
        ];

        return { split_size, text_size, texture_text_size };
    },

    // Draw text at specified location, adjusting for buffer and baseline
    drawText (text, [x, y], tile, stroke, capitalized) {
        let str = capitalized ? text.toUpperCase() : text;
        if (stroke) {
            this.canvas[tile].context.strokeText(str, x + this.buffer, y + this.buffer + this.size);
        }
        this.canvas[tile].context.fillText(str, x + this.buffer, y + this.buffer + this.size);
    },

    setTextureTextPositions (texts) {
        // Find widest label and sum of all label heights
        let widest = 0, height = 0;

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_info = text_infos[text];
                let size = text_info.size.texture_text_size;

                text_info.position = [0, height];

                if (size[0] > widest) {
                    widest = size[0];
                }

                height += size[1];
            }
        }

        return [ widest, height ];
    },

    getTextSizes (tile, texts) {
        // create a canvas
        if(!this.canvas[tile]) {
            let canvas = document.createElement('canvas');
            this.canvas[tile] = {
                canvas: canvas,
                context: canvas.getContext('2d')
            };
        }

        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let text_style = text_infos[text].text_style;
                // update text sizes
                this.setFont(tile, text_style);
                text_infos[text].size = this.textSize(text, tile, text_style.capitalized);
            }
        }

        return Promise.resolve(texts);
    },

    // Called on main thread to release tile-specific resources
    freeTile (tile) {
        delete this.canvas[tile];
        delete this.texture[tile];
    },

    rasterize (tile, texts, texture_size) {
        for (let style in texts) {
            let text_infos = texts[style];

            for (let text in text_infos) {
                let info = text_infos[text];

                this.setFont(tile, info.text_style);
                this.drawText(text, info.position, tile, info.text_style.stroke, info.text_style.capitalized);

                info.texcoords = Builders.getTexcoordsForSprite(
                    info.position,
                    info.size.texture_text_size,
                    texture_size
                );

                // sub-texts uv mapping
                for (let i in info.sub_texts) {
                    let sub_text = info.sub_texts[i];

                    if (!info.subtexcoords) {
                        info.subtexcoords = {}
                    }

                    // TODO : compute sub texture coordinates
                    // info.subtexcoords[sub_text] = Builders.getTexcoordsForSprite(
                    //    info.position,
                    //    info.size.texture_text_size,
                    //    texture_size
                    //);
                }
            }
        }
    },

    // Called on main thread from worker, to create atlas of labels for a tile
    addTexts (tile, texts) {
        this.texts[tile] = texts;

        let texture_size = this.setTextureTextPositions(texts);
        let context = this.canvas[tile].context;

        log.trace(`text summary for tile ${tile}: fits in ${texture_size[0]}x${texture_size[1]}px`);

        // update the canvas "context"
        this.canvas[tile].canvas.width = texture_size[0];
        this.canvas[tile].canvas.height = texture_size[1];
        context.clearRect(0, 0, texture_size[0], texture_size[1]);

        // create a texture
        let texture = 'labels-' + tile + '-' + (TextStyle.texture_id++);
        this.texture[tile] = new Texture(this.gl, texture, { filtering: 'linear' });
        // this.texture[tile].owner = { tile };

        // ask for rasterization for the text set
        this.rasterize(tile, texts, texture_size);

        this.texture[tile].setCanvas(this.canvas[tile].canvas);
        delete this.texture[tile];
        delete this.canvas[tile]; // we don't need canvas once it has been copied to GPU texture

        return Promise.resolve({ texts: this.texts[tile], texture });
    },

    // Override
    startData () {
        let tile_data = this.super.startData.apply(this);
        tile_data.queue = [];
        return tile_data;
    },

    labelsFromGeometry (geometry, { text, size }) {
        let labels = [];

        if (geometry.type === "LineString") {
            let lines = geometry.coordinates;
            labels.push(new LabelLine(text, size, lines, this.label_style.lines, true, true));
        } else if (geometry.type === "MultiLineString") {
            let lines = geometry.coordinates;
            for (let i = 0; i < lines.length; ++i) {
                let line = lines[i];
                labels.push(new LabelLine(text, size, line, this.label_style.lines, true, true));
            }
        } else if (geometry.type === "Point") {
            let width = this.label_style.points.max_width;
            if (width && size.text_size[0] > width) {
            //    let label = LabelPoint.explode(text, geometry.coordinates, size, width, Utils.pixelToMercator(24), false, true);
            //    labels.push(label);
            } else {
                labels.push(new LabelPoint(text, geometry.coordinates, size, null, false, true));
            }
        } else if (geometry.type === "MultiPoint") {
            let points = geometry.coordinates;
            for (let i = 0; i < points.length; ++i) {
                let point = points[i];
                labels.push(new LabelPoint(text, point, size, null, false, true));
            }
        } else if (geometry.type === "Polygon") {
            let centroid = Utils.centroid(geometry.coordinates[0]);
            let area = Utils.polygonArea(geometry.coordinates[0]);
            labels.push(new LabelPoint(text, centroid, size, area, false, false));
        } else if (geometry.type === "MultiPolygon") {
            let centroid = Utils.multiCentroid(geometry.coordinates);
            let area = Utils.multiPolygonArea(geometry.coordinates);
            labels.push(new LabelPoint(text, centroid, size, area, false, false));
        }

        return labels;
    },

    subTextInfos (label_composite, text_info) {
        if (!text_info.sub_texts) {
            text_info.sub_texts = [];
        }

{'fraction_non_alphanumeric': 0.11584699453551912, 'fraction_numerical': 0.004319542024460057, 'mean_word_length': 1.7026722925457103, 'pattern_counts': {'":': 2, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4788497', 'n_tokens_mistral': 5525, 'n_tokens_neox': 5282, 'n_words': 1328}