import { LineString, Point, Polygon } from "geom"; export class SvgOptions { attributes; children; classes; id; styles; /** * @param {Object} [options] * @param {Record} [options.attributes] - SVG attributes * @param {Record} [options.styles] - CSS styles * @param {string} [options.id] - Element ID * @param {string[]} [options.classes] - CSS classes * @param {SVGElement[]} [options.children] - Child elements */ constructor({ attributes = {}, styles = {}, id = "", classes = [], children = [] } = {}) { this.attributes = attributes; this.children = children; this.classes = classes; this.id = id; this.styles = styles; } } export class Svg { /** * Create a rectangle with explicit coordinates * @param {number} x * @param {number} y * @param {number} width * @param {number} height * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGRectElement} */ static rectXY(x, y, width, height, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "rect"); for (const [key, value] of Object.entries({ height: String(height), width: String(width), x: String(x), y: String(y), ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a text element at specific coordinates * @param {number} x * @param {number} y * @param {string} text * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGTextElement} */ static textXY(x, y, text, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "text"); element.textContent = text; for (const [key, value] of Object.entries({ x: String(x), y: String(y), ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a line element * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGLineElement} */ static line(x1, y1, x2, y2, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "line"); for (const [key, value] of Object.entries({ x1: String(x1), x2: String(x2), y1: String(y1), y2: String(y2), ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create an SVG element * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGSVGElement} */ static svg(options = new SvgOptions()) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); for (const [key, value] of Object.entries({ preserveAspectRatio: "xMidYMid slice", ...options.attributes, })) { svg.setAttribute(key, value); } Object.assign(svg.style, options.styles); if (options.id) svg.id = options.id; if (options.classes.length) svg.classList.add(...options.classes.filter(Boolean)); svg.append(...options.children.filter(Boolean)); return svg; } /** * Create an SVG title element for tooltips * @param {string} text - The tooltip text * @returns {SVGTitleElement} */ static title(text) { const title = document.createElementNS("http://www.w3.org/2000/svg", "title"); title.textContent = text; return title; } /** * Create a group element * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGGElement} */ static g(options = new SvgOptions()) { const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); for (const [key, value] of Object.entries(options.attributes)) { g.setAttribute(key, value); } Object.assign(g.style, options.styles); if (options.id) g.id = options.id; if (options.classes.length) g.classList.add(...options.classes.filter(Boolean)); g.append(...options.children.filter(Boolean)); return g; } /** * Create a polygon from Polygon geometry * @param {Polygon} polygon * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPolygonElement} */ static polygon(polygon, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); const points = polygon .getExterior() .map(([x, y]) => `${x},${y}`) .join(" "); element.setAttribute("points", points); for (const [key, value] of Object.entries(options.attributes)) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a path from LineString geometry * @param {Array<[number, number]>} coords */ static getPath(coords) { const [startLng, startLat] = coords[0]; let pathData = `M ${startLng},${startLat}`; for (let i = 1; i < coords.length; i++) { const [lng, lat] = coords[i]; pathData += ` L ${lng},${lat}`; } return pathData; } /** * Create a path from LineString geometry * @param {LineString} lineString * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPathElement} */ static path(lineString, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "path"); element.setAttribute("d", Svg.getPath(lineString.coordinates)); for (const [key, value] of Object.entries(options.attributes)) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a circle from Point geometry * @param {Point} point * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGCircleElement} */ static circle(point, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "circle"); for (const [key, value] of Object.entries({ cx: String(point.lng), cy: String(point.lat), r: "0.002", ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a rectangle * @param {Point} point * @param {number} width - Rectangle width * @param {number} height - Rectangle height * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGRectElement} */ static rect(point, width, height, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "rect"); for (const [key, value] of Object.entries({ height: String(height), width: String(width), x: String(point.lng), y: String(point.lat), ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a text element * @param {Point} point - point * @param {string} text - Text content * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGTextElement} */ static text(point, text, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "text"); element.textContent = text; for (const [key, value] of Object.entries({ x: String(point.lng), y: String(point.lat), ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a circle element with explicit coordinates * @param {Point} point - Center X coordinate * @param {number} r - Radius * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGCircleElement} */ static circleXY(point, r, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "circle"); for (const [key, value] of Object.entries({ cx: String(point.lng), cy: String(point.lat), fill: "#4caf50", r: String(r), stroke: "#333", "stroke-width": "0.001", ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a path from raw path data * @param {string} pathData - SVG path data string * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPathElement} */ static pathData(pathData, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "path"); for (const [key, value] of Object.entries({ d: pathData, fill: "none", stroke: "#000", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "0.001", ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } /** * Create a polygon from points array * @param {Array<[number, number]>} points - Array of [x,y] points * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPolygonElement} */ static polygonPoints(points, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); const pointsString = points.map(([x, y]) => `${x},${y}`).join(" "); for (const [key, value] of Object.entries({ points: pointsString, ...options.attributes, })) { element.setAttribute(key, value); } Object.assign(element.style, options.styles); if (options.id) element.id = options.id; if (options.classes.length) element.classList.add(...options.classes.filter(Boolean)); element.append(...options.children.filter(Boolean)); return element; } }