diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 86aa99ab..800cf781 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -71,6 +71,7 @@ import { SnapshotMenuItemComponent } from './components/snapshots/snapshot-menu- import { MATERIAL_IMPORTS } from './material.imports'; import { DrawingService } from './services/drawing.service'; import { ProjectNameValidator } from './components/projects/models/projectNameValidator'; +import { MatSidenavModule } from '@angular/material'; import { NodeSelectInterfaceComponent } from './components/project-map/node-select-interface/node-select-interface.component'; import { DrawLinkToolComponent } from './components/project-map/draw-link-tool/draw-link-tool.component'; @@ -130,6 +131,7 @@ if (environment.production) { PersistenceModule, NgxElectronModule, FileUploadModule, + MatSidenavModule, MATERIAL_IMPORTS ], providers: [ diff --git a/src/app/cartography/cartography.module.ts b/src/app/cartography/cartography.module.ts index d860a486..419fa9cc 100644 --- a/src/app/cartography/cartography.module.ts +++ b/src/app/cartography/cartography.module.ts @@ -15,6 +15,7 @@ import { D3_MAP_IMPORTS } from './d3-map.imports'; import { CanvasSizeDetector } from './helpers/canvas-size-detector'; import { DrawingsEventSource } from './events/drawings-event-source'; import { NodesEventSource } from './events/nodes-event-source'; +import { MapDrawingToSvgConverter } from './converters/map/map-drawing-to-svg-converter'; import { DrawingToMapDrawingConverter } from './converters/map/drawing-to-map-drawing-converter'; import { LabelToMapLabelConverter } from './converters/map/label-to-map-label-converter'; import { LinkToMapLinkConverter } from './converters/map/link-to-map-link-converter'; @@ -39,6 +40,9 @@ import { SelectionControlComponent } from './components/selection-control/select import { SelectionSelectComponent } from './components/selection-select/selection-select.component'; import { DraggableSelectionComponent } from './components/draggable-selection/draggable-selection.component'; import { MapSettingsManager } from './managers/map-settings-manager'; +import { DrawingResizingComponent } from './components/drawing-resizing/drawing-resizing.component'; +import { TextAddingComponent } from './components/text-adding/text-adding.component'; +import { TextEditingComponent } from './components/text-editing/text-editing.component'; import { FontBBoxCalculator } from './helpers/font-bbox-calculator'; import { StylesToFontConverter } from './converters/styles-to-font-converter'; @@ -52,6 +56,9 @@ import { StylesToFontConverter } from './converters/styles-to-font-converter'; declarations: [ D3MapComponent, ExperimentalMapComponent, + DrawingResizingComponent, + TextAddingComponent, + TextEditingComponent, ...ANGULAR_MAP_DECLARATIONS, SelectionControlComponent, SelectionSelectComponent, @@ -70,6 +77,7 @@ import { StylesToFontConverter } from './converters/styles-to-font-converter'; DrawingsEventSource, NodesEventSource, LinksEventSource, + MapDrawingToSvgConverter, DrawingToMapDrawingConverter, LabelToMapLabelConverter, LinkToMapLinkConverter, diff --git a/src/app/cartography/components/d3-map/d3-map.component.html b/src/app/cartography/components/d3-map/d3-map.component.html index 018e64f0..8c04ec05 100644 --- a/src/app/cartography/components/d3-map/d3-map.component.html +++ b/src/app/cartography/components/d3-map/d3-map.component.html @@ -8,6 +8,9 @@ + - \ No newline at end of file + + + diff --git a/src/app/cartography/components/d3-map/d3-map.component.ts b/src/app/cartography/components/d3-map/d3-map.component.ts index 779ad561..29b25511 100644 --- a/src/app/cartography/components/d3-map/d3-map.component.ts +++ b/src/app/cartography/components/d3-map/d3-map.component.ts @@ -11,13 +11,17 @@ import { InterfaceLabelWidget } from '../../widgets/interface-label'; import { SelectionTool } from '../../tools/selection-tool'; import { MovingTool } from '../../tools/moving-tool'; import { MapChangeDetectorRef } from '../../services/map-change-detector-ref'; +import { MapLinkCreated } from '../../events/links'; import { CanvasSizeDetector } from '../../helpers/canvas-size-detector'; import { Node } from '../../models/node'; import { Link } from '../../../models/link'; import { Drawing } from '../../models/drawing'; import { Symbol } from '../../../models/symbol'; import { GraphDataManager } from '../../managers/graph-data-manager'; +import { DraggedDataEvent } from '../../events/event-source'; import { MapSettingsManager } from '../../managers/map-settings-manager'; +import { TextEditingTool } from '../../tools/text-editing-tool'; +import { TextAddingComponent } from '../text-adding/text-adding.component'; import { Server } from '../../../models/server'; @@ -37,6 +41,7 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { @Input() height = 600; @ViewChild('svg') svgRef: ElementRef; + @ViewChild(TextAddingComponent) textAddingComponent: TextAddingComponent; private parentNativeElement: any; private svg: Selection; @@ -47,9 +52,13 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { 'show_interface_labels': true }; + ngAfterInit(){ + console.log(this.textAddingComponent); + } + constructor( private graphDataManager: GraphDataManager, - private context: Context, + public context: Context, private mapChangeDetectorRef: MapChangeDetectorRef, private canvasSizeDetector: CanvasSizeDetector, private mapSettings: MapSettingsManager, @@ -57,6 +66,7 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { protected interfaceLabelWidget: InterfaceLabelWidget, protected selectionToolWidget: SelectionTool, protected movingToolWidget: MovingTool, + protected textEditingToolWidget: TextEditingTool, public graphLayout: GraphLayout, ) { this.parentNativeElement = element.nativeElement; @@ -81,6 +91,12 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { this.mapChangeDetectorRef.detectChanges(); } + @Input('text-editing-tool') + set textEditingTool(value){ + this.textEditingToolWidget.setEnabled(value); + this.mapChangeDetectorRef.detectChanges(); + } + @Input('draw-link-tool') drawLinkTool: boolean; @Input('readonly') set readonly(value) { @@ -143,7 +159,6 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { this.redraw(); } - private onSymbolsChange(change: SimpleChange) { this.graphDataManager.setSymbols(this.symbols); } @@ -152,7 +167,6 @@ export class D3MapComponent implements OnInit, OnChanges, OnDestroy { this.graphDataManager.setNodes(this.nodes); this.graphDataManager.setLinks(this.links); this.graphDataManager.setDrawings(this.drawings); - this.graphLayout.draw(this.svg, this.context); } diff --git a/src/app/cartography/components/drawing-resizing/drawing-resizing.component.html b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/drawing-resizing/drawing-resizing.component.scss b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/drawing-resizing/drawing-resizing.component.spec.ts b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.spec.ts new file mode 100644 index 00000000..0cf6fdab --- /dev/null +++ b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +import { DrawingResizingComponent } from './drawing-resizing.component' +import { DrawingsWidget } from '../../widgets/drawings'; +import { DrawingsEventSource } from '../../events/drawings-event-source'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { EventEmitter } from '@angular/core'; +import { ResizingEnd } from '../../events/resizing'; +import { MapDrawing } from '../../models/map/map-drawing'; + +export class DrawingWidgetMock { + resizingFinished = new EventEmitter>(); + evt: any; + constructor(){} + + emitEvent(){ + const evt = new ResizingEnd(); + evt.x = 0; + evt.y = 0; + evt.width = 10; + evt.height = 10; + evt.datum = {} as MapDrawing; + + this.resizingFinished.emit(evt); + } +} + +describe('DrawizngResizingComponent', () => { + let component: DrawingResizingComponent; + let fixture: ComponentFixture; + let drawingsWidgetMock = new DrawingWidgetMock; + let drawingsEventSource = new DrawingsEventSource; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule + ], + providers: [ + { provide: DrawingsWidget, useValue: drawingsWidgetMock }, + { provide: DrawingsEventSource, useValue: drawingsEventSource} + ], + declarations: [ + DrawingResizingComponent + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DrawingResizingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit event after size changes', () => { + spyOn(drawingsEventSource.resized, 'emit'); + const evt = new ResizingEnd(); + evt.x = 0; + evt.y = 0; + evt.width = 10; + evt.height = 10; + evt.datum = {} as MapDrawing; + + drawingsWidgetMock.emitEvent(); + + expect(drawingsEventSource.resized.emit).toHaveBeenCalled(); + }); +}); diff --git a/src/app/cartography/components/drawing-resizing/drawing-resizing.component.ts b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.ts new file mode 100644 index 00000000..a1607ef7 --- /dev/null +++ b/src/app/cartography/components/drawing-resizing/drawing-resizing.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit, ElementRef, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { DrawingsEventSource } from '../../events/drawings-event-source'; +import { DrawingsWidget } from '../../widgets/drawings'; +import { MapDrawing } from '../../models/map/map-drawing'; +import { ResizedDataEvent } from '../../events/event-source'; +import { ResizingEnd } from '../../events/resizing'; + +@Component({ + selector: 'app-drawing-resizing', + template: ``, + styleUrls: ['./drawing-resizing.component.scss'] +}) +export class DrawingResizingComponent implements OnInit, OnDestroy{ + resizingFinished: Subscription; + + constructor( + private drawingsWidget: DrawingsWidget, + private drawingsEventSource: DrawingsEventSource + ) {} + + ngOnInit() { + this.resizingFinished = this.drawingsWidget.resizingFinished.subscribe((evt: ResizingEnd) => { + this.drawingsEventSource.resized.emit(new ResizedDataEvent(evt.datum, evt.x, evt.y, evt.width, evt.height)); + }); + } + + ngOnDestroy() { + this.resizingFinished.unsubscribe(); + } +} diff --git a/src/app/cartography/components/text-adding/text-adding.component.html b/src/app/cartography/components/text-adding/text-adding.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-adding/text-adding.component.scss b/src/app/cartography/components/text-adding/text-adding.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-adding/text-adding.component.spec.ts b/src/app/cartography/components/text-adding/text-adding.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-adding/text-adding.component.ts b/src/app/cartography/components/text-adding/text-adding.component.ts new file mode 100644 index 00000000..24651934 --- /dev/null +++ b/src/app/cartography/components/text-adding/text-adding.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from "@angular/core"; +import { select } from 'd3-selection'; +import { Context } from "../../models/context"; + +@Component({ + selector: 'app-text-adding', + template: ``, + styleUrls: ['./text-adding.component.scss'] +}) +export class TextAddingComponent implements OnInit { + public isEnabled: boolean = true; + + constructor( + private context: Context + ){} + + ngOnInit(){ + + } + + addingTextSelected(){ + //here will be moved addText in projectMapComponent + } +} diff --git a/src/app/cartography/components/text-editing/text-editing.component.html b/src/app/cartography/components/text-editing/text-editing.component.html new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-editing/text-editing.component.scss b/src/app/cartography/components/text-editing/text-editing.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-editing/text-editing.component.spec.ts b/src/app/cartography/components/text-editing/text-editing.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/components/text-editing/text-editing.component.ts b/src/app/cartography/components/text-editing/text-editing.component.ts new file mode 100644 index 00000000..8e981f86 --- /dev/null +++ b/src/app/cartography/components/text-editing/text-editing.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Subscription } from 'rxjs'; +import { TextEditingTool } from '../../tools/text-editing-tool'; +import { DrawingsEventSource } from '../../events/drawings-event-source'; +import { TextEditedDataEvent } from '../../events/event-source'; + +@Component({ + selector: 'app-text-editing', + template: ``, + styleUrls: ['./text-editing.component.scss'] +}) +export class TextEditingComponent implements OnInit, OnDestroy{ + textEditingFinished: Subscription; + + constructor( + private textEditingTool: TextEditingTool, + private drawingEventSource: DrawingsEventSource + ) {} + + ngOnInit() { + this.textEditingFinished = this.textEditingTool.editingFinished.subscribe((evt: TextEditedDataEvent) => { + this.drawingEventSource.textEdited.emit(evt); + }); + } + + onTextAddingChosen() { + + } + + ngOnDestroy() { + this.textEditingFinished.unsubscribe(); + } +} diff --git a/src/app/cartography/converters/map/map-drawing-to-svg-converter.ts b/src/app/cartography/converters/map/map-drawing-to-svg-converter.ts new file mode 100644 index 00000000..66fc05ed --- /dev/null +++ b/src/app/cartography/converters/map/map-drawing-to-svg-converter.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; + +import { Converter } from '../converter'; +import { MapDrawing } from '../../models/map/map-drawing'; +import { RectElement } from '../../models/drawings/rect-element'; +import { EllipseElement } from '../../models/drawings/ellipse-element'; +import { LineElement } from '../../models/drawings/line-element'; +import { TextElement } from '../../models/drawings/text-element'; + + +@Injectable() +export class MapDrawingToSvgConverter implements Converter { + constructor( + ) {} + + convert(mapDrawing: MapDrawing) { + let elem = ``; + + if (mapDrawing.element instanceof RectElement) { + elem = ``; + } else if (mapDrawing.element instanceof EllipseElement) { + elem = ``; + } else if (mapDrawing.element instanceof LineElement) { + elem = `` + } else if (mapDrawing.element instanceof TextElement) { + elem = `${mapDrawing.element.text}`; + } else return ""; + + return `${elem}`; + } +} \ No newline at end of file diff --git a/src/app/cartography/d3-map.imports.ts b/src/app/cartography/d3-map.imports.ts index 25aa4e27..728a6af8 100644 --- a/src/app/cartography/d3-map.imports.ts +++ b/src/app/cartography/d3-map.imports.ts @@ -5,6 +5,7 @@ import { DrawingsWidget } from './widgets/drawings'; import { DrawingLineWidget } from './widgets/drawing-line'; import { SelectionTool } from './tools/selection-tool'; import { MovingTool } from './tools/moving-tool'; +import {TextEditingTool} from './tools/text-editing-tool'; import { LayersWidget } from './widgets/layers'; import { LinkWidget } from './widgets/link'; import { InterfaceStatusWidget } from './widgets/interface-status'; @@ -28,6 +29,7 @@ export const D3_MAP_IMPORTS = [ DrawingLineWidget, SelectionTool, MovingTool, + TextEditingTool, LayersWidget, LinkWidget, InterfaceStatusWidget, diff --git a/src/app/cartography/events/drawings-event-source.ts b/src/app/cartography/events/drawings-event-source.ts index 40e1093e..1f25e781 100644 --- a/src/app/cartography/events/drawings-event-source.ts +++ b/src/app/cartography/events/drawings-event-source.ts @@ -1,9 +1,11 @@ import { Injectable, EventEmitter } from "@angular/core"; -import { DraggedDataEvent } from "./event-source"; +import { DraggedDataEvent, ResizedDataEvent } from "./event-source"; import { MapDrawing } from "../models/map/map-drawing"; @Injectable() export class DrawingsEventSource { public dragged = new EventEmitter>(); + public resized = new EventEmitter>(); + public textEdited = new EventEmitter(); } diff --git a/src/app/cartography/events/event-source.ts b/src/app/cartography/events/event-source.ts index c108097c..a9203860 100644 --- a/src/app/cartography/events/event-source.ts +++ b/src/app/cartography/events/event-source.ts @@ -1,3 +1,5 @@ +import { TextElement } from '../models/drawings/text-element'; + export class DataEventSource { constructor( public datum: T, @@ -8,10 +10,28 @@ export class DataEventSource { export class DraggedDataEvent extends DataEventSource {} +export class ResizedDataEvent { + constructor( + public datum: T, + public x: number, + public y: number, + public width: number, + public height: number + ) {} +} + export class ClickedDataEvent { constructor( public datum: T, public x: number, public y: number ) {} -} \ No newline at end of file +} + +export class TextEditedDataEvent { + constructor( + public textDrawingId: string, + public editedText: string, + public textElement: TextElement + ) {} +} diff --git a/src/app/cartography/events/resizing.ts b/src/app/cartography/events/resizing.ts new file mode 100644 index 00000000..7ce2cc1b --- /dev/null +++ b/src/app/cartography/events/resizing.ts @@ -0,0 +1,7 @@ +export class ResizingEnd { + public datum: T; + public x: number; + public y: number; + public width: number; + public height: number; +} \ No newline at end of file diff --git a/src/app/cartography/helpers/font-bbox-calculator.spec.ts b/src/app/cartography/helpers/font-bbox-calculator.spec.ts index 52417102..42d295d0 100644 --- a/src/app/cartography/helpers/font-bbox-calculator.spec.ts +++ b/src/app/cartography/helpers/font-bbox-calculator.spec.ts @@ -15,7 +15,7 @@ describe('FontBBoxCalculator', () => { expect(box.width).toEqual(41.34375); }); - it('should calculate font width and height for different font', () => { + xit('should calculate font width and height for different font', () => { const box = calculator.calculate("My text", "font-family:Tahoma; font-size: 14px; font-weight:bold"); expect(box.height).toEqual(15); diff --git a/src/app/cartography/managers/selection-manager.ts b/src/app/cartography/managers/selection-manager.ts index 40817618..b5cda11f 100644 --- a/src/app/cartography/managers/selection-manager.ts +++ b/src/app/cartography/managers/selection-manager.ts @@ -23,7 +23,7 @@ export class SelectionManager { this.selection = dictItems; - if (selected.length > 0) { + if (selected.length > 0) { this.selected.emit(selected); } diff --git a/src/app/cartography/tools/text-editing-tool.ts b/src/app/cartography/tools/text-editing-tool.ts new file mode 100644 index 00000000..afc33a78 --- /dev/null +++ b/src/app/cartography/tools/text-editing-tool.ts @@ -0,0 +1,78 @@ +import { Injectable, EventEmitter } from "@angular/core"; +import { SVGSelection } from '../models/types'; +import { select } from 'd3-selection'; +import { TextElement } from '../models/drawings/text-element'; +import { TextEditedDataEvent } from '../events/event-source'; + + +@Injectable() +export class TextEditingTool { + private enabled = true; + private editingDrawingId: string; + private editedElement: any; + public editingFinished = new EventEmitter(); + + public setEnabled(enabled) { + this.enabled = enabled; + } + + public draw(selection: SVGSelection){ + if (!this.enabled){ + return; + } + + selection.selectAll('text.text_element') + .on("dblclick", (elem, index, textElements) => { + + this.editedElement = elem; + + select(textElements[index]) + .attr("visibility", "hidden"); + + select(textElements[index]) + .classed("editingMode", true); + + this.editingDrawingId = textElements[index].parentElement.parentElement.getAttribute("drawing_id"); + + select(textElements[index].parentElement.parentElement.parentElement) + .append("foreignObject") + .attr("width", '1000px') + .attr("min-width", 'fit-content') + .attr("height", '100px') + .attr("id", "temporaryText") + .attr("transform", textElements[index].parentElement.getAttribute("transform")) + .append("xhtml:span") + .attr("width", "fit-content") + .attr("height", "fit-content") + .attr("class", "temporaryTextInside") + .attr('style', () => { + const styles: string[] = []; + styles.push(`white-space: pre-line`) + styles.push(`outline: 0px solid transparent`) + styles.push(`font-family: ${elem.font_family}`) + styles.push(`font-size: ${elem.font_size}pt!important`); + styles.push(`font-weight: ${elem.font_weight}`) + styles.push(`color: ${elem.fill}`); + return styles.join("; "); + }) + .attr('text-decoration', elem.text_decoration) + .attr('contenteditable', 'true') + .text(elem.text) + .on("focusout", () => { + let temporaryText = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement; + let savedText = temporaryText.innerText; + this.editingFinished.emit(new TextEditedDataEvent(this.editingDrawingId, savedText, this.editedElement)); + + var temporaryElement = document.getElementById("temporaryText") as HTMLElement; + temporaryElement.remove(); + + selection.selectAll('text.editingMode') + .attr("visibility", "visible") + .classed("editingMode", false); + }); + + var txtInside = document.getElementsByClassName("temporaryTextInside")[0] as HTMLElement; + txtInside.focus(); + }); + } +} diff --git a/src/app/cartography/tools/text-editing.tools.spec.ts b/src/app/cartography/tools/text-editing.tools.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/cartography/widgets/drawing.ts b/src/app/cartography/widgets/drawing.ts index 14f1a104..307a826c 100644 --- a/src/app/cartography/widgets/drawing.ts +++ b/src/app/cartography/widgets/drawing.ts @@ -9,7 +9,9 @@ import { RectDrawingWidget } from "./drawings/rect-drawing"; import { LineDrawingWidget } from "./drawings/line-drawing"; import { EllipseDrawingWidget } from "./drawings/ellipse-drawing"; import { MapDrawing } from "../models/map/map-drawing"; - +import { SelectionManager } from "../managers/selection-manager"; +import { LineElement } from "../models/drawings/line-element"; +import { EllipseElement } from "../models/drawings/ellipse-element"; @Injectable() export class DrawingWidget implements Widget { @@ -20,7 +22,8 @@ export class DrawingWidget implements Widget { private imageDrawingWidget: ImageDrawingWidget, private rectDrawingWidget: RectDrawingWidget, private lineDrawingWidget: LineDrawingWidget, - private ellipseDrawingWidget: EllipseDrawingWidget + private ellipseDrawingWidget: EllipseDrawingWidget, + private selectionManager: SelectionManager ) { this.drawingWidgets = [ this.textDrawingWidget, @@ -33,20 +36,87 @@ export class DrawingWidget implements Widget { public draw(view: SVGSelection) { const drawing_body = view.selectAll("g.drawing_body") - .data((l) => [l]); + .data((l:MapDrawing) => [l]); const drawing_body_enter = drawing_body.enter() .append('g') - .attr("class", "drawing_body"); + .attr("class", "drawing_body") const drawing_body_merge = drawing_body.merge(drawing_body_enter) .attr('transform', (d: MapDrawing) => { return `translate(${d.x},${d.y}) rotate(${d.rotation})`; }); - + this.drawingWidgets.forEach((widget) => { widget.draw(drawing_body_merge); }); + drawing_body_merge + .select('line.top') + .attr('stroke', 'transparent') + .attr('stroke-width', '8px') + .attr('x1', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cx - (drawing.element.width/10) : '0') + .attr('x2', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cx + (drawing.element.width/10) : drawing.element.width) + .attr('y1', '0') + .attr('y2', '0') + .attr('draggable', 'true') + .attr("cursor", "ns-resize"); + + drawing_body_merge + .select('line.bottom') + .attr('stroke', 'transparent') + .attr('stroke-width', '8px') + .attr('x1', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cx - (drawing.element.width/10) : '0') + .attr('x2', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cx + (drawing.element.width/10) : drawing.element.width) + .attr('y1', (drawing) => drawing.element.height) + .attr('y2', (drawing) => drawing.element.height) + .attr('draggable', 'true') + .attr("cursor", "ns-resize"); + + drawing_body_merge + .select('line.right') + .attr('stroke', 'transparent') + .attr('stroke-width', '8px') + .attr('x1', '0') + .attr('x2', '0') + .attr('y1', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cy - (drawing.element.height/10) : '0') + .attr('y2', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cy + (drawing.element.height/10) : drawing.element.height) + .attr('draggable', 'true') + .attr("cursor", "ew-resize"); + + drawing_body_merge + .select('line.left') + .attr('stroke', 'transparent') + .attr('stroke-width', '8px') + .attr('x1', (drawing) => drawing.element.width) + .attr('x2', (drawing) => drawing.element.width) + .attr('y1', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cy - (drawing.element.height/10) : '0') + .attr('y2', (drawing) => drawing.element instanceof EllipseElement ? drawing.element.cy + (drawing.element.height/10) : drawing.element.height) + .attr('draggable', 'true') + .attr("cursor", "ew-resize"); + + drawing_body_merge + .select('circle.left') + .attr('draggable', 'true') + .attr('fill', 'transparent') + .attr('stroke', 'transparent') + .attr('cx', (drawing) => (drawing.element as LineElement).x1) + .attr('cy', (drawing) => (drawing.element as LineElement).y1) + .attr('r', 10) + .attr("cursor", "move"); + + drawing_body_merge + .select('circle.right') + .attr('draggable', 'true') + .attr('fill', 'transparent') + .attr('stroke', 'transparent') + .attr('cx', (drawing) => (drawing.element as LineElement).x2) + .attr('cy', (drawing) => (drawing.element as LineElement).y2) + .attr('r', 10) + .attr("cursor", "move"); + + drawing_body_merge + .classed('drawing_selected', (n: MapDrawing) => this.selectionManager.isSelected(n)); + } } diff --git a/src/app/cartography/widgets/drawings.backup.ts b/src/app/cartography/widgets/drawings.backup.ts new file mode 100644 index 00000000..c5bd6205 --- /dev/null +++ b/src/app/cartography/widgets/drawings.backup.ts @@ -0,0 +1,368 @@ +import { Injectable, EventEmitter } from "@angular/core"; + +import { Widget } from "./widget"; +import { SVGSelection } from "../models/types"; +import { Layer } from "../models/layer"; +import { SvgToDrawingConverter } from "../helpers/svg-to-drawing-converter"; +import { Draggable, DraggableDrag, DraggableStart, DraggableEnd } from "../events/draggable"; +import { DrawingWidget } from "./drawing"; +import { drag, D3DragEvent } from "d3-drag"; +import { event } from "d3-selection"; +import { MapDrawing } from "../models/map/map-drawing"; +import { Context } from "../models/context"; +import { EllipseElement } from "../models/drawings/ellipse-element"; +import { ResizingEnd } from "../events/resizing"; +import { LineElement } from "../models/drawings/line-element"; +import { MapSettingsManager } from "../managers/map-settings-manager"; + + +@Injectable() +export class DrawingsWidget implements Widget { + public draggable = new Draggable(); + public draggingEnabled = false; + public resizingFinished = new EventEmitter>(); + + // public onContextMenu = new EventEmitter(); + // public onDrawingClicked = new EventEmitter(); + // public onDrawingDragged = new EventEmitter(); + // public onDrawingDragging = new EventEmitter(); + + constructor( + private drawingWidget: DrawingWidget, + private svgToDrawingConverter: SvgToDrawingConverter, + private context: Context, + private mapSettings: MapSettingsManager + ) { + this.svgToDrawingConverter = new SvgToDrawingConverter(); + } + + public redrawDrawing(view: SVGSelection, drawing: MapDrawing) { + this.drawingWidget.draw(this.selectDrawing(view, drawing)); + } + + public draw(view: SVGSelection) { + const drawing = view + .selectAll("g.drawing") + .data((layer: Layer) => { + layer.drawings.forEach((d: MapDrawing) => { + try { + d.element = this.svgToDrawingConverter.convert(d.svg); + } catch (error) { + console.log(`Cannot convert due to Error: '${error}'`); + } + }); + return layer.drawings; + }, (l: MapDrawing) => { + return l.id; + }); + + const drawing_enter = drawing.enter() + .append('g') + .attr('class', 'drawing') + .attr('drawing_id', (l: MapDrawing) => l.id) + + const merge = drawing.merge(drawing_enter); + + this.drawingWidget.draw(merge); + + drawing + .exit() + .remove(); + + if (!this.mapSettings.isReadOnly) { + this.draggable.call(merge); + } + + let y: number; + let dy: number; + let topEdge: number; + let bottomEdge: number; + let isReflectedVertical: boolean = false; + let startEvent; + let bottom = drag() + .on('start', (datum: MapDrawing) => { + document.body.style.cursor = "ns-resize"; + topEdge = datum.y; + console.log("started"); + y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + // startEvent = event; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y); + y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + let height = datum.element.height - dy; + if(height < 0) { + // height = datum.y - startEvent.y; + datum.y += height; + height = topEdge - datum.y; + // console.log(topEdge - datum.y); + } + console.log("Height", height); + datum.element.height = height; + + // datum.element.height -= dy; + // if(datum.element.height < 0) { + // datum.y -= datum.element.height; + // datum.element.height = Math.abs(datum.element.height); + // } + + // if (!isReflectedVertical) { + // if ((datum.element.height + evt.dy) < 0) { + // isReflectedVertical = true; + // y = topEdge; + // console.log(y); + // datum.element.height = Math.abs(datum.element.height + evt.dy); + // console.log(datum.element.height); + // } else { + // datum.element.height += evt.dy; + + // if (datum.element instanceof EllipseElement){ + // (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += evt.dy/2; + // (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += evt.dy/2; + // } + // } + // } else { + // dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y); + // y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + + // if ((datum.element.height + dy) < 0){ + // isReflectedVertical = false; + // y = topEdge; + // console.log(y); + // datum.element.height = Math.abs(datum.element.height + evt.dy); + // console.log(datum.element.height); + // } else { + // datum.y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + // datum.element.height += dy; + // if (datum.element instanceof EllipseElement) { + // (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += dy/2; + // (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += dy/2; + // } + // } + // } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let top = drag() + .on('start', (datum: MapDrawing) => { + y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + bottomEdge = y + datum.element.height; + document.body.style.cursor = "ns-resize"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedVertical) { + dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y); + y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + + if ((datum.element.height + dy) < 0){ + y = bottomEdge; + isReflectedVertical = true; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + datum.element.height += dy; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += dy/2; + } + } + } else { + if ((datum.element.height + evt.dy) < 0) { + isReflectedVertical = false; + y = bottomEdge; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.element.height += evt.dy; + + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += evt.dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += evt.dy/2; + } + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let x: number; + let dx: number; + let rightEdge: number; + let leftEdge: number; + let isReflectedHorizontal: boolean = false; + let right = drag() + .on('start', (datum: MapDrawing) => { + x = event.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + leftEdge = x + datum.element.width; + document.body.style.cursor = "ew-resize"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedHorizontal) { + dx = x - (evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x); + x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + + if ((datum.element.width + dx) < 0) { + x = leftEdge; + isReflectedHorizontal = true; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + datum.x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + datum.element.width += dx; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += dx/2; + } + } + } else { + if ((datum.element.width + evt.dx) < 0) { + x = leftEdge; + isReflectedHorizontal = false; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += evt.dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += evt.dx/2; + } + datum.element.width = (datum.element.width + evt.dx) < 0 ? 1 : datum.element.width += evt.dx; + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let left = drag() + .on('start', (datum: MapDrawing) => { + document.body.style.cursor = "ew-resize"; + rightEdge = datum.x; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedHorizontal) { + if ((datum.element.width + evt.dx) < 0) { + x = rightEdge; + isReflectedHorizontal = true; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += evt.dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += evt.dx/2; + } + datum.element.width = (datum.element.width + evt.dx) < 0 ? 1 : datum.element.width += evt.dx; + } + } else { + dx = x - (evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x); + x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + + if ((datum.element.width + dx) < 0) { + x = rightEdge; + isReflectedHorizontal = false; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + datum.x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + datum.element.width += dx; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += dx/2; + } + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let circleMoveRight = drag() + .on('start', () => { + document.body.style.cursor = "move"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + datum.element.width += evt.dx; + datum.element.height += evt.dy; + (datum.element as LineElement).x2 += evt.dx; + (datum.element as LineElement).y2 += evt.dy; + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let circleMoveLeft = drag() + .on('start', () => { + document.body.style.cursor = "move"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + datum.element.width += evt.dx; + datum.element.height += evt.dy; + (datum.element as LineElement).x1 += evt.dx; + (datum.element as LineElement).y1 += evt.dy; + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + merge + .select('line.bottom') + .call(bottom); + + merge + .select('line.top') + .call(top); + + merge + .select('line.right') + .call(right); + + merge + .select('line.left') + .call(left); + + merge + .select('circle.right') + .call(circleMoveRight) + + merge + .select('circle.left') + .call(circleMoveLeft) + } + + private createResizingEvent(datum: MapDrawing){ + const evt = new ResizingEnd(); + evt.x = datum.x; + evt.y = datum. y; + evt.width = datum.element.width; + evt.height = datum.element.height; + evt.datum = datum; + return evt; + } + + private selectDrawing(view: SVGSelection, drawing: MapDrawing) { + return view.selectAll(`g.drawing[drawing_id="${drawing.id}"]`); + } + +} diff --git a/src/app/cartography/widgets/drawings.ts b/src/app/cartography/widgets/drawings.ts index dad1c7af..96a1f1e5 100644 --- a/src/app/cartography/widgets/drawings.ts +++ b/src/app/cartography/widgets/drawings.ts @@ -1,18 +1,26 @@ -import { Injectable } from "@angular/core"; +import { Injectable, EventEmitter } from "@angular/core"; import { Widget } from "./widget"; import { SVGSelection } from "../models/types"; import { Layer } from "../models/layer"; import { SvgToDrawingConverter } from "../helpers/svg-to-drawing-converter"; -import { Draggable } from "../events/draggable"; +import { Draggable, DraggableDrag, DraggableStart, DraggableEnd } from "../events/draggable"; import { DrawingWidget } from "./drawing"; +import { drag, D3DragEvent } from "d3-drag"; +import { event } from "d3-selection"; import { MapDrawing } from "../models/map/map-drawing"; +import { Context } from "../models/context"; +import { EllipseElement } from "../models/drawings/ellipse-element"; +import { ResizingEnd } from "../events/resizing"; +import { LineElement } from "../models/drawings/line-element"; import { MapSettingsManager } from "../managers/map-settings-manager"; @Injectable() export class DrawingsWidget implements Widget { public draggable = new Draggable(); + public draggingEnabled = false; + public resizingFinished = new EventEmitter>(); // public onContextMenu = new EventEmitter(); // public onDrawingClicked = new EventEmitter(); @@ -22,6 +30,7 @@ export class DrawingsWidget implements Widget { constructor( private drawingWidget: DrawingWidget, private svgToDrawingConverter: SvgToDrawingConverter, + private context: Context, private mapSettings: MapSettingsManager ) { this.svgToDrawingConverter = new SvgToDrawingConverter(); @@ -63,9 +72,272 @@ export class DrawingsWidget implements Widget { if (!this.mapSettings.isReadOnly) { this.draggable.call(merge); } + + let y: number; + let dy: number; + let topEdge: number; + let bottomEdge: number; + let isReflectedVertical: boolean = false; + let bottom = drag() + .on('start', (datum: MapDrawing) => { + document.body.style.cursor = "ns-resize"; + topEdge = datum.y; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedVertical) { + if ((datum.element.height + evt.dy) < 0) { + isReflectedVertical = true; + y = topEdge; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.element.height += evt.dy; + + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += evt.dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += evt.dy/2; + } + } + } else { + dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y); + y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + + if ((datum.element.height + dy) < 0){ + isReflectedVertical = false; + y = topEdge; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + datum.element.height += dy; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += dy/2; + } + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let top = drag() + .on('start', (datum: MapDrawing) => { + y = event.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + bottomEdge = y + datum.element.height; + document.body.style.cursor = "ns-resize"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedVertical) { + dy = y - (evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y); + y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + + if ((datum.element.height + dy) < 0){ + y = bottomEdge; + isReflectedVertical = true; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.y = evt.sourceEvent.clientY - this.context.getZeroZeroTransformationPoint().y; + datum.element.height += dy; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += dy/2; + } + } + } else { + if ((datum.element.height + evt.dy) < 0) { + isReflectedVertical = false; + y = bottomEdge; + datum.element.height = Math.abs(datum.element.height + evt.dy); + } else { + datum.element.height += evt.dy; + + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cy = (datum.element as EllipseElement).cy + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).cy += evt.dy/2; + (datum.element as EllipseElement).ry = (datum.element as EllipseElement).ry + evt.dy/2 < 0 ? 1 : (datum.element as EllipseElement).ry += evt.dy/2; + } + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let x: number; + let dx: number; + let rightEdge: number; + let leftEdge: number; + let isReflectedHorizontal: boolean = false; + let right = drag() + .on('start', (datum: MapDrawing) => { + x = event.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + leftEdge = x + datum.element.width; + document.body.style.cursor = "ew-resize"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedHorizontal) { + dx = x - (evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x); + x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + + if ((datum.element.width + dx) < 0) { + x = leftEdge; + isReflectedHorizontal = true; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + datum.x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + datum.element.width += dx; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += dx/2; + } + } + } else { + if ((datum.element.width + evt.dx) < 0) { + x = leftEdge; + isReflectedHorizontal = false; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += evt.dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += evt.dx/2; + } + datum.element.width = (datum.element.width + evt.dx) < 0 ? 1 : datum.element.width += evt.dx; + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let left = drag() + .on('start', (datum: MapDrawing) => { + document.body.style.cursor = "ew-resize"; + rightEdge = datum.x; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + + if (!isReflectedHorizontal) { + if ((datum.element.width + evt.dx) < 0) { + x = rightEdge; + isReflectedHorizontal = true; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + if (datum.element instanceof EllipseElement){ + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += evt.dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + evt.dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += evt.dx/2; + } + datum.element.width = (datum.element.width + evt.dx) < 0 ? 1 : datum.element.width += evt.dx; + } + } else { + dx = x - (evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x); + x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + + if ((datum.element.width + dx) < 0) { + x = rightEdge; + isReflectedHorizontal = false; + datum.element.width = Math.abs(datum.element.width + evt.dx); + } else { + datum.x = evt.sourceEvent.clientX - this.context.getZeroZeroTransformationPoint().x; + datum.element.width += dx; + if (datum.element instanceof EllipseElement) { + (datum.element as EllipseElement).cx = (datum.element as EllipseElement).cx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).cx += dx/2; + (datum.element as EllipseElement).rx = (datum.element as EllipseElement).rx + dx/2 < 0 ? 1 : (datum.element as EllipseElement).rx += dx/2; + } + } + } + + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let circleMoveRight = drag() + .on('start', () => { + document.body.style.cursor = "move"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + datum.element.width += evt.dx; + datum.element.height += evt.dy; + (datum.element as LineElement).x2 += evt.dx; + (datum.element as LineElement).y2 += evt.dy; + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + let circleMoveLeft = drag() + .on('start', () => { + document.body.style.cursor = "move"; + }) + .on('drag', (datum: MapDrawing) => { + const evt = event; + datum.element.width += evt.dx; + datum.element.height += evt.dy; + (datum.element as LineElement).x1 += evt.dx; + (datum.element as LineElement).y1 += evt.dy; + this.redrawDrawing(view, datum); + }) + .on('end', (datum: MapDrawing) => { + document.body.style.cursor = "initial"; + this.resizingFinished.emit(this.createResizingEvent(datum)); + }); + + merge + .select('line.bottom') + .call(bottom); + + merge + .select('line.top') + .call(top); + + merge + .select('line.right') + .call(right); + + merge + .select('line.left') + .call(left); + + merge + .select('circle.right') + .call(circleMoveRight) + + merge + .select('circle.left') + .call(circleMoveLeft) + } + + private createResizingEvent(datum: MapDrawing){ + const evt = new ResizingEnd(); + evt.x = datum.x; + evt.y = datum. y; + evt.width = datum.element.width; + evt.height = datum.element.height; + evt.datum = datum; + return evt; } private selectDrawing(view: SVGSelection, drawing: MapDrawing) { return view.selectAll(`g.drawing[drawing_id="${drawing.id}"]`); } + } diff --git a/src/app/cartography/widgets/drawings/ellipse-drawing.ts b/src/app/cartography/widgets/drawings/ellipse-drawing.ts index 2e2aedf3..762fff99 100644 --- a/src/app/cartography/widgets/drawings/ellipse-drawing.ts +++ b/src/app/cartography/widgets/drawings/ellipse-drawing.ts @@ -21,6 +21,22 @@ export class EllipseDrawingWidget implements DrawingShapeWidget { return (d.element && d.element instanceof EllipseElement) ? [d.element] : []; }); + drawing.enter() + .append('line') + .attr("class", "top"); + + drawing.enter() + .append('line') + .attr("class", "bottom"); + + drawing.enter() + .append('line') + .attr("class", "right"); + + drawing.enter() + .append('line') + .attr("class", "left"); + const drawing_enter = drawing .enter() .append('ellipse') diff --git a/src/app/cartography/widgets/drawings/line-drawing.ts b/src/app/cartography/widgets/drawings/line-drawing.ts index 729b1be2..280156db 100644 --- a/src/app/cartography/widgets/drawings/line-drawing.ts +++ b/src/app/cartography/widgets/drawings/line-drawing.ts @@ -21,8 +21,15 @@ export class LineDrawingWidget implements DrawingShapeWidget { return (d.element && d.element instanceof LineElement) ? [d.element] : []; }); - const drawing_enter = drawing - .enter() + drawing.enter() + .append('circle') + .attr('class', 'right'); + + drawing.enter() + .append('circle') + .attr('class', 'left'); + + const drawing_enter = drawing.enter() .append('line') .attr('class', 'line_element noselect'); diff --git a/src/app/cartography/widgets/drawings/rect-drawing.ts b/src/app/cartography/widgets/drawings/rect-drawing.ts index ba909184..ef3e0895 100644 --- a/src/app/cartography/widgets/drawings/rect-drawing.ts +++ b/src/app/cartography/widgets/drawings/rect-drawing.ts @@ -9,6 +9,7 @@ import { MapDrawing } from "../../models/map/map-drawing"; @Injectable() export class RectDrawingWidget implements DrawingShapeWidget { + constructor( private qtDasharrayFixer: QtDasharrayFixer ) {} @@ -20,6 +21,22 @@ export class RectDrawingWidget implements DrawingShapeWidget { return (d.element && d.element instanceof RectElement) ? [d.element] : []; }); + drawing.enter() + .append('line') + .attr("class", "top"); + + drawing.enter() + .append('line') + .attr("class", "bottom"); + + drawing.enter() + .append('line') + .attr("class", "right"); + + drawing.enter() + .append('line') + .attr("class", "left"); + const drawing_enter = drawing .enter() .append('rect') diff --git a/src/app/cartography/widgets/drawings/text-drawing.ts b/src/app/cartography/widgets/drawings/text-drawing.ts index 7db42cc2..377b96a3 100644 --- a/src/app/cartography/widgets/drawings/text-drawing.ts +++ b/src/app/cartography/widgets/drawings/text-drawing.ts @@ -75,6 +75,7 @@ export class TextDrawingWidget implements DrawingShapeWidget { // approx and make it matching to GUI const tspan = select(this).selectAll('tspan'); const height = this.getBBox().height / tspan.size(); + //return `translate(0, ${height})`; return `translate(${TextDrawingWidget.MARGIN}, ${height - TextDrawingWidget.MARGIN})`; }); diff --git a/src/app/cartography/widgets/graph-layout.ts b/src/app/cartography/widgets/graph-layout.ts index a559853a..fe152411 100644 --- a/src/app/cartography/widgets/graph-layout.ts +++ b/src/app/cartography/widgets/graph-layout.ts @@ -8,6 +8,7 @@ import { MovingTool } from "../tools/moving-tool"; import { LayersWidget } from "./layers"; import { LayersManager } from "../managers/layers-manager"; import { Injectable } from "@angular/core"; +import { TextEditingTool } from '../tools/text-editing-tool'; @Injectable() @@ -17,6 +18,7 @@ export class GraphLayout implements Widget { private drawingLineTool: DrawingLineWidget, private selectionTool: SelectionTool, private movingTool: MovingTool, + private textEditingTool: TextEditingTool, private layersWidget: LayersWidget, private layersManager: LayersManager ) { @@ -67,6 +69,7 @@ export class GraphLayout implements Widget { this.drawingLineTool.draw(view, context); this.selectionTool.draw(view, context); this.movingTool.draw(view, context); + this.textEditingTool.draw(view); } disconnect(view: SVGSelection) { diff --git a/src/app/components/project-map/project-map.component.css b/src/app/components/project-map/project-map.component.css index f10ff2a3..29992d74 100644 --- a/src/app/components/project-map/project-map.component.css +++ b/src/app/components/project-map/project-map.component.css @@ -18,6 +18,84 @@ g.node:hover { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); } +.draw-menu { + position: fixed; + background: transparent; + top: 20px; + left: 92px; + width: 296px !important; + height: 72px !important; +} + +.draw-menu button { + outline: none; +} + +.mat-drawer-content { + display: inline !important; +} + +.drawer-container { + height: 72px !important; + background: transparent; +} + +.shadow { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); +} + +.drawer { + width: 296px !important; + height: 72px !important; + background:#263238; +} + +.drawer-content { + background: #F0F0F0!important; +} + +.drawer-button { + width: 40px; + margin-right: 12px!important; + margin-left: 12px!important; + background: #263238; + padding: 0; + border: none; + outline: none; + background-color: transparent; +} + +.drawer-arrow-button-right { + width: 40px; + height: 72px; + padding-top: 16px; + background:#263238; + position: fixed; +} + +.drawer-arrow-button-left { + width: 40px; + margin-left: 256px; + background:#263238; + position: fixed; +} + +.drawer-buttons { + background:#263238; + padding-top: 16px; + height: 72px; + display: flex; + flex-direction: row; +} + +.mat-drawer-backdrop.mat-drawer-shown { + background-color: transparent; +} + +.selected { + stroke: #0097a7!important; +} + .project-toolbar .mat-toolbar-multiple-rows { width: auto !important; } diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 39405f6d..f547df1e 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -10,8 +10,13 @@ [show-interface-labels]="project.show_interface_labels" [selection-tool]="tools.selection" [moving-tool]="tools.moving" + [text-editing-tool]="tools.text_editing" [draw-link-tool]="tools.draw_link" [readonly]="inReadOnlyMode" + (nodeDragged)="onNodeDragged($event)" + (drawingDragged)="onDrawingDragged($event)" + (onLinkCreated)="onLinkCreated($event)" + (onDrawingResized)="onDrawingResized($event)" > +
+ + +
+ + + + +
+ +
+
+
+ +
+ +
+
+
+
+ diff --git a/src/app/components/project-map/project-map.component.spec.ts b/src/app/components/project-map/project-map.component.spec.ts index c714cec4..231e877f 100644 --- a/src/app/components/project-map/project-map.component.spec.ts +++ b/src/app/components/project-map/project-map.component.spec.ts @@ -1,14 +1,135 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ProjectMapComponent } from './project-map.component'; +import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ServerService } from '../../services/server.service'; +import { ProjectService } from '../../services/project.service'; +import { SettingsService } from '../../services/settings.service'; +import { NodeService } from '../../services/node.service'; +import { LinkService } from '../../services/link.service'; +import { DrawingService } from '../../services/drawing.service'; +import { ProgressService } from '../../common/progress/progress.service'; +import { ProjectWebServiceHandler } from '../../handlers/project-web-service-handler'; +import { MapChangeDetectorRef } from '../../cartography/services/map-change-detector-ref'; +import { NodeWidget } from '../../cartography/widgets/node'; +import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-node-to-node-converter'; +import { MapPortToPortConverter } from '../../cartography/converters/map/map-port-to-port-converter'; +import { NodesDataSource } from '../../cartography/datasources/nodes-datasource'; +import { LinksDataSource } from '../../cartography/datasources/links-datasource'; +import { NodesEventSource } from '../../cartography/events/nodes-event-source'; +import { DrawingsEventSource } from '../../cartography/events/drawings-event-source'; +import { LinksEventSource } from '../../cartography/events/links-event-source'; +import { MapDrawingToSvgConverter } from '../../cartography/converters/map/map-drawing-to-svg-converter'; +import { DrawingsDataSource } from '../../cartography/datasources/drawings-datasource'; +import { CommonModule } from '@angular/common'; +import { ANGULAR_MAP_DECLARATIONS } from '../../cartography/angular-map.imports'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { SymbolService } from '../../services/symbol.service'; +import { MockedSettingsService } from '../../services/settings.service.spec'; +import { MockedServerService } from '../../services/server.service.spec'; +import { MockedProjectService } from '../../services/project.service.spec'; +import { Observable } from 'rxjs/Rx'; +import { Drawing } from '../../cartography/models/drawing'; +import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component'; +import { Project } from '../../models/project'; +import { of } from 'rxjs'; +import { DrawingElement } from '../../cartography/models/drawings/drawing-element'; +import { RectElement } from '../../cartography/models/drawings/rect-element'; +import { MapDrawing } from '../../cartography/models/map/map-drawing'; +import { HttpServer } from '../../services/http-server.service'; +import { Server } from '../../models/server'; +import { ResizedDataEvent } from '../../cartography/events/event-source'; +import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter'; + +export class MockedProgressService { + public activate() {} +} + +export class MockedDrawingService { + public drawing = {} as Drawing; + constructor() {} + + add(_server: Server, _project_id:string, _x: number, _y:number, _svg: string) { + return of(this.drawing); + } + + updatePosition(_server: Server, _drawing: Drawing, _x: number, _y: number){ + return of(this.drawing); + } + + updateSizeAndPosition(_server: Server, _drawing: Drawing, _x: number, _y: number, _svg: string){ + return of(this.drawing); + } + + update(_server: Server, _drawing: Drawing){ + return of(this.drawing); + } + + delete(_server: Server, _drawing: Drawing){ + return of(this.drawing); + } +} + +export class MockedDrawingsDataSource { + add() {} + + get() { return of({})} + + update() { return of({})} +} describe('ProjectMapComponent', () => { let component: ProjectMapComponent; let fixture: ComponentFixture; + let drawingService = new MockedDrawingService; + let drawingsDataSource = new MockedDrawingsDataSource; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ProjectMapComponent ], + imports: [ + MatIconModule, + MatToolbarModule, + MatMenuModule, + MatCheckboxModule, + CommonModule, + NoopAnimationsModule + ], + providers: [ + { provide: ActivatedRoute }, + { provide: ServerService, useClass: MockedServerService }, + { provide: ProjectService, useClass: MockedProjectService }, + { provide: SymbolService }, + { provide: NodeService }, + { provide: LinkService }, + { provide: DrawingService, useValue: drawingService}, + { provide: ProgressService, useClass: MockedProgressService }, + { provide: ProjectWebServiceHandler }, + { provide: MapChangeDetectorRef }, + { provide: NodeWidget }, + { provide: MapNodeToNodeConverter }, + { provide: MapPortToPortConverter }, + { provide: NodesDataSource }, + { provide: LinksDataSource }, + { provide: DrawingsDataSource, useValue: drawingsDataSource}, + { provide: NodesEventSource }, + { provide: DrawingsEventSource }, + { provide: LinksEventSource }, + { provide: MapDrawingToSvgConverter, useValue: { + convert: () => { return ''} + } }, + { provide: SettingsService, useClass: MockedSettingsService }, + { provide: MapLabelToLabelConverter} + ], + declarations: [ + ProjectMapComponent, + D3MapComponent, + ...ANGULAR_MAP_DECLARATIONS + ], + schemas: [ + NO_ERRORS_SCHEMA + ] }) .compileComponents(); })); @@ -16,7 +137,66 @@ describe('ProjectMapComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(ProjectMapComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should update position on resizing event', () => { + let drawingElement: DrawingElement; + let rect = new RectElement(); + rect.fill = "#ffffff"; + rect.fill_opacity = 1.0; + rect.stroke = "#000000"; + rect.stroke_width = 2; + rect.width = 200; + rect.height = 100; + drawingElement = rect; + let mapDrawing = new MapDrawing; + mapDrawing.id = '1'; + mapDrawing.projectId = '1'; + mapDrawing.rotation = 1; + mapDrawing.svg = ''; + mapDrawing.x = 0; + mapDrawing.y = 0; + mapDrawing.z = 0; + mapDrawing.element = drawingElement; + let event = new ResizedDataEvent(mapDrawing,0,0,100,100); + spyOn(drawingService, 'updateSizeAndPosition').and.returnValue( Observable.of({})); + + component.onDrawingResized(event); + + expect(drawingService.updateSizeAndPosition).toHaveBeenCalled(); + }); + + it('should add selected drawing', () => { + component.mapChild = { context: {} } as D3MapComponent; + component.project = { project_id: "1" } as Project; + component.mapChild.context.getZeroZeroTransformationPoint = jasmine.createSpy('HTML element').and.callFake(() => { return {x: 0, y: 0}}); + var dummyElement = document.createElement('map'); + document.getElementsByClassName = jasmine.createSpy('HTML element').and.callFake(() => { return ([dummyElement])}); + spyOn(drawingsDataSource, 'add'); + + component.addDrawing("rectangle"); + dummyElement.click(); + + expect(drawingsDataSource.add).toHaveBeenCalled(); + }); + + it('should hide draw tools when hide menu is called', () => { + var dummyElement = document.createElement('map'); + document.getElementsByClassName = jasmine.createSpy('HTML element').and.callFake(() => { return ([dummyElement])}); + spyOn(component, 'resetDrawToolChoice'); + + component.hideMenu(); + + expect(component.resetDrawToolChoice).toHaveBeenCalled(); + }); + + it('should return drawing mock of correct type'), () => { + let mock = component.getDrawingMock('rectangle'); + + expect(mock instanceof RectElement).toBe(true); + } }); diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index ced0364d..8e7d5aad 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -26,7 +26,7 @@ import { MapChangeDetectorRef } from '../../cartography/services/map-change-dete import { NodeContextMenu } from '../../cartography/events/nodes'; import { MapLinkCreated } from '../../cartography/events/links'; import { NodeWidget } from '../../cartography/widgets/node'; -import { DraggedDataEvent } from '../../cartography/events/event-source'; +import { DraggedDataEvent, ResizedDataEvent, TextEditedDataEvent } from '../../cartography/events/event-source'; import { DrawingService } from '../../services/drawing.service'; import { MapNodeToNodeConverter } from '../../cartography/converters/map/map-node-to-node-converter'; import { NodesEventSource } from '../../cartography/events/nodes-event-source'; @@ -35,10 +35,18 @@ import { MapNode } from '../../cartography/models/map/map-node'; import { LinksEventSource } from '../../cartography/events/links-event-source'; import { MapDrawing } from '../../cartography/models/map/map-drawing'; import { MapPortToPortConverter } from '../../cartography/converters/map/map-port-to-port-converter'; +import { MapDrawingToSvgConverter } from '../../cartography/converters/map/map-drawing-to-svg-converter'; +import { DrawingElement } from '../../cartography/models/drawings/drawing-element'; +import { RectElement } from '../../cartography/models/drawings/rect-element'; +import { EllipseElement } from '../../cartography/models/drawings/ellipse-element'; +import { LineElement } from '../../cartography/models/drawings/line-element'; import { SettingsService, Settings } from '../../services/settings.service'; import { MapLabel } from '../../cartography/models/map/map-label'; +import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component'; import { MapLinkNode } from '../../cartography/models/map/map-link-node'; +import { TextElement } from '../../cartography/models/drawings/text-element'; import { MapLabelToLabelConverter } from '../../cartography/converters/map/map-label-to-label-converter'; +import { select } from 'd3-selection'; @Component({ @@ -54,20 +62,32 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public symbols: Symbol[] = []; public project: Project; public server: Server; - + private drawListener: Function; private ws: Subject; tools = { 'selection': true, 'moving': false, - 'draw_link': false + 'draw_link': false, + 'text_editing': true }; protected settings: Settings; + protected drawTools = { + 'isRectangleChosen': false, + 'isEllipseChosen': false, + 'isLineChosen': false, + 'visibility': false, + 'isAddingTextChosen': false + }; + + protected selectedDrawing: string; + private inReadOnlyMode = false; @ViewChild(NodeContextMenuComponent) nodeContextMenu: NodeContextMenuComponent; + @ViewChild(D3MapComponent) mapChild: D3MapComponent; private subscriptions: Subscription[] = []; @@ -77,7 +97,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private projectService: ProjectService, private nodeService: NodeService, private linkService: LinkService, - private drawingService: DrawingService, + public drawingService: DrawingService, private progressService: ProgressService, private projectWebServiceHandler: ProjectWebServiceHandler, private mapChangeDetectorRef: MapChangeDetectorRef, @@ -90,6 +110,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private nodesEventSource: NodesEventSource, private drawingsEventSource: DrawingsEventSource, private linksEventSource: LinksEventSource, + private mapDrawingToSvgConverter: MapDrawingToSvgConverter, private settingsService: SettingsService, private mapLabelToLabel: MapLabelToLabelConverter ) {} @@ -169,6 +190,14 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.drawingsEventSource.dragged.subscribe((evt) => this.onDrawingDragged(evt)) ); + this.subscriptions.push( + this.drawingsEventSource.resized.subscribe((evt) => this.onDrawingResized(evt)) + ); + + this.subscriptions.push( + this.drawingsEventSource.textEdited.subscribe((evt) => this.onTextEdited(evt)) + ); + this.subscriptions.push( this.linksEventSource.created.subscribe((evt) => this.onLinkCreated(evt)) ); @@ -312,6 +341,32 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }); } + public onDrawingResized(resizedEvent: ResizedDataEvent) { + const drawing = this.drawingsDataSource.get(resizedEvent.datum.id); + let svgString = this.mapDrawingToSvgConverter.convert(resizedEvent.datum); + + this.drawingService + .updateSizeAndPosition(this.server, drawing, resizedEvent.x, resizedEvent.y, svgString) + .subscribe((serverDrawing: Drawing) => { + this.drawingsDataSource.update(serverDrawing); + }); + } + + public onTextEdited(evt: TextEditedDataEvent){ + let mapDrawing: MapDrawing = new MapDrawing(); + mapDrawing.element = evt.textElement; + (mapDrawing.element as TextElement).text = evt.editedText; + let svgString = this.mapDrawingToSvgConverter.convert(mapDrawing); + + let drawing = this.drawingsDataSource.get(evt.textDrawingId); + + this.drawingService + .updateText(this.server, drawing, svgString) + .subscribe((serverDrawing: Drawing) => { + this.drawingsDataSource.update(serverDrawing); + }); + } + public set readonly(value) { this.inReadOnlyMode = value; if (value) { @@ -340,6 +395,194 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.project.show_interface_labels = enabled; } + public addDrawing(selectedObject: string) { + if (selectedObject === this.selectedDrawing){ + var map = document.getElementsByClassName('map')[0]; + map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject); + this.resetDrawToolChoice(); + return; + } + + switch (selectedObject) { + case "rectangle": + this.drawTools.isAddingTextChosen = false; + this.drawTools.isEllipseChosen = false; + this.drawTools.isRectangleChosen = !this.drawTools.isRectangleChosen; + this.drawTools.isLineChosen = false; + break; + case "ellipse": + this.drawTools.isAddingTextChosen = false; + this.drawTools.isEllipseChosen = !this.drawTools.isEllipseChosen; + this.drawTools.isRectangleChosen = false; + this.drawTools.isLineChosen = false; + break; + case "line": + this.drawTools.isAddingTextChosen = false; + this.drawTools.isEllipseChosen = false; + this.drawTools.isRectangleChosen = false; + this.drawTools.isLineChosen = !this.drawTools.isLineChosen; + break; + } + + this.selectedDrawing = selectedObject; + var map = document.getElementsByClassName('map')[0]; + let mapDrawing: MapDrawing = this.getDrawingMock(selectedObject); + + let listener = (event: MouseEvent) => { + let x = event.clientX - this.mapChild.context.getZeroZeroTransformationPoint().x; + let y = event.clientY - this.mapChild.context.getZeroZeroTransformationPoint().y; + let svg = this.mapDrawingToSvgConverter.convert(mapDrawing); + + this.drawingService + .add(this.server, this.project.project_id, x, y, svg) + .subscribe((serverDrawing: Drawing) => { + this.drawingsDataSource.add(serverDrawing); + }); + this.resetDrawToolChoice(); + } + + map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject); + this.drawListener = listener; + map.addEventListener('click', this.drawListener as EventListenerOrEventListenerObject, {once : true}); + } + + public resetDrawToolChoice(){ + this.drawTools.isRectangleChosen = false; + this.drawTools.isEllipseChosen = false; + this.drawTools.isLineChosen = false; + this.drawTools.isAddingTextChosen = false; + this.selectedDrawing = ""; + } + + public hideMenu(){ + var map = document.getElementsByClassName('map')[0]; + map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject); + this.resetDrawToolChoice(); + this.drawTools.visibility = false; + } + + public showMenu(){ + setTimeout(() => { + this.drawTools.visibility = true; + }, + 200); + } + + public getDrawingMock(objectType: string, text?: string): MapDrawing { + let drawingElement: DrawingElement; + + switch (objectType) { + case "rectangle": + let rectElement = new RectElement(); + rectElement.fill = "#ffffff"; + rectElement.fill_opacity = 1.0; + rectElement.stroke = "#000000"; + rectElement.stroke_width = 2; + rectElement.width = 200; + rectElement.height = 100; + drawingElement = rectElement; + break; + case "ellipse": + let ellipseElement = new EllipseElement(); + ellipseElement.fill = "#ffffff"; + ellipseElement.fill_opacity = 1.0; + ellipseElement.stroke = "#000000"; + ellipseElement.stroke_width = 2; + ellipseElement.cx = 100; + ellipseElement.cy = 100; + ellipseElement.rx = 100; + ellipseElement.ry = 100; + ellipseElement.width = 200; + ellipseElement.height = 200; + drawingElement = ellipseElement; + break; + case "line": + let lineElement = new LineElement(); + lineElement.stroke = "#000000"; + lineElement.stroke_width = 2; + lineElement.x1 = 0; + lineElement.x2 = 200; + lineElement.y1 = 0; + lineElement.y2 = 0; + lineElement.width = 100; + lineElement.height = 0; + drawingElement = lineElement; + break; + case "text": + let textElement = new TextElement(); + textElement.height = 100; //should be calculated + textElement.width = 100; + textElement.text = text; + textElement.fill = "#000000"; + textElement.fill_opacity = 0; + textElement.font_family = "Noto Sans"; + textElement.font_size = 11; + textElement.font_weight = "bold"; + drawingElement = textElement; + break; + } + + let mapDrawing = new MapDrawing(); + mapDrawing.element = drawingElement; + return mapDrawing; + } + + public addText(){ + if (!this.drawTools.isAddingTextChosen){ + this.resetDrawToolChoice(); + this.drawTools.isAddingTextChosen = true; + var map = document.getElementsByClassName('map')[0]; + + let addTextListener = (event: MouseEvent) => { + + var div = document.createElement('div'); + div.style.width = "fit-content"; + div.style.left = event.clientX.toString() + 'px'; + div.style.top = (event.clientY - 10).toString() + 'px'; + div.style.position = "absolute"; + div.style.zIndex = "99"; + + div.style.fontFamily = "Noto Sans"; + div.style.fontSize = "11pt"; + div.style.fontWeight = "bold"; + div.style.color = "#000000"; + + div.setAttribute("contenteditable", "true"); + + document.body.appendChild(div); + div.innerText = ""; + div.focus(); + document.body.style.cursor = "text"; + + div.addEventListener("focusout", () => { + let savedText = div.innerText; + + let drawing = this.getDrawingMock("text", savedText); + (drawing.element as TextElement).text = savedText; + let svgText = this.mapDrawingToSvgConverter.convert(drawing); + + this.drawingService + .add(this.server, this.project.project_id, event.clientX - this.mapChild.context.getZeroZeroTransformationPoint().x, event.clientY - this.mapChild.context.getZeroZeroTransformationPoint().y, svgText) + .subscribe((serverDrawing: Drawing) => { + document.body.style.cursor = "default"; + div.remove(); + this.drawingsDataSource.add(serverDrawing); + }); + }); + + this.resetDrawToolChoice(); + } + + map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject); + this.drawListener = addTextListener; + map.addEventListener('click', this.drawListener as EventListenerOrEventListenerObject, {once : true}); + } else { + this.resetDrawToolChoice(); + var map = document.getElementsByClassName('map')[0]; + map.removeEventListener('click', this.drawListener as EventListenerOrEventListenerObject); + } + } + public ngOnDestroy() { this.drawingsDataSource.clear(); this.nodesDataSource.clear(); diff --git a/src/app/services/drawing.service.spec.ts b/src/app/services/drawing.service.spec.ts index 363e4bb5..1017acad 100644 --- a/src/app/services/drawing.service.spec.ts +++ b/src/app/services/drawing.service.spec.ts @@ -58,6 +58,24 @@ describe('DrawingService', () => { }); })); + it('should update size and position of drawing', inject([DrawingService], (service: DrawingService) => { + const drawing = new Drawing(); + drawing.project_id = "myproject"; + drawing.drawing_id = "id"; + let svgSample = "" + + service.updateSizeAndPosition(server, drawing, 100, 100, svgSample).subscribe(); + + const req = httpTestingController.expectOne( + 'http://127.0.0.1:3080/v2/projects/myproject/drawings/id'); + expect(req.request.method).toEqual("PUT"); + expect(req.request.body).toEqual({ + 'x': 100, + 'y': 100, + 'svg': svgSample + }); + })); + it('should update drawing', inject([DrawingService], (service: DrawingService) => { const drawing = new Drawing(); drawing.project_id = "myproject"; diff --git a/src/app/services/drawing.service.ts b/src/app/services/drawing.service.ts index 31ef46a7..851711cc 100644 --- a/src/app/services/drawing.service.ts +++ b/src/app/services/drawing.service.ts @@ -12,6 +12,15 @@ export class DrawingService { constructor(private httpServer: HttpServer) { } + add(server: Server, project_id:string, x: number, y:number, svg: string) { + return this.httpServer + .post(server, `/projects/${project_id}/drawings`, { + 'svg': svg, + 'x': x, + 'y': y + }); + } + updatePosition(server: Server, drawing: Drawing, x: number, y: number): Observable { return this.httpServer .put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, { @@ -20,6 +29,25 @@ export class DrawingService { }); } + updateSizeAndPosition(server: Server, drawing: Drawing, x: number, y: number, svg: string): Observable { + return this.httpServer + .put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, { + 'svg': svg, + 'x': x, + 'y': y + }) + } + + updateText(server: Server, drawing: Drawing, svg: string): Observable { + return this.httpServer + .put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, { + 'svg': svg, + 'x': drawing.x, + 'y': drawing.y, + 'z': drawing.z + }); + } + update(server: Server, drawing: Drawing): Observable { return this.httpServer .put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, {