protomaps-leaflet
Version:
Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).
757 lines (756 loc) • 29 kB
JavaScript
import Point from "@mapbox/point-geometry";
import { ArrayAttr, FontAttr, NumberAttr, StringAttr, TextAttr, } from "./attribute";
import { lineCells, simpleLabel } from "./line";
import { linebreak } from "./text";
import { GeomType } from "./tilecache";
export var Justify;
(function (Justify) {
Justify[Justify["Left"] = 1] = "Left";
Justify[Justify["Center"] = 2] = "Center";
Justify[Justify["Right"] = 3] = "Right";
})(Justify || (Justify = {}));
export var TextPlacements;
(function (TextPlacements) {
TextPlacements[TextPlacements["N"] = 1] = "N";
TextPlacements[TextPlacements["Ne"] = 2] = "Ne";
TextPlacements[TextPlacements["E"] = 3] = "E";
TextPlacements[TextPlacements["Se"] = 4] = "Se";
TextPlacements[TextPlacements["S"] = 5] = "S";
TextPlacements[TextPlacements["Sw"] = 6] = "Sw";
TextPlacements[TextPlacements["W"] = 7] = "W";
TextPlacements[TextPlacements["Nw"] = 8] = "Nw";
})(TextPlacements || (TextPlacements = {}));
export const createPattern = (width, height, fn) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
if (ctx !== null)
fn(canvas, ctx);
return canvas;
};
export class PolygonSymbolizer {
constructor(options) {
var _a;
this.pattern = options.pattern;
this.fill = new StringAttr(options.fill, "black");
this.opacity = new NumberAttr(options.opacity, 1);
this.stroke = new StringAttr(options.stroke, "black");
this.width = new NumberAttr(options.width, 0);
this.perFeature =
(_a = (this.fill.perFeature ||
this.opacity.perFeature ||
this.stroke.perFeature ||
this.width.perFeature ||
options.perFeature)) !== null && _a !== void 0 ? _a : false;
this.doStroke = false;
}
before(ctx, z) {
if (!this.perFeature) {
ctx.globalAlpha = this.opacity.get(z);
ctx.fillStyle = this.fill.get(z);
ctx.strokeStyle = this.stroke.get(z);
const width = this.width.get(z);
if (width > 0)
this.doStroke = true;
ctx.lineWidth = width;
}
if (this.pattern) {
const patten = ctx.createPattern(this.pattern, "repeat");
if (patten)
ctx.fillStyle = patten;
}
}
draw(ctx, geom, z, f) {
let doStroke = false;
if (this.perFeature) {
ctx.globalAlpha = this.opacity.get(z, f);
ctx.fillStyle = this.fill.get(z, f);
const width = this.width.get(z, f);
if (width) {
doStroke = true;
ctx.strokeStyle = this.stroke.get(z, f);
ctx.lineWidth = width;
}
}
const drawPath = () => {
ctx.fill();
if (doStroke || this.doStroke) {
ctx.stroke();
}
};
ctx.beginPath();
for (const poly of geom) {
for (let p = 0; p < poly.length; p++) {
const pt = poly[p];
if (p === 0)
ctx.moveTo(pt.x, pt.y);
else
ctx.lineTo(pt.x, pt.y);
}
}
drawPath();
}
}
export function arr(base, a) {
return (z) => {
const b = z - base;
if (b >= 0 && b < a.length) {
return a[b];
}
return 0;
};
}
function getStopIndex(input, stops) {
let idx = 0;
while (stops[idx + 1][0] < input)
idx++;
return idx;
}
function interpolate(factor, start, end) {
return factor * (end - start) + start;
}
function computeInterpolationFactor(z, idx, base, stops) {
const difference = stops[idx + 1][0] - stops[idx][0];
const progress = z - stops[idx][0];
if (difference === 0)
return 0;
if (base === 1)
return progress / difference;
return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1);
}
export function exp(base, stops) {
return (z) => {
if (stops.length < 1)
return 0;
if (z <= stops[0][0])
return stops[0][1];
if (z >= stops[stops.length - 1][0])
return stops[stops.length - 1][1];
const idx = getStopIndex(z, stops);
const factor = computeInterpolationFactor(z, idx, base, stops);
return interpolate(factor, stops[idx][1], stops[idx + 1][1]);
};
}
export function step(output0, stops) {
// Step computes discrete results by evaluating a piecewise-constant
// function defined by stops.
// Returns the output value of the stop with a stop input value just less than
// the input one. If the input value is less than the input of the first stop,
// output0 is returned
return (z) => {
if (stops.length < 1)
return 0;
let retval = output0;
for (let i = 0; i < stops.length; i++) {
if (z >= stops[i][0])
retval = stops[i][1];
}
return retval;
};
}
export function linear(stops) {
return exp(1, stops);
}
export class LineSymbolizer {
constructor(options) {
var _a;
this.color = new StringAttr(options.color, "black");
this.width = new NumberAttr(options.width);
this.opacity = new NumberAttr(options.opacity);
this.dash = options.dash ? new ArrayAttr(options.dash) : null;
this.dashColor = new StringAttr(options.dashColor, "black");
this.dashWidth = new NumberAttr(options.dashWidth, 1.0);
this.lineCap = new StringAttr(options.lineCap, "butt");
this.lineJoin = new StringAttr(options.lineJoin, "miter");
this.skip = false;
this.perFeature = !!(((_a = this.dash) === null || _a === void 0 ? void 0 : _a.perFeature) ||
this.color.perFeature ||
this.opacity.perFeature ||
this.width.perFeature ||
this.lineCap.perFeature ||
this.lineJoin.perFeature ||
options.perFeature);
}
before(ctx, z) {
if (!this.perFeature) {
ctx.strokeStyle = this.color.get(z);
ctx.lineWidth = this.width.get(z);
ctx.globalAlpha = this.opacity.get(z);
ctx.lineCap = this.lineCap.get(z);
ctx.lineJoin = this.lineJoin.get(z);
}
}
draw(ctx, geom, z, f) {
if (this.skip)
return;
const strokePath = () => {
if (this.perFeature) {
ctx.globalAlpha = this.opacity.get(z, f);
ctx.lineCap = this.lineCap.get(z, f);
ctx.lineJoin = this.lineJoin.get(z, f);
}
if (this.dash) {
ctx.save();
if (this.perFeature) {
ctx.lineWidth = this.dashWidth.get(z, f);
ctx.strokeStyle = this.dashColor.get(z, f);
ctx.setLineDash(this.dash.get(z, f));
}
else {
ctx.setLineDash(this.dash.get(z));
}
ctx.stroke();
ctx.restore();
}
else {
ctx.save();
if (this.perFeature) {
ctx.lineWidth = this.width.get(z, f);
ctx.strokeStyle = this.color.get(z, f);
}
ctx.stroke();
ctx.restore();
}
};
ctx.beginPath();
for (const ls of geom) {
for (let p = 0; p < ls.length; p++) {
const pt = ls[p];
if (p === 0)
ctx.moveTo(pt.x, pt.y);
else
ctx.lineTo(pt.x, pt.y);
}
}
strokePath();
}
}
export class IconSymbolizer {
constructor(options) {
this.name = options.name;
this.sheet = options.sheet;
this.dpr = window.devicePixelRatio;
}
place(layout, geom, feature) {
const pt = geom[0];
const a = new Point(geom[0][0].x, geom[0][0].y);
const loc = this.sheet.get(this.name);
const width = loc.w / this.dpr;
const height = loc.h / this.dpr;
const bbox = {
minX: a.x - width / 2,
minY: a.y - height / 2,
maxX: a.x + width / 2,
maxY: a.y + height / 2,
};
const draw = (ctx) => {
ctx.globalAlpha = 1;
ctx.drawImage(this.sheet.canvas, loc.x, loc.y, loc.w, loc.h, -loc.w / 2 / this.dpr, -loc.h / 2 / this.dpr, loc.w / 2, loc.h / 2);
};
return [{ anchor: a, bboxes: [bbox], draw: draw }];
}
}
export class CircleSymbolizer {
constructor(options) {
this.radius = new NumberAttr(options.radius, 3);
this.fill = new StringAttr(options.fill, "black");
this.stroke = new StringAttr(options.stroke, "white");
this.width = new NumberAttr(options.width, 0);
this.opacity = new NumberAttr(options.opacity);
}
draw(ctx, geom, z, f) {
ctx.globalAlpha = this.opacity.get(z, f);
const radius = this.radius.get(z, f);
const width = this.width.get(z, f);
if (width > 0) {
ctx.strokeStyle = this.stroke.get(z, f);
ctx.lineWidth = width;
ctx.beginPath();
ctx.arc(geom[0][0].x, geom[0][0].y, radius + width / 2, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.fillStyle = this.fill.get(z, f);
ctx.beginPath();
ctx.arc(geom[0][0].x, geom[0][0].y, radius, 0, 2 * Math.PI);
ctx.fill();
}
place(layout, geom, feature) {
const pt = geom[0];
const a = new Point(geom[0][0].x, geom[0][0].y);
const radius = this.radius.get(layout.zoom, feature);
const bbox = {
minX: a.x - radius,
minY: a.y - radius,
maxX: a.x + radius,
maxY: a.y + radius,
};
const draw = (ctx) => {
this.draw(ctx, [[new Point(0, 0)]], layout.zoom, feature);
};
return [{ anchor: a, bboxes: [bbox], draw }];
}
}
export class ShieldSymbolizer {
constructor(options) {
this.font = new FontAttr(options);
this.text = new TextAttr(options);
this.fill = new StringAttr(options.fill, "black");
this.background = new StringAttr(options.background, "white");
this.padding = new NumberAttr(options.padding, 0); // TODO check falsy
}
place(layout, geom, f) {
const property = this.text.get(layout.zoom, f);
if (!property)
return undefined;
const font = this.font.get(layout.zoom, f);
layout.scratch.font = font;
const metrics = layout.scratch.measureText(property);
const width = metrics.width;
const ascent = metrics.actualBoundingBoxAscent;
const descent = metrics.actualBoundingBoxDescent;
const pt = geom[0];
const a = new Point(geom[0][0].x, geom[0][0].y);
const p = this.padding.get(layout.zoom, f);
const bbox = {
minX: a.x - width / 2 - p,
minY: a.y - ascent - p,
maxX: a.x + width / 2 + p,
maxY: a.y + descent + p,
};
const draw = (ctx) => {
ctx.globalAlpha = 1;
ctx.fillStyle = this.background.get(layout.zoom, f);
ctx.fillRect(-width / 2 - p, -ascent - p, width + 2 * p, ascent + descent + 2 * p);
ctx.fillStyle = this.fill.get(layout.zoom, f);
ctx.font = font;
ctx.fillText(property, -width / 2, 0);
};
return [{ anchor: a, bboxes: [bbox], draw: draw }];
}
}
// TODO make me work with multiple anchors
export class FlexSymbolizer {
constructor(list) {
this.list = list;
}
place(layout, geom, feature) {
let labels = this.list[0].place(layout, geom, feature);
if (!labels)
return undefined;
let label = labels[0];
const anchor = label.anchor;
let bbox = label.bboxes[0];
const height = bbox.maxY - bbox.minY;
const draws = [{ draw: label.draw, translate: { x: 0, y: 0 } }];
const newGeom = [[new Point(geom[0][0].x, geom[0][0].y + height)]];
for (let i = 1; i < this.list.length; i++) {
labels = this.list[i].place(layout, newGeom, feature);
if (labels) {
label = labels[0];
bbox = mergeBbox(bbox, label.bboxes[0]);
draws.push({ draw: label.draw, translate: { x: 0, y: height } });
}
}
const draw = (ctx) => {
for (const sub of draws) {
ctx.save();
ctx.translate(sub.translate.x, sub.translate.y);
sub.draw(ctx);
ctx.restore();
}
};
return [{ anchor: anchor, bboxes: [bbox], draw: draw }];
}
}
const mergeBbox = (b1, b2) => {
return {
minX: Math.min(b1.minX, b2.minX),
minY: Math.min(b1.minY, b2.minY),
maxX: Math.max(b1.maxX, b2.maxX),
maxY: Math.max(b1.maxY, b2.maxY),
};
};
export class GroupSymbolizer {
constructor(list) {
this.list = list;
}
place(layout, geom, feature) {
const first = this.list[0];
if (!first)
return undefined;
let labels = first.place(layout, geom, feature);
if (!labels)
return undefined;
let label = labels[0];
const anchor = label.anchor;
let bbox = label.bboxes[0];
const draws = [label.draw];
for (let i = 1; i < this.list.length; i++) {
labels = this.list[i].place(layout, geom, feature);
if (!labels)
return undefined;
label = labels[0];
bbox = mergeBbox(bbox, label.bboxes[0]);
draws.push(label.draw);
}
const draw = (ctx) => {
for (const d of draws) {
d(ctx);
}
};
return [{ anchor: anchor, bboxes: [bbox], draw: draw }];
}
}
export class CenteredSymbolizer {
constructor(symbolizer) {
this.symbolizer = symbolizer;
}
place(layout, geom, feature) {
const a = geom[0][0];
const placed = this.symbolizer.place(layout, [[new Point(0, 0)]], feature);
if (!placed || placed.length === 0)
return undefined;
const firstLabel = placed[0];
const bbox = firstLabel.bboxes[0];
const width = bbox.maxX - bbox.minX;
const height = bbox.maxY - bbox.minY;
const centered = {
minX: a.x - width / 2,
maxX: a.x + width / 2,
minY: a.y - height / 2,
maxY: a.y + height / 2,
};
const draw = (ctx) => {
ctx.translate(-width / 2, height / 2 - bbox.maxY);
firstLabel.draw(ctx, { justify: Justify.Center });
};
return [{ anchor: a, bboxes: [centered], draw: draw }];
}
}
export class Padding {
constructor(padding, symbolizer) {
this.padding = new NumberAttr(padding, 0);
this.symbolizer = symbolizer;
}
place(layout, geom, feature) {
const placed = this.symbolizer.place(layout, geom, feature);
if (!placed || placed.length === 0)
return undefined;
const padding = this.padding.get(layout.zoom, feature);
for (const label of placed) {
for (const bbox of label.bboxes) {
bbox.minX -= padding;
bbox.minY -= padding;
bbox.maxX += padding;
bbox.maxY += padding;
}
}
return placed;
}
}
export class TextSymbolizer {
constructor(options) {
this.font = new FontAttr(options);
this.text = new TextAttr(options);
this.fill = new StringAttr(options.fill, "black");
this.stroke = new StringAttr(options.stroke, "black");
this.width = new NumberAttr(options.width, 0);
this.lineHeight = new NumberAttr(options.lineHeight, 1);
this.letterSpacing = new NumberAttr(options.letterSpacing, 0);
this.maxLineCodeUnits = new NumberAttr(options.maxLineChars, 15);
this.justify = options.justify;
}
place(layout, geom, feature) {
const property = this.text.get(layout.zoom, feature);
if (!property)
return undefined;
const font = this.font.get(layout.zoom, feature);
layout.scratch.font = font;
const letterSpacing = this.letterSpacing.get(layout.zoom, feature);
// line breaking
const lines = linebreak(property, this.maxLineCodeUnits.get(layout.zoom, feature));
let longestLine = "";
let longestLineLen = 0;
for (const line of lines) {
if (line.length > longestLineLen) {
longestLineLen = line.length;
longestLine = line;
}
}
const metrics = layout.scratch.measureText(longestLine);
const width = metrics.width + letterSpacing * (longestLineLen - 1);
const ascent = metrics.actualBoundingBoxAscent;
const descent = metrics.actualBoundingBoxDescent;
const lineHeight = (ascent + descent) * this.lineHeight.get(layout.zoom, feature);
const a = new Point(geom[0][0].x, geom[0][0].y);
const bbox = {
minX: a.x,
minY: a.y - ascent,
maxX: a.x + width,
maxY: a.y + descent + (lines.length - 1) * lineHeight,
};
// inside draw, the origin is the anchor
// and the anchor is the typographic baseline of the first line
const draw = (ctx, extra) => {
ctx.globalAlpha = 1;
ctx.font = font;
ctx.fillStyle = this.fill.get(layout.zoom, feature);
const textStrokeWidth = this.width.get(layout.zoom, feature);
let y = 0;
for (const line of lines) {
let startX = 0;
if (this.justify === Justify.Center ||
(extra && extra.justify === Justify.Center)) {
startX = (width - ctx.measureText(line).width) / 2;
}
else if (this.justify === Justify.Right ||
(extra && extra.justify === Justify.Right)) {
startX = width - ctx.measureText(line).width;
}
if (textStrokeWidth) {
ctx.lineWidth = textStrokeWidth * 2; // centered stroke
ctx.strokeStyle = this.stroke.get(layout.zoom, feature);
if (letterSpacing > 0) {
let xPos = startX;
for (const letter of line) {
ctx.strokeText(letter, xPos, y);
xPos += ctx.measureText(letter).width + letterSpacing;
}
}
else {
ctx.strokeText(line, startX, y);
}
}
if (letterSpacing > 0) {
let xPos = startX;
for (const letter of line) {
ctx.fillText(letter, xPos, y);
xPos += ctx.measureText(letter).width + letterSpacing;
}
}
else {
ctx.fillText(line, startX, y);
}
y += lineHeight;
}
};
return [{ anchor: a, bboxes: [bbox], draw: draw }];
}
}
export class CenteredTextSymbolizer {
constructor(options) {
this.centered = new CenteredSymbolizer(new TextSymbolizer(options));
}
place(layout, geom, feature) {
return this.centered.place(layout, geom, feature);
}
}
export class OffsetSymbolizer {
constructor(symbolizer, options) {
var _a, _b, _c;
this.symbolizer = symbolizer;
this.offsetX = new NumberAttr(options.offsetX, 0);
this.offsetY = new NumberAttr(options.offsetY, 0);
this.justify = (_a = options.justify) !== null && _a !== void 0 ? _a : undefined;
this.placements = (_b = options.placements) !== null && _b !== void 0 ? _b : [
TextPlacements.Ne,
TextPlacements.Sw,
TextPlacements.Nw,
TextPlacements.Se,
TextPlacements.N,
TextPlacements.E,
TextPlacements.S,
TextPlacements.W,
];
this.ddValues =
(_c = options.ddValues) !== null && _c !== void 0 ? _c : (() => {
return {};
});
}
place(layout, geom, feature) {
if (feature.geomType !== GeomType.Point)
return undefined;
const anchor = geom[0][0];
const placed = this.symbolizer.place(layout, [[new Point(0, 0)]], feature);
if (!placed || placed.length === 0)
return undefined;
const firstLabel = placed[0];
const fb = firstLabel.bboxes[0];
// Overwrite options values via the data driven function if exists
let offsetXvalue = this.offsetX;
let offsetYvalue = this.offsetY;
let justifyValue = this.justify;
let placements = this.placements;
const { offsetX: ddOffsetX, offsetY: ddOffsetY, justify: ddJustify, placements: ddPlacements, } = this.ddValues(layout.zoom, feature) || {};
if (ddOffsetX)
offsetXvalue = new NumberAttr(ddOffsetX, 0);
if (ddOffsetY)
offsetYvalue = new NumberAttr(ddOffsetY, 0);
if (ddJustify)
justifyValue = ddJustify;
if (ddPlacements)
placements = ddPlacements;
const offsetX = offsetXvalue.get(layout.zoom, feature);
const offsetY = offsetYvalue.get(layout.zoom, feature);
const getBbox = (a, o) => {
return {
minX: a.x + o.x + fb.minX,
minY: a.y + o.y + fb.minY,
maxX: a.x + o.x + fb.maxX,
maxY: a.y + o.y + fb.maxY,
};
};
let origin = new Point(offsetX, offsetY);
let justify;
const draw = (ctx) => {
ctx.translate(origin.x, origin.y);
firstLabel.draw(ctx, { justify: justify });
};
const placeLabelInPoint = (a, o) => {
const bbox = getBbox(a, o);
if (!layout.index.bboxCollides(bbox, layout.order))
return [{ anchor: anchor, bboxes: [bbox], draw: draw }];
};
for (const placement of placements) {
const xAxisOffset = this.computeXaxisOffset(offsetX, fb, placement);
const yAxisOffset = this.computeYaxisOffset(offsetY, fb, placement);
justify = this.computeJustify(justifyValue, placement);
origin = new Point(xAxisOffset, yAxisOffset);
return placeLabelInPoint(anchor, origin);
}
return undefined;
}
computeXaxisOffset(offsetX, fb, placement) {
const labelWidth = fb.maxX;
const labelHalfWidth = labelWidth / 2;
if ([TextPlacements.N, TextPlacements.S].includes(placement))
return offsetX - labelHalfWidth;
if ([TextPlacements.Nw, TextPlacements.W, TextPlacements.Sw].includes(placement))
return offsetX - labelWidth;
return offsetX;
}
computeYaxisOffset(offsetY, fb, placement) {
const labelHalfHeight = Math.abs(fb.minY);
const labelBottom = fb.maxY;
const labelCenterHeight = (fb.minY + fb.maxY) / 2;
if ([TextPlacements.E, TextPlacements.W].includes(placement))
return offsetY - labelCenterHeight;
if ([TextPlacements.Nw, TextPlacements.Ne, TextPlacements.N].includes(placement))
return offsetY - labelBottom;
if ([TextPlacements.Sw, TextPlacements.Se, TextPlacements.S].includes(placement))
return offsetY + labelHalfHeight;
return offsetY;
}
computeJustify(fixedJustify, placement) {
if (fixedJustify)
return fixedJustify;
if ([TextPlacements.N, TextPlacements.S].includes(placement))
return Justify.Center;
if ([TextPlacements.Ne, TextPlacements.E, TextPlacements.Se].includes(placement))
return Justify.Left;
return Justify.Right;
}
}
export class OffsetTextSymbolizer {
constructor(options) {
this.symbolizer = new OffsetSymbolizer(new TextSymbolizer(options), options);
}
place(layout, geom, feature) {
return this.symbolizer.place(layout, geom, feature);
}
}
export var LineLabelPlacement;
(function (LineLabelPlacement) {
LineLabelPlacement[LineLabelPlacement["Above"] = 1] = "Above";
LineLabelPlacement[LineLabelPlacement["Center"] = 2] = "Center";
LineLabelPlacement[LineLabelPlacement["Below"] = 3] = "Below";
})(LineLabelPlacement || (LineLabelPlacement = {}));
export class LineLabelSymbolizer {
constructor(options) {
var _a;
this.font = new FontAttr(options);
this.text = new TextAttr(options);
this.fill = new StringAttr(options.fill, "black");
this.stroke = new StringAttr(options.stroke, "black");
this.width = new NumberAttr(options.width, 0);
this.offset = new NumberAttr(options.offset, 0);
this.position = (_a = options.position) !== null && _a !== void 0 ? _a : LineLabelPlacement.Above;
this.maxLabelCodeUnits = new NumberAttr(options.maxLabelChars, 40);
this.repeatDistance = new NumberAttr(options.repeatDistance, 250);
}
place(layout, geom, feature) {
const name = this.text.get(layout.zoom, feature);
if (!name)
return undefined;
if (name.length > this.maxLabelCodeUnits.get(layout.zoom, feature))
return undefined;
const minLabelableDim = 20;
const fbbox = feature.bbox;
if (fbbox.maxY - fbbox.minY < minLabelableDim &&
fbbox.maxX - fbbox.minX < minLabelableDim)
return undefined;
const font = this.font.get(layout.zoom, feature);
layout.scratch.font = font;
const metrics = layout.scratch.measureText(name);
const width = metrics.width;
const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
let repeatDistance = this.repeatDistance.get(layout.zoom, feature);
if (layout.overzoom > 4)
repeatDistance *= 1 << (layout.overzoom - 4);
const cellSize = height * 2;
const labelCandidates = simpleLabel(geom, width, repeatDistance, cellSize);
if (labelCandidates.length === 0)
return undefined;
const labels = [];
for (const candidate of labelCandidates) {
const dx = candidate.end.x - candidate.start.x;
const dy = candidate.end.y - candidate.start.y;
const cells = lineCells(candidate.start, candidate.end, width, cellSize / 2);
const bboxes = cells.map((c) => {
return {
minX: c.x - cellSize / 2,
minY: c.y - cellSize / 2,
maxX: c.x + cellSize / 2,
maxY: c.y + cellSize / 2,
};
});
const draw = (ctx) => {
ctx.globalAlpha = 1;
// ctx.beginPath();
// ctx.moveTo(0, 0);
// ctx.lineTo(dx, dy);
// ctx.strokeStyle = "red";
// ctx.stroke();
ctx.rotate(Math.atan2(dy, dx));
if (dx < 0) {
ctx.scale(-1, -1);
ctx.translate(-width, 0);
}
let heightPlacement = 0;
if (this.position === LineLabelPlacement.Below)
heightPlacement += height;
else if (this.position === LineLabelPlacement.Center)
heightPlacement += height / 2;
ctx.translate(0, heightPlacement - this.offset.get(layout.zoom, feature));
ctx.font = font;
const lineWidth = this.width.get(layout.zoom, feature);
if (lineWidth) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = this.stroke.get(layout.zoom, feature);
ctx.strokeText(name, 0, 0);
}
ctx.fillStyle = this.fill.get(layout.zoom, feature);
ctx.fillText(name, 0, 0);
};
labels.push({
anchor: candidate.start,
bboxes: bboxes,
draw: draw,
deduplicationKey: name,
deduplicationDistance: repeatDistance,
});
}
return labels;
}
}