Added native Bazel build to GUI2. Reduced a lot of the unused Angular CLI structures
Reviewers should look at the changes in WORKSPACE, BUILD, BUILD.bazel, README.md files
This is only possible now as rules_nodejs went to 1.0.0 on December 20
gui2 has now been made the entry point (rather than gui2-fw-lib)
No tests or linting are functional yet for Typescript
Each NgModule now has its own BUILD.bazel file with ng_module
gui2-fw-lib is all one module and has been refactored to simplify the directory structure
gui2-topo-lib is also all one module - its directory structure has had 3 layers removed
The big bash script in web/gui2/BUILD has been removed - all is done through ng_module rules
in web/gui2/src/main/webapp/BUILD.bazel and web/gui2/src/main/webapp/app/BUILD.bazel
Change-Id: Ifcfcc23a87be39fe6d6c8324046cc8ebadb90551
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts
new file mode 100644
index 0000000..2fb9155
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.spec.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {ForceDirectedGraph, Options} from './force-directed-graph';
+import {Node} from './node';
+import {Link} from './link';
+import {LogService} from '../../../../../gui2-fw-lib/public_api';
+import {TestBed} from '@angular/core/testing';
+
+export class TestNode extends Node {
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+export class TestLink extends Link {
+ constructor(source: Node, target: Node) {
+ super(source, target);
+ }
+}
+
+/**
+ * ONOS GUI -- ForceDirectedGraph - Unit Tests
+ */
+describe('ForceDirectedGraph', () => {
+ let logServiceSpy: jasmine.SpyObj<LogService>;
+ let fdg: ForceDirectedGraph;
+ const options: Options = {width: 1000, height: 1000};
+
+ beforeEach(() => {
+ const logSpy = jasmine.createSpyObj('LogService', ['info', 'debug', 'warn', 'error']);
+ const nodes: Node[] = [];
+ const links: Link[] = [];
+ fdg = new ForceDirectedGraph(options, logSpy);
+
+ for (let i = 0; i < 10; i++) {
+ const newNode: TestNode = new TestNode('id' + i);
+ nodes.push(newNode);
+ }
+ for (let j = 1; j < 10; j++) {
+ const newLink = new TestLink(nodes[0], nodes[j]);
+ links.push(newLink);
+ }
+ fdg.nodes = nodes;
+ fdg.links = links;
+ fdg.reinitSimulation();
+ logServiceSpy = TestBed.get(LogService);
+ });
+
+ afterEach(() => {
+ fdg.stopSimulation();
+ fdg.nodes = [];
+ fdg.links = [];
+ fdg.reinitSimulation();
+ });
+
+ it('should be created', () => {
+ expect(fdg).toBeTruthy();
+ });
+
+ it('should have simulation', () => {
+ expect(fdg.simulation).toBeTruthy();
+ });
+
+ it('should have 10 nodes', () => {
+ expect(fdg.nodes.length).toEqual(10);
+ });
+
+ it('should have 10 links', () => {
+ expect(fdg.links.length).toEqual(9);
+ });
+
+ // TODO fix these up to listen for tick
+ // it('nodes should not be at zero', () => {
+ // expect(nodes[0].x).toBeGreaterThan(0);
+ // });
+ // it('ticker should emit', () => {
+ // let tickMe = jasmine.createSpy("tickMe() spy");
+ // fdg.ticker.subscribe((simulation) => tickMe());
+ // expect(tickMe).toHaveBeenCalled();
+ // });
+
+ // it('init links chould be called ', () => {
+ // spyOn(fdg, 'initLinks');
+ // // expect(fdg).toBeTruthy();
+ // fdg.reinitSimulation(options);
+ // expect(fdg.initLinks).toHaveBeenCalled();
+ // });
+});
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts
new file mode 100644
index 0000000..7bf44af
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/force-directed-graph.ts
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { EventEmitter } from '@angular/core';
+import { Link } from './link';
+import { Node } from './node';
+import * as d3 from 'd3-force';
+import {LogService} from '../../../../../gui2-fw-lib/public_api';
+
+const FORCES = {
+ COLLISION: 1,
+ GRAVITY: 0.4,
+ FRICTION: 0.7
+};
+
+const CHARGES = {
+ device: -800,
+ host: -2000,
+ region: -800,
+ _def_: -1200
+};
+
+const LINK_DISTANCE = {
+ // note: key is link.type
+ direct: 100,
+ optical: 120,
+ UiEdgeLink: 3,
+ UiDeviceLink: 100,
+ _def_: 50,
+};
+
+/**
+ * note: key is link.type
+ * range: {0.0 ... 1.0}
+ */
+const LINK_STRENGTH = {
+ _def_: 0.5
+};
+
+export interface Options {
+ width: number;
+ height: number;
+}
+
+/**
+ * The inspiration for this approach comes from
+ * https://medium.com/netscape/visualizing-data-with-angular-and-d3-209dde784aeb
+ *
+ * Do yourself a favour and read https://d3indepth.com/force-layout/
+ */
+export class ForceDirectedGraph {
+ public ticker: EventEmitter<d3.Simulation<Node, Link>> = new EventEmitter();
+ public simulation: d3.Simulation<any, any>;
+ public canvasOptions: Options;
+ public nodes: Node[] = [];
+ public links: Link[] = [];
+
+ constructor(options: Options, public log: LogService) {
+ this.canvasOptions = options;
+ const ticker = this.ticker;
+
+ // Creating the force simulation and defining the charges
+ this.simulation = d3.forceSimulation()
+ .force('charge',
+ d3.forceManyBody().strength(this.charges.bind(this)))
+ // .distanceMin(100).distanceMax(500))
+ .force('gravity',
+ d3.forceManyBody().strength(FORCES.GRAVITY))
+ .force('friction',
+ d3.forceManyBody().strength(FORCES.FRICTION))
+ .force('center',
+ d3.forceCenter(this.canvasOptions.width / 2, this.canvasOptions.height / 2))
+ .force('x', d3.forceX())
+ .force('y', d3.forceY())
+ .on('tick', () => {
+ ticker.emit(this.simulation); // ForceSvgComponent.ngOnInit listens
+ });
+
+ }
+
+ /**
+ * Assigning updated node and restarting the simulation
+ * Setting alpha to 0.3 and it will count down to alphaTarget=0
+ */
+ public reinitSimulation() {
+ this.simulation.nodes(this.nodes);
+ this.simulation.force('link',
+ d3.forceLink(this.links)
+ .strength(LINK_STRENGTH._def_)
+ .distance(this.distance.bind(this))
+ );
+ this.simulation.alpha(0.3).restart();
+ }
+
+ charges(node: Node) {
+ const nodeType = node.nodeType;
+ return CHARGES[nodeType] || CHARGES._def_;
+ }
+
+ distance(link: Link) {
+ const linkType = link.type;
+ return LINK_DISTANCE[linkType] || LINK_DISTANCE._def_;
+ }
+
+ stopSimulation() {
+ this.simulation.stop();
+ this.log.debug('Simulation stopped');
+ }
+
+ public restartSimulation(alpha: number = 0.3) {
+ this.simulation.alpha(alpha).restart();
+ this.log.debug('Simulation restarted. Alpha:', alpha);
+ }
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts
new file mode 100644
index 0000000..36fd2e7
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './node';
+export * from './link';
+export * from './regions';
+
+export * from './force-directed-graph';
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts
new file mode 100644
index 0000000..d5ff2a7
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/link.ts
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {Node, UiElement} from './node';
+import * as d3 from 'd3';
+
+export enum LinkType {
+ UiRegionLink,
+ UiDeviceLink,
+ UiEdgeLink
+}
+
+/**
+ * model of the topo2CurrentRegion region rollup from Region below
+ *
+ */
+export interface RegionRollup {
+ id: string;
+ epA: string;
+ epB: string;
+ portA: string;
+ portB: string;
+ type: LinkType;
+}
+
+/**
+ * Implementing SimulationLinkDatum interface into our custom Link class
+ */
+export class Link implements UiElement, d3.SimulationLinkDatum<Node> {
+ // Optional - defining optional implementation properties - required for relevant typing assistance
+ index?: number;
+ id: string; // The id of the link in the format epA/portA~epB/portB
+ epA: string; // The name of the device or host at one end
+ epB: string; // The name of the device or host at the other end
+ portA: string; // The number of the port at one end
+ portB: string; // The number of the port at the other end
+ type: LinkType;
+ rollup: RegionRollup[]; // Links in sub regions represented by this one link
+
+ // Must - defining enforced implementation properties
+ source: Node;
+ target: Node;
+
+ public static deviceNameFromEp(ep: string): string {
+ if (ep !== undefined && ep.lastIndexOf('/') > 0) {
+ return ep.substr(0, ep.lastIndexOf('/'));
+ }
+ return ep;
+ }
+
+ /**
+ * The WSS event showHighlights is sent up with a slightly different
+ * name format on the link id using the "-" separator rather than the "~"
+ * @param linkId The id of the link in either format
+ */
+ public static linkIdFromShowHighlights(linkId: string) {
+ if (linkId.includes('-')) {
+ const parts: string[] = linkId.split('-');
+ const part0 = Link.removeHostPortNum(parts[0]);
+ const part1 = Link.removeHostPortNum(parts[1]);
+ return part0 + '~' + part1;
+ }
+ return linkId;
+ }
+
+ private static removeHostPortNum(hostStr: string) {
+ if (hostStr.includes('/None/')) {
+ const subparts = hostStr.split('/');
+ return subparts[0] + '/' + subparts[1];
+ }
+ return hostStr;
+ }
+
+ constructor(source, target) {
+ this.source = source;
+ this.target = target;
+ }
+
+ linkTypeStr(): string {
+ return LinkType[this.type];
+ }
+}
+
+/**
+ * model of the topo2CurrentRegion region link from Region
+ */
+export class RegionLink extends Link {
+
+ constructor(type: LinkType, nodeA: Node, nodeB: Node) {
+ super(nodeA, nodeB);
+ this.type = type;
+ }
+}
+
+/**
+ * model of the highlights that are sent back from WebSocket when traffic is shown
+ */
+export interface LinkHighlight {
+ id: string;
+ css: string;
+ label: string;
+ fadems: number;
+}
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts
new file mode 100644
index 0000000..5bcba8c
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/node.ts
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as d3 from 'd3';
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {LayerType, Location, NodeType, RegionProps} from './regions';
+import {LocMeta, MetaUi, ZoomUtils} from '../../../../../gui2-fw-lib/public_api';
+
+export interface UiElement {
+ index?: number;
+ id: string;
+}
+
+export namespace LabelToggle {
+ /**
+ * Toggle state for how device labels should be displayed
+ */
+ export enum Enum {
+ NONE,
+ ID,
+ NAME
+ }
+
+ /**
+ * Add the method 'next()' to the LabelToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.NONE) {
+ return Enum.ID;
+ } else if (current === Enum.ID) {
+ return Enum.NAME;
+ } else if (current === Enum.NAME) {
+ return Enum.NONE;
+ }
+ }
+}
+
+export namespace HostLabelToggle {
+ /**
+ * Toggle state for how host labels should be displayed
+ */
+ export enum Enum {
+ NONE,
+ NAME,
+ IP,
+ MAC
+ }
+
+ /**
+ * Add the method 'next()' to the HostLabelToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.NONE) {
+ return Enum.NAME;
+ } else if (current === Enum.NAME) {
+ return Enum.IP;
+ } else if (current === Enum.IP) {
+ return Enum.MAC;
+ } else if (current === Enum.MAC) {
+ return Enum.NONE;
+ }
+ }
+}
+
+export namespace GridDisplayToggle {
+ /**
+ * Toggle state for how the grid should be displayed
+ */
+ export enum Enum {
+ GRIDNONE,
+ GRID1000,
+ GRIDGEO,
+ GRIDBOTH
+ }
+
+ /**
+ * Add the method 'next()' to the GridDisplayToggle enum above
+ */
+ export function next(current: Enum) {
+ if (current === Enum.GRIDNONE) {
+ return Enum.GRID1000;
+ } else if (current === Enum.GRID1000) {
+ return Enum.GRIDGEO;
+ } else if (current === Enum.GRIDGEO) {
+ return Enum.GRIDBOTH;
+ } else if (current === Enum.GRIDBOTH) {
+ return Enum.GRIDNONE;
+ }
+ }
+}
+
+/**
+ * model of the topo2CurrentRegion device props from Device below
+ */
+export interface DeviceProps {
+ latitude: string;
+ longitude: string;
+ gridX: number;
+ gridY: number;
+ name: string;
+ locType: LocationType;
+ uiType: string;
+ channelId: string;
+ managementAddress: string;
+ protocol: string;
+ driver: string;
+}
+
+export interface HostProps {
+ gridX: number;
+ gridY: number;
+ latitude: number;
+ longitude: number;
+ locType: LocationType;
+ name: string;
+}
+
+/**
+ * Implementing SimulationNodeDatum interface into our custom Node class
+ */
+export class Node implements UiElement, d3.SimulationNodeDatum {
+ // Optional - defining optional implementation properties - required for relevant typing assistance
+ index?: number;
+ x: number;
+ y: number;
+ vx?: number;
+ vy?: number;
+ fx?: number | null;
+ fy?: number | null;
+ nodeType: NodeType;
+ location: Location;
+ id: string;
+
+ protected constructor(id) {
+ this.id = id;
+ this.x = 0;
+ this.y = 0;
+ }
+
+ /**
+ * Static method to reset the node's position to that specified in its
+ * coordinates
+ * This is overridden for host
+ * @param node The node to reset
+ */
+ static resetNodeLocation(node: Node): void {
+ let origLoc: MetaUi;
+
+ if (!node.location || node.location.locType === LocationType.NONE) {
+ // No location - nothing to do
+ return;
+ } else if (node.location.locType === LocationType.GEO) {
+ origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+ lng: node.location.longOrX,
+ lat: node.location.latOrY
+ });
+ } else if (node.location.locType === LocationType.GRID) {
+ origLoc = ZoomUtils.convertXYtoGeo(
+ node.location.longOrX, node.location.latOrY);
+ }
+ Node.moveNodeTo(node, origLoc);
+ }
+
+ protected static moveNodeTo(node: Node, origLoc: MetaUi) {
+ const currentX = node.fx;
+ const currentY = node.fy;
+ const distX = origLoc.x - node.fx;
+ const distY = origLoc.y - node.fy;
+ let count = 0;
+ const task = setInterval(() => {
+ count++;
+ if (count >= 10) {
+ clearInterval(task);
+ }
+ node.fx = currentX + count * distX / 10;
+ node.fy = currentY + count * distY / 10;
+ }, 50);
+ }
+
+ static unpinOrFreezeNode(node: Node, freeze: boolean): void {
+ if (!node.location || node.location.locType === LocationType.NONE) {
+ if (freeze) {
+ node.fx = node.x;
+ node.fy = node.y;
+ } else {
+ node.fx = null;
+ node.fy = null;
+ }
+ }
+ }
+}
+
+export interface Badge {
+ status: string;
+ isGlyph: boolean;
+ txt: string;
+ msg: string;
+}
+
+/**
+ * model of the topo2CurrentRegion device from Region below
+ */
+export class Device extends Node {
+ id: string;
+ layer: LayerType;
+ metaUi: MetaUi;
+ master: string;
+ online: boolean;
+ props: DeviceProps;
+ type: string;
+
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+export interface DeviceHighlight {
+ id: string;
+ badge: Badge;
+}
+
+export interface HostHighlight {
+ id: string;
+ badge: Badge;
+}
+
+/**
+ * Model of the ONOS Host element in the topology
+ */
+export class Host extends Node {
+ configured: boolean;
+ id: string;
+ ips: string[];
+ layer: LayerType;
+ props: HostProps;
+
+ constructor(id: string) {
+ super(id);
+ }
+
+ static resetNodeLocation(host: Host): void {
+ let origLoc: MetaUi;
+
+ if (!host.props || host.props.locType === LocationType.NONE) {
+ // No location - nothing to do
+ return;
+ } else if (host.props.locType === LocationType.GEO) {
+ origLoc = ZoomUtils.convertGeoToCanvas(<LocMeta>{
+ lng: host.props.longitude,
+ lat: host.props.latitude
+ });
+ } else if (host.props.locType === LocationType.GRID) {
+ origLoc = ZoomUtils.convertXYtoGeo(
+ host.props.gridX, host.props.gridY);
+ }
+ Node.moveNodeTo(host, origLoc);
+ }
+}
+
+
+/**
+ * model of the topo2CurrentRegion subregion from Region below
+ */
+export class SubRegion extends Node {
+ id: string;
+ location: Location;
+ nDevs: number;
+ nHosts: number;
+ name: string;
+ props: RegionProps;
+
+ constructor(id: string) {
+ super(id);
+ }
+}
+
+/**
+ * Enumerated values for topology update event types
+ */
+export enum ModelEventType {
+ HOST_ADDED_OR_UPDATED,
+ LINK_ADDED_OR_UPDATED,
+ DEVICE_ADDED_OR_UPDATED,
+ DEVICE_REMOVED,
+ HOST_REMOVED,
+ LINK_REMOVED,
+}
+
+/**
+ * Enumerated values for topology update event memo field
+ */
+export enum ModelEventMemo {
+ ADDED = 'added',
+ REMOVED = 'removed',
+ UPDATED = 'updated'
+}
+
diff --git a/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts b/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts
new file mode 100644
index 0000000..3c1894b
--- /dev/null
+++ b/web/gui2-topo-lib/lib/layer/forcesvg/models/regions.ts
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the 'License');
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an 'AS IS' BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Enum of the topo2CurrentRegion node type from SubRegion below
+ */
+import {LocationType} from '../../backgroundsvg/backgroundsvg.component';
+import {Device, Host, SubRegion} from './node';
+import {RegionLink} from './link';
+
+export enum NodeType {
+ REGION = 'region',
+ DEVICE = 'device',
+ HOST = 'host',
+}
+
+/**
+ * Enum of the topo2CurrentRegion layerOrder from Region below
+ */
+export enum LayerType {
+ LAYER_OPTICAL = 'opt',
+ LAYER_PACKET = 'pkt',
+ LAYER_DEFAULT = 'def'
+}
+
+/**
+ * model of the topo2CurrentRegion location from SubRegion below
+ */
+export interface Location {
+ locType: LocationType;
+ latOrY: number;
+ longOrX: number;
+}
+
+/**
+ * model of the topo2CurrentRegion props from SubRegion below
+ */
+export interface RegionProps {
+ latitude: number;
+ longitude: number;
+ name: string;
+ peerLocations: string;
+}
+
+/**
+ * model of the topo2CurrentRegion WebSocket response
+ *
+ * The Devices are in a 2D array - 1st order is layer type, 2nd order is
+ * devices in that layer
+ */
+export interface Region {
+ note?: string;
+ id: string;
+ devices: Device[][];
+ hosts: Host[][];
+ links: RegionLink[];
+ layerOrder: LayerType[];
+ peerLocations?: Location[];
+ subregions: SubRegion[];
+}
+
+/**
+ * model of the topo2PeerRegions WebSocket response
+ */
+export interface Peer {
+ peers: SubRegion[];
+}
+