Drawings - draw as different shapes

This commit is contained in:
ziajka 2018-05-21 12:59:20 +02:00
parent 0b47232f89
commit 6002ebacdf
20 changed files with 587 additions and 20 deletions

View File

@ -1,4 +1,5 @@
import { SvgToDrawingConverter } from "./svg-to-drawing-converter";
import { TextElement } from "../../shared/models/drawings/text-element";
describe('SvgToDrawingHelper', () => {
@ -32,4 +33,14 @@ describe('SvgToDrawingHelper', () => {
expect(drawing.width).toBe(78);
expect(drawing.height).toBe(53);
});
it('should parse element even when is text between', () => {
const svg = '<svg height="53" width="78"> <text>Label</text> </svg>';
const drawing: TextElement = svgToDrawingConverter.convert(svg);
expect(drawing.text).toEqual('Label');
});
it('should match supported elements', () => {
expect(svgToDrawingConverter.supportedTags()).toEqual(['text', 'image', 'rect', 'line', 'ellipse']);
});
});

View File

@ -2,6 +2,10 @@ import { Injectable } from "@angular/core";
import { DrawingElement } from "../../shared/models/drawings/drawing-element";
import { SvgConverter } from "./svg-to-drawing-converter/svg-converter";
import { TextConverter } from "./svg-to-drawing-converter/text-converter";
import { ImageConverter } from "./svg-to-drawing-converter/image-converter";
import { RectConverter } from "./svg-to-drawing-converter/rect-converter";
import { LineConverter } from "./svg-to-drawing-converter/line-converter";
import { EllipseConverter } from "./svg-to-drawing-converter/ellipse-converter";
@Injectable()
@ -12,10 +16,18 @@ export class SvgToDrawingConverter {
constructor() {
this.parser = new DOMParser();
this.elementParsers = {
'text': new TextConverter()
'text': new TextConverter(),
'image': new ImageConverter(),
'rect': new RectConverter(),
'line': new LineConverter(),
'ellipse': new EllipseConverter()
};
}
supportedTags() {
return Object.keys(this.elementParsers);
}
convert(svg: string): DrawingElement {
const svgDom = this.parser.parseFromString(svg, 'text/xml');
const roots = svgDom.getElementsByTagName('svg');
@ -24,17 +36,23 @@ export class SvgToDrawingConverter {
}
const svgRoot = roots[0];
const child = svgRoot.firstChild;
if (!child) {
throw new Error(`Cannot find first child in '${svg}`);
}
let parser: SvgConverter = null;
let child: any = null;
// find matching tag
for (const i in svgRoot.children) {
child = svgRoot.children[i];
const name = child.nodeName;
if (!(name in this.elementParsers)) {
throw new Error(`Cannot find parser for '${name}'`);
if (name in this.elementParsers) {
parser = this.elementParsers[name];
break;
}
}
if (parser === null) {
throw new Error(`Cannot find parser for '${svg}'`);
}
const parser = this.elementParsers[name];
const drawing = parser.convert(child);
drawing.width = +svgRoot.getAttribute('width');

View File

@ -0,0 +1,33 @@
import { EllipseConverter } from "./ellipse-converter";
describe('EllipseConverter', () => {
let ellipseConverter: EllipseConverter;
beforeEach(() => {
ellipseConverter = new EllipseConverter();
});
it('should parse attributes', () => {
const node = document.createElement("ellipse");
node.setAttribute("fill", "#ffffff");
node.setAttribute("fill-opacity", "1.0");
node.setAttribute("stroke", "#000000");
node.setAttribute("stroke-width", "2");
node.setAttribute("cx", "63");
node.setAttribute("cy", "59");
node.setAttribute("rx", "63");
node.setAttribute("ry", "59");
const drawing = ellipseConverter.convert(node);
expect(drawing.fill).toEqual("#ffffff");
expect(drawing.fill_opacity).toEqual(1.0);
expect(drawing.stroke).toEqual("#000000");
expect(drawing.stroke_width).toEqual(2.0);
expect(drawing.cx).toEqual(63);
expect(drawing.cy).toEqual(59);
expect(drawing.rx).toEqual(63);
expect(drawing.ry).toEqual(59);
});
});

View File

@ -0,0 +1,51 @@
import { SvgConverter } from "./svg-converter";
import { EllipseElement } from "../../../shared/models/drawings/ellipse-element";
export class EllipseConverter implements SvgConverter {
convert(node: Node): EllipseElement {
const drawing = new EllipseElement();
const fill = node.attributes.getNamedItem("fill");
if (fill) {
drawing.fill = fill.value;
}
const fill_opacity = node.attributes.getNamedItem("fill-opacity");
if (fill) {
drawing.fill_opacity = parseInt(fill_opacity.value, 10);
}
const stroke = node.attributes.getNamedItem("stroke");
if (stroke) {
drawing.stroke = stroke.value;
}
const stroke_width = node.attributes.getNamedItem("stroke-width");
if (stroke) {
drawing.stroke_width = parseInt(stroke_width.value, 10);
}
const cx = node.attributes.getNamedItem('cx');
if (cx) {
drawing.cx = parseInt(cx.value, 10);
}
const cy = node.attributes.getNamedItem('cy');
if (cy) {
drawing.cy = parseInt(cy.value, 10);
}
const rx = node.attributes.getNamedItem('rx');
if (rx) {
drawing.rx = parseInt(rx.value, 10);
}
const ry = node.attributes.getNamedItem('ry');
if (ry) {
drawing.ry = parseInt(ry.value, 10);
}
return drawing;
}
}

View File

@ -0,0 +1,23 @@
import { ImageConverter } from "./image-converter";
describe('ImageConverter', () => {
let imageConverter: ImageConverter;
beforeEach(() => {
imageConverter = new ImageConverter();
});
it('should parse attributes', () => {
const node = document.createElement("image");
node.setAttribute("xlink:href", "data:image/png");
node.setAttribute("width", "100px");
node.setAttribute("height", "200px");
const drawing = imageConverter.convert(node);
expect(drawing.data).toEqual("data:image/png");
expect(drawing.width).toEqual(100);
expect(drawing.height).toEqual(200);
});
});

View File

@ -0,0 +1,26 @@
import { SvgConverter } from "./svg-converter";
import { ImageElement } from "../../../shared/models/drawings/image-element";
export class ImageConverter implements SvgConverter {
convert(node: Node): ImageElement {
const drawing = new ImageElement();
const data = node.attributes.getNamedItem("xlink:href");
if (data) {
drawing.data = data.value;
}
const width = node.attributes.getNamedItem('width');
if (width) {
drawing.width = parseInt(width.value, 10);
}
const height = node.attributes.getNamedItem('height');
if (height) {
drawing.height = parseInt(height.value, 10);
}
return drawing;
}
}

View File

@ -0,0 +1,29 @@
import { LineConverter } from "./line-converter";
describe('LineConverter', () => {
let lineConverter: LineConverter;
beforeEach(() => {
lineConverter = new LineConverter();
});
it('should parse attributes', () => {
const node = document.createElement("line");
node.setAttribute("stroke", "#000000");
node.setAttribute("stroke-width", "2");
node.setAttribute("x1", "10.10");
node.setAttribute("x2", "20");
node.setAttribute("y1", "30");
node.setAttribute("y2", "40");
const drawing = lineConverter.convert(node);
expect(drawing.stroke).toEqual("#000000");
expect(drawing.stroke_width).toEqual(2.0);
expect(drawing.x1).toEqual(10);
expect(drawing.x2).toEqual(20);
expect(drawing.y1).toEqual(30);
expect(drawing.y2).toEqual(40);
});
});

View File

@ -0,0 +1,41 @@
import { SvgConverter } from "./svg-converter";
import { LineElement } from "../../../shared/models/drawings/line-element";
export class LineConverter implements SvgConverter {
convert(node: Node): LineElement {
const drawing = new LineElement();
const stroke = node.attributes.getNamedItem("stroke");
if (stroke) {
drawing.stroke = stroke.value;
}
const stroke_width = node.attributes.getNamedItem("stroke-width");
if (stroke) {
drawing.stroke_width = parseInt(stroke_width.value, 10);
}
const x1 = node.attributes.getNamedItem('x1');
if (x1) {
drawing.x1 = parseInt(x1.value, 10);
}
const x2 = node.attributes.getNamedItem('x2');
if (x2) {
drawing.x2 = parseInt(x2.value, 10);
}
const y1 = node.attributes.getNamedItem('y1');
if (y1) {
drawing.y1 = parseInt(y1.value, 10);
}
const y2 = node.attributes.getNamedItem('y2');
if (y2) {
drawing.y2 = parseInt(y2.value, 10);
}
return drawing;
}
}

View File

@ -0,0 +1,30 @@
import { RectConverter } from "./rect-converter";
describe('RectConverter', () => {
let rectConverter: RectConverter;
beforeEach(() => {
rectConverter = new RectConverter();
});
it('should parse attributes', () => {
const node = document.createElement("rect");
node.setAttribute("fill", "#ffffff");
node.setAttribute("fill-opacity", "1.0");
node.setAttribute("stroke", "#000000");
node.setAttribute("stroke-width", "2");
node.setAttribute("width", "100px");
node.setAttribute("height", "200px");
const drawing = rectConverter.convert(node);
expect(drawing.fill).toEqual("#ffffff");
expect(drawing.fill_opacity).toEqual(1.0);
expect(drawing.stroke).toEqual("#000000");
expect(drawing.stroke_width).toEqual(2.0);
expect(drawing.width).toEqual(100);
expect(drawing.height).toEqual(200);
});
});

View File

@ -0,0 +1,41 @@
import { SvgConverter } from "./svg-converter";
import { RectElement } from "../../../shared/models/drawings/rect-element";
export class RectConverter implements SvgConverter {
convert(node: Node): RectElement {
const drawing = new RectElement();
const fill = node.attributes.getNamedItem("fill");
if (fill) {
drawing.fill = fill.value;
}
const fill_opacity = node.attributes.getNamedItem("fill-opacity");
if (fill) {
drawing.fill_opacity = parseInt(fill_opacity.value, 10);
}
const stroke = node.attributes.getNamedItem("stroke");
if (stroke) {
drawing.stroke = stroke.value;
}
const stroke_width = node.attributes.getNamedItem("stroke-width");
if (stroke) {
drawing.stroke_width = parseInt(stroke_width.value, 10);
}
const width = node.attributes.getNamedItem('width');
if (width) {
drawing.width = parseInt(width.value, 10);
}
const height = node.attributes.getNamedItem('height');
if (height) {
drawing.height = parseInt(height.value, 10);
}
return drawing;
}
}

View File

@ -1,7 +1,7 @@
import { TextConverter } from "./text-converter";
describe('SvgToDrawingHelper', () => {
describe('TextConverter', () => {
let textConverter: TextConverter;
beforeEach(() => {

View File

@ -4,6 +4,10 @@ import {SVGSelection} from "../models/types";
import {Layer} from "../models/layer";
import { TextDrawingWidget } from "./drawings/text-drawing";
import { SvgToDrawingConverter } from "../../map/helpers/svg-to-drawing-converter";
import { ImageDrawingWidget } from "./drawings/image-drawing";
import { RectDrawingWidget } from "./drawings/rect-drawing";
import { LineDrawingWidget } from "./drawings/line-drawing";
import { EllipseDrawingWidget } from "./drawings/ellipse-drawing";
export class DrawingsWidget implements Widget {
@ -73,6 +77,18 @@ export class DrawingsWidget implements Widget {
const text_drawing = new TextDrawingWidget();
text_drawing.draw(drawing_merge);
const image_drawing = new ImageDrawingWidget();
image_drawing.draw(drawing_merge);
const rect_drawing = new RectDrawingWidget();
rect_drawing.draw(drawing_merge);
const line_drawing = new LineDrawingWidget();
line_drawing.draw(drawing_merge);
const ellipse_drawing = new EllipseDrawingWidget();
ellipse_drawing.draw(drawing_merge);
drawing
.exit()
.remove();

View File

@ -0,0 +1,53 @@
import { TestSVGCanvas } from "../../../testing";
import { Drawing } from "../../models/drawing";
import { EllipseDrawingWidget } from "./ellipse-drawing";
import { EllipseElement } from "../../models/drawings/ellipse-element";
describe('EllipseDrawingWidget', () => {
let svg: TestSVGCanvas;
let widget: EllipseDrawingWidget;
let drawing: Drawing;
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new EllipseDrawingWidget();
});
afterEach(() => {
svg.destroy();
});
it('should draw ellipse drawing', () => {
const ellipse = new EllipseElement();
ellipse.fill = "#FFFFFFF";
ellipse.fill_opacity = 2.0;
ellipse.stroke = "#000000";
ellipse.stroke_width = 2.0;
ellipse.cx = 10;
ellipse.cy = 20;
ellipse.rx = 30;
ellipse.ry = 40;
drawing.element = ellipse;
const drawings = svg.canvas.selectAll<SVGGElement, Drawing>('g.drawing').data([drawing]);
const drawings_enter = drawings.enter().append<SVGGElement>('g').classed('drawing', true);
const drawings_merge = drawings.merge(drawings_enter);
widget.draw(drawings_merge);
const drew = drawings_merge.selectAll<SVGEllipseElement, EllipseElement>('ellipse.ellipse_element');
expect(drew.size()).toEqual(1);
const ellipse_element = drew.nodes()[0];
expect(ellipse_element.getAttribute('fill')).toEqual('#FFFFFFF');
expect(ellipse_element.getAttribute('fill-opacity')).toEqual('2');
expect(ellipse_element.getAttribute('stroke')).toEqual('#000000');
expect(ellipse_element.getAttribute('stroke-width')).toEqual('2');
expect(ellipse_element.getAttribute('cx')).toEqual('10');
expect(ellipse_element.getAttribute('cy')).toEqual('20');
expect(ellipse_element.getAttribute('rx')).toEqual('30');
expect(ellipse_element.getAttribute('ry')).toEqual('40');
});
});

View File

@ -0,0 +1,36 @@
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { EllipseElement } from "../../models/drawings/ellipse-element";
export class EllipseDrawingWidget {
public draw(view: SVGSelection) {
const drawing = view
.selectAll<SVGEllipseElement, EllipseElement>('ellipse.ellipse_element')
.data((d: Drawing) => {
return (d.element && d.element instanceof EllipseElement) ? [d.element] : [];
});
const drawing_enter = drawing
.enter()
.append<SVGEllipseElement>('ellipse')
.attr('class', 'ellipse_element noselect');
const merge = drawing.merge(drawing_enter);
merge
.attr('fill', (ellipse) => ellipse.fill)
.attr('fill-opacity', (ellipse) => ellipse.fill_opacity)
.attr('stroke', (ellipse) => ellipse.stroke)
.attr('stroke-width', (ellipse) => ellipse.stroke_width)
.attr('cx', (ellipse) => ellipse.cx)
.attr('cy', (ellipse) => ellipse.cy)
.attr('rx', (ellipse) => ellipse.rx)
.attr('ry', (ellipse) => ellipse.ry);
drawing
.exit()
.remove();
}
}

View File

@ -24,7 +24,7 @@ describe('ImageDrawingWidget', () => {
const image = new ImageElement();
image.width = 100;
image.height = 200;
image.data = "DATA";
image.data = "data:image/svg+xml;base64,DATA";
drawing.element = image;
const drawings = svg.canvas.selectAll<SVGGElement, Drawing>('g.drawing').data([drawing]);
@ -38,6 +38,6 @@ describe('ImageDrawingWidget', () => {
const image_element = drew.nodes()[0];
expect(image_element.getAttribute('width')).toEqual('100');
expect(image_element.getAttribute('height')).toEqual('200');
expect(image_element.getAttribute('href')).toEqual('data:image/svg+xml;base64,REFUQQ==');
expect(image_element.getAttribute('href')).toEqual('data:image/svg+xml;base64,DATA');
});
});

View File

@ -19,17 +19,10 @@ export class ImageDrawingWidget {
const merge = drawing.merge(drawing_enter);
merge
.attr('xlink:href', (image: ImageElement) => {
let svg = image.data;
if (svg.indexOf("xmlns") < 0) {
svg = svg.replace('svg', 'svg xmlns="http://www.w3.org/2000/svg"');
}
return 'data:image/svg+xml;base64,' + btoa(svg);
})
.attr('xlink:href', (image: ImageElement) => image.data)
.attr('width', (image) => image.width)
.attr('height', (image) => image.height);
drawing
.exit()
.remove();

View File

@ -0,0 +1,49 @@
import { TestSVGCanvas } from "../../../testing";
import { Drawing } from "../../models/drawing";
import { LineDrawingWidget } from "./line-drawing";
import { LineElement } from "../../models/drawings/line-element";
describe('LineDrawingWidget', () => {
let svg: TestSVGCanvas;
let widget: LineDrawingWidget;
let drawing: Drawing;
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new LineDrawingWidget();
});
afterEach(() => {
svg.destroy();
});
it('should draw line drawing', () => {
const line = new LineElement();
line.stroke = "#000000";
line.stroke_width = 2.0;
line.x1 = 10;
line.x2 = 20;
line.y1 = 30;
line.y2 = 40;
drawing.element = line;
const drawings = svg.canvas.selectAll<SVGGElement, Drawing>('g.drawing').data([drawing]);
const drawings_enter = drawings.enter().append<SVGGElement>('g').classed('drawing', true);
const drawings_merge = drawings.merge(drawings_enter);
widget.draw(drawings_merge);
const drew = drawings_merge.selectAll<SVGLineElement, LineElement>('line.line_element');
expect(drew.size()).toEqual(1);
const line_element = drew.nodes()[0];
expect(line_element.getAttribute('stroke')).toEqual('#000000');
expect(line_element.getAttribute('stroke-width')).toEqual('2');
expect(line_element.getAttribute('x1')).toEqual('10');
expect(line_element.getAttribute('x2')).toEqual('20');
expect(line_element.getAttribute('y1')).toEqual('30');
expect(line_element.getAttribute('y2')).toEqual('40');
});
});

View File

@ -0,0 +1,34 @@
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { LineElement } from "../../models/drawings/line-element";
export class LineDrawingWidget {
public draw(view: SVGSelection) {
const drawing = view
.selectAll<SVGLineElement, LineElement>('line.line_element')
.data((d: Drawing) => {
return (d.element && d.element instanceof LineElement) ? [d.element] : [];
});
const drawing_enter = drawing
.enter()
.append<SVGLineElement>('line')
.attr('class', 'line_element noselect');
const merge = drawing.merge(drawing_enter);
merge
.attr('stroke', (line) => line.stroke)
.attr('stroke-width', (line) => line.stroke_width)
.attr('x1', (line) => line.x1)
.attr('x2', (line) => line.x2)
.attr('y1', (line) => line.y1)
.attr('y2', (line) => line.y2);
drawing
.exit()
.remove();
}
}

View File

@ -0,0 +1,49 @@
import { TestSVGCanvas } from "../../../testing";
import { Drawing } from "../../models/drawing";
import { RectDrawingWidget } from "./rect-drawing";
import { RectElement } from "../../models/drawings/rect-element";
describe('RectDrawingWidget', () => {
let svg: TestSVGCanvas;
let widget: RectDrawingWidget;
let drawing: Drawing;
beforeEach(() => {
svg = new TestSVGCanvas();
drawing = new Drawing();
widget = new RectDrawingWidget();
});
afterEach(() => {
svg.destroy();
});
it('should draw rect drawing', () => {
const rect = new RectElement();
rect.fill = "#FFFFFF";
rect.fill_opacity = 1.0;
rect.stroke = "#000000";
rect.stroke_width = 2.0;
rect.width = 100;
rect.height = 200;
drawing.element = rect;
const drawings = svg.canvas.selectAll<SVGGElement, Drawing>('g.drawing').data([drawing]);
const drawings_enter = drawings.enter().append<SVGGElement>('g').classed('drawing', true);
const drawings_merge = drawings.merge(drawings_enter);
widget.draw(drawings_merge);
const drew = drawings_merge.selectAll<SVGRectElement, RectElement>('rect.rect_element');
expect(drew.size()).toEqual(1);
const rect_element = drew.nodes()[0];
expect(rect_element.getAttribute('fill')).toEqual('#FFFFFF');
expect(rect_element.getAttribute('fill-opacity')).toEqual('1');
expect(rect_element.getAttribute('stroke')).toEqual('#000000');
expect(rect_element.getAttribute('stroke-width')).toEqual('2');
expect(rect_element.getAttribute('width')).toEqual('100');
expect(rect_element.getAttribute('height')).toEqual('200');
});
});

View File

@ -0,0 +1,34 @@
import { SVGSelection } from "../../models/types";
import { Drawing } from "../../models/drawing";
import { RectElement } from "../../models/drawings/rect-element";
export class RectDrawingWidget {
public draw(view: SVGSelection) {
const drawing = view
.selectAll<SVGRectElement, RectElement>('rect.rect_element')
.data((d: Drawing) => {
return (d.element && d.element instanceof RectElement) ? [d.element] : [];
});
const drawing_enter = drawing
.enter()
.append<SVGRectElement>('rect')
.attr('class', 'rect_element noselect');
const merge = drawing.merge(drawing_enter);
merge
.attr('fill', (rect) => rect.fill)
.attr('fill-opacity', (rect) => rect.fill_opacity)
.attr('stroke', (rect) => rect.stroke)
.attr('stroke-width', (rect) => rect.stroke_width)
.attr('width', (rect) => rect.width)
.attr('height', (rect) => rect.height);
drawing
.exit()
.remove();
}
}