aboutsummaryrefslogtreecommitdiffstats
path: root/app/svg.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/svg.js')
-rw-r--r--app/svg.js304
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);
+ }
+ }
+}