diff options
Diffstat (limited to 'app/svg.js')
| -rw-r--r-- | app/svg.js | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/app/svg.js b/app/svg.js new file mode 100644 index 0000000..61d3717 --- /dev/null +++ b/app/svg.js @@ -0,0 +1,304 @@ +import { LineString, Point, Polygon } from "geom"; + +/** + * @typedef {Object} SvgOptions + * @property {Record<string, string|number>} [attributes] - SVG attributes + * @property {Record<string, string>} [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); + } + } +} |
