gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
167 lines • 5.88 kB
JavaScript
import { equalPoints, xmlFindTagsByName, xmlGetAttribute } from '../../index.js';
/**
* # SVG Reader
*
* ## Description
*
* Reads data from an SVG string. The assumed projection is WGS84
*
* Implements the {@link FeatureIterator} interface
*
* ## Usage
*
* ```ts
* import { SVGReader } from 'gis-tools-ts';
*
* const reader = new SVGReader('svg-text');
*
* // read all the features
* for await (const feature of reader) {
* console.log(feature);
* }
* ```
*
* ## Links
* - <https://en.wikipedia.org/wiki/SVG>
*/
export class SVGReader {
input;
extent;
features = [];
constructor(input) {
this.input = input;
// build extent
const viewBox = xmlGetAttribute(input, 'viewBox');
if (viewBox !== undefined) {
const [minx, miny, width, height] = viewBox.split(' ').map(Number);
this.extent = [minx, miny, minx + width, miny + height];
}
else {
throw new Error('Could not find viewBox in SVG.');
}
// build features
this.#buildFeatures();
}
#buildFeatures() {
this.#buildPaths();
// TODO: circle - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/circle
// TODO: ellipse - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/ellipse
// TODO: rect - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/rect
// TODO: line - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/line
// TODO: polyline - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/polyline
// TODO: polygon - https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/polygon
// TODO: text - https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
}
#buildPaths() {
for (const { stroke, strokeWidth, fill, d } of pathsFromSVG(this.input)) {
const points = getPointsFromSVGPath(d, this.extent);
const properties = { stroke, strokeWidth, fill };
if (fill !== undefined) {
// Ensure it self closes
if (!equalPoints(points[0], points[points.length - 1])) {
points.push(points[0]);
}
this.features.push({
type: 'VectorFeature',
geometry: { type: 'Polygon', coordinates: [points], is3D: false },
properties,
});
}
else {
this.features.push({
type: 'VectorFeature',
geometry: { type: 'LineString', coordinates: points, is3D: false },
properties,
});
}
}
}
/**
* Iterate over all features in the shapefile
* @yields {VectorFeature}
*/
async *[Symbol.asyncIterator]() {
for (let i = 0; i < this.features.length; i++) {
const feature = this.features[i];
if (feature !== undefined)
yield feature;
}
}
}
export function pathsFromSVG(svg) {
const pathTags = xmlFindTagsByName(svg, 'path');
return pathTags.map(pathFromSVG);
}
export function pathFromSVG(path) {
return {
// The raw "d" attribute string (e.g., "M10,10 L20,20...")
d: xmlGetAttribute(path, 'd') ?? '',
stroke: xmlGetAttribute(path, 'stroke'),
strokeWidth: xmlGetAttribute(path, 'stroke-width'),
fill: xmlGetAttribute(path, 'fill'),
};
}
export function getPointsFromSVGPath(d, extent) {
const [minX, minY, maxX, maxY] = extent;
const width = maxX - minX;
const height = maxY - minY;
const commands = d
.split(' ')
.map((s) => s.trim())
.filter(Boolean);
const points = [];
const cursor = { x: 0, y: 0 };
let lastPoint = { x: 0, y: 0 };
let x = 0;
let y = 0;
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
const isRelative = cmd === cmd.toLowerCase();
const type = cmd.toUpperCase();
// The next element in the array contains the numbers for this command
switch (type) {
case 'M': // MoveTo
x = Number(commands[i + 1]);
y = Number(commands[i + 2]);
cursor.x = isRelative ? cursor.x + x : x;
cursor.y = isRelative ? cursor.y + y : y;
points.push({ ...cursor });
i += 2; // skip the args
break;
case 'L': // LineTo
x = Number(commands[i + 1]);
y = Number(commands[i + 2]);
cursor.x = isRelative ? cursor.x + x : x;
cursor.y = isRelative ? cursor.y + y : y;
points.push({ ...cursor });
i += 2; // skip the args
break;
case 'H': // Horizontal
x = Number(commands[i + 1]);
cursor.x = isRelative ? cursor.x + x : x;
points.push({ ...cursor });
i++;
break;
case 'V': // Vertical
y = Number(commands[i + 1]);
cursor.y = isRelative ? cursor.y + y : y;
points.push({ ...cursor });
i++;
break;
case 'Z': // Close
points.push({ ...lastPoint });
break;
// TODO: C, S, Q, T, A: interpolations
}
lastPoint = { ...cursor };
}
// remap to extent
for (const p of points) {
// Normalize X: (Value - Offset) / Total Range
p.x = (p.x - minX) / width;
// Normalize Y and Flip: 1 - ((Value - Offset) / Total Range)
p.y = 1 - (p.y - minY) / height;
}
return points;
}
//# sourceMappingURL=index.js.map