From a4ed99a370930b1a0c0f065906ed99c15a015fd4 Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Mon, 3 Nov 2025 11:19:15 +0200 Subject: Update documentation --- app/svg.js | 327 ++++++++++++++++++++++++++++++------------------------------- 1 file changed, 160 insertions(+), 167 deletions(-) (limited to 'app/svg.js') diff --git a/app/svg.js b/app/svg.js index 61d3717..24f6a3d 100644 --- a/app/svg.js +++ b/app/svg.js @@ -1,27 +1,55 @@ 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 + * Class representing options for SVG elements + * @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 SvgOptions { + attributes; + styles; + id; + classes; + children; + + /** + * @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.styles = styles; + this.id = id; + this.classes = classes; + this.children = children; + } +} export class Svg { /** * Create an SVG element - * @param {SvgOptions} [options={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGSVGElement} */ - static svg(options = {}) { + static svg(options = new SvgOptions()) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - const defaultAttributes = { + for (const [key, value] of Object.entries({ preserveAspectRatio: "xMidYMid slice", ...options.attributes, - }; - Svg.#applyOptions(svg, { ...options, attributes: defaultAttributes }); + })) { + 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; } @@ -38,96 +66,97 @@ export class Svg { /** * Create a group element - * @param {SvgOptions} [options={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGGElement} */ - static g(options = {}) { + static g(options = new SvgOptions()) { const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); - Svg.#applyOptions(g, options); + 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={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPolygonElement} */ - static polygon(polygon, options = {}) { + static polygon(polygon, options = new SvgOptions()) { 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 }); + 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={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPathElement} */ - static path(lineString, options = {}) { + static path(lineString, options = new SvgOptions()) { 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}`; - } + element.setAttribute("d", Svg.getPath(lineString.coordinates)); + for (const [key, value] of Object.entries({ fill: "none", ...options.attributes })) { + element.setAttribute(key, value); } - - 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 }); + 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={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGCircleElement} */ - static circle(point, options = {}) { + static circle(point, options = new SvgOptions()) { 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, + for (const [key, value] of Object.entries({ + cx: String(point.lng), + cy: String(point.lat), + r: "0.002", ...options.attributes, - }; - - Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); + })) { + 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; } @@ -136,21 +165,24 @@ export class Svg { * @param {Point} point * @param {number} width - Rectangle width * @param {number} height - Rectangle height - * @param {SvgOptions} [options={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGRectElement} */ - static rect(point, width, height, options = {}) { + static rect(point, width, height, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - - const defaultAttributes = { - height, - width, - x: point.lng, - y: point.lat, + for (const [key, value] of Object.entries({ + height: String(height), + width: String(width), + x: String(point.lng), + y: String(point.lat), ...options.attributes, - }; - - Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); + })) { + 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; } @@ -158,32 +190,23 @@ export class Svg { * Create a text element * @param {Point} point - point * @param {string} text - Text content - * @param {SvgOptions} [options={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGTextElement} */ - static text(point, text, options = {}) { + static text(point, text, options = new SvgOptions()) { 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, - }); + 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; } @@ -191,105 +214,75 @@ export class Svg { * Create a circle element with explicit coordinates * @param {Point} point - Center X coordinate * @param {number} r - Radius - * @param {SvgOptions} [options={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGCircleElement} */ - static circleXY(point, r, options = {}) { + static circleXY(point, r, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - - const defaultAttributes = { - cx: point.lng, - cy: point.lat, + for (const [key, value] of Object.entries({ + cx: String(point.lng), + cy: String(point.lat), fill: "#4caf50", - r, + r: String(r), stroke: "#333", - "stroke-width": 0.001, + "stroke-width": "0.001", ...options.attributes, - }; - - Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); + })) { + 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={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPathElement} */ - static pathData(pathData, options = {}) { + static pathData(pathData, options = new SvgOptions()) { const element = document.createElementNS("http://www.w3.org/2000/svg", "path"); - - const defaultAttributes = { + for (const [key, value] of Object.entries({ d: pathData, fill: "none", stroke: "#000", "stroke-linecap": "round", "stroke-linejoin": "round", - "stroke-width": 0.001, + "stroke-width": "0.001", ...options.attributes, - }; - - Svg.#applyOptions(element, { ...options, attributes: defaultAttributes }); + })) { + 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={}] + * @param {SvgOptions} [options=new SvgOptions()] * @returns {SVGPolygonElement} */ - static polygonPoints(points, options = {}) { + 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(" "); - - const defaultAttributes = { - fill: "rgba(100,150,255,0.2)", + for (const [key, value] of Object.entries({ 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); - } + })) { + 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; } /** -- cgit v1.2.3-70-g09d2