import { LineString, Point, Polygon } from "geom"; /** * @typedef {Object} SvgOptions * @property {Record} [attributes] - SVG attributes * @property {Record} [styles] - CSS styles * @property {string} [id] - Element ID * @property {string[]} [classes] - CSS classes * @property {SVGElement[]} [children] - Child elements */ export class Svg { /** * Create an SVG element * @param {SvgOptions} [options={}] * @returns {SVGSVGElement} */ static svg(options = {}) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const defaultAttributes = { preserveAspectRatio: "xMidYMid slice", ...options.attributes, }; Svg.#applyOptions(svg, { ...options, attributes: defaultAttributes }); 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={}] * @returns {SVGGElement} */ static g(options = {}) { const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); Svg.#applyOptions(g, options); return g; } /** * Create a polygon from Polygon geometry * @param {Polygon} polygon * @param {SvgOptions} [options={}] * @returns {SVGPolygonElement} */ static polygon(polygon, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); // Convert polygon rings to SVG points string const exteriorRing = polygon.getExterior(); const points = exteriorRing.map(([x, y]) => `${x},${y}`).join(" "); const defaultAttributes = { fill: "rgba(100,150,255,0.2)", points, stroke: "#555", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a path from LineString geometry * @param {LineString} lineString * @param {SvgOptions} [options={}] * @returns {SVGPathElement} */ static path(lineString, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "path"); // Convert LineString coordinates to SVG path data const coords = lineString.coordinates; let pathData = ""; if (coords.length > 0) { const [startLng, startLat] = coords[0]; pathData = `M ${startLng},${startLat}`; for (let i = 1; i < coords.length; i++) { const [lng, lat] = coords[i]; pathData += ` L ${lng},${lat}`; } } const defaultAttributes = { d: pathData, fill: "none", stroke: "#000", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a circle from Point geometry * @param {Point} point * @param {SvgOptions} [options={}] * @returns {SVGCircleElement} */ static circle(point, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "circle"); const defaultAttributes = { cx: point.lng, cy: point.lat, fill: "#4caf50", r: 0.002, stroke: "#333", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a rectangle * @param {Point} point * @param {number} width - Rectangle width * @param {number} height - Rectangle height * @param {SvgOptions} [options={}] * @returns {SVGRectElement} */ static rect(point, width, height, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "rect"); const defaultAttributes = { height, width, x: point.lng, y: point.lat, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a text element * @param {Point} point - point * @param {string} text - Text content * @param {SvgOptions} [options={}] * @returns {SVGTextElement} */ static text(point, text, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "text"); const defaultAttributes = { x: point.lng, y: point.lat, ...options.attributes, }; const defaultStyles = { dominantBaseline: "middle", fill: "#333", fontSize: "0.005", pointerEvents: "none", textAnchor: "middle", ...options.styles, }; element.textContent = text; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes, styles: defaultStyles, }); return element; } /** * Create a circle element with explicit coordinates * @param {Point} point - Center X coordinate * @param {number} r - Radius * @param {SvgOptions} [options={}] * @returns {SVGCircleElement} */ static circleXY(point, r, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "circle"); const defaultAttributes = { cx: point.lng, cy: point.lat, fill: "#4caf50", r, stroke: "#333", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a path from raw path data * @param {string} pathData - SVG path data string * @param {SvgOptions} [options={}] * @returns {SVGPathElement} */ static pathData(pathData, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "path"); const defaultAttributes = { d: pathData, fill: "none", stroke: "#000", "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Create a polygon from points array * @param {Array<[number, number]>} points - Array of [x,y] points * @param {SvgOptions} [options={}] * @returns {SVGPolygonElement} */ static polygonPoints(points, options = {}) { const element = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); const pointsString = points.map(([x, y]) => `${x},${y}`).join(" "); const defaultAttributes = { fill: "rgba(100,150,255,0.2)", points: pointsString, stroke: "#555", "stroke-width": 0.001, ...options.attributes, }; Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); return element; } /** * Apply options to SVG element * @param {SVGElement} element * @param {SvgOptions} options */ static #applyOptions(element, options = {}) { const { attributes = {}, styles = {}, id = "", classes = [], children = [] } = options; // Set attributes for (const [key, value] of Object.entries(attributes)) { if (value != null) { element.setAttribute(key, value.toString()); } } // Set styles for (const [property, value] of Object.entries(styles)) { if (value != null) { element.style[property] = value; } } // Set ID and classes if (id) element.id = id; if (classes.length > 0) { element.classList.add(...classes.filter(Boolean)); } for (const child of children) { if (child) { element.appendChild(child); } } } /** * Clear all children from an element * @param {SVGElement} element */ static clear(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } }