aboutsummaryrefslogtreecommitdiffstats
path: root/app/svg.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/svg.js')
-rw-r--r--app/svg.js327
1 files changed, 160 insertions, 167 deletions
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<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
+ * Class representing options for SVG elements
+ * @property {Record<string, string>} 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 SvgOptions {
+ attributes;
+ styles;
+ id;
+ classes;
+ children;
+
+ /**
+ * @param {Object} [options]
+ * @param {Record<string, string>} [options.attributes] - SVG attributes
+ * @param {Record<string, string>} [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;
}
/**