Skip to content

Commit

Permalink
[1596] Add sample parametric SVG images to Sirius Web
Browse files Browse the repository at this point in the history
Generic support for them was added in v2022.11.0 with #1316, but there
are no concrete examples in Sirius Web which makes it difficult to
test for.

Bug: #1596
Signed-off-by: Pierre-Charles David <pierre-charles.david@obeo.fr>
  • Loading branch information
pcdavid authored and sbegaudeau committed Jan 24, 2023
1 parent fe30288 commit 80c1c92
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ image:doc/images/View_edit_begin_label_in_action.png[width=550,height=234]
- https://github.com/eclipse-sirius/sirius-components/issues/1574[#1574] [diagram] Single click tools can now be executed on Edges in addition to Nodes
- https://github.com/eclipse-sirius/sirius-components/issues/1569[#1569] [view] Only delegate semantic deletion to the element's _Delete Tool_
- https://github.com/eclipse-sirius/sirius-components/issues/1562[#1562] [view] The default/canonical behaviors for diagram elements can now be invoked explicitly from AQL expressions. See `org.eclipse.sirius.components.view.emf.CanonicalServices`. This feature will be used only for internal for now. There will be breaking changes on this topic soon.
- https://github.com/eclipse-sirius/sirius-components/issues/1596[#1596] [diagram] Sirius Web now includes two example parametric SVG images named "Package" and "Class".
They can be used as any custom image (e.g. in a View-based diagram), but their precise shape is partially computed on the backend, in this case to adjust the size of the label compartment to the actual label's width.

== v2023.1.0

Expand Down
15 changes: 15 additions & 0 deletions packages/sirius-web/backend/sirius-web-services/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@
<artifactId>sirius-web-services-api</artifactId>
<version>2023.1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.sirius</groupId>
<artifactId>sirius-components-diagrams-layout</artifactId>
<version>2023.1.1</version>
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis-ext</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.sirius</groupId>
<artifactId>sirius-components-collaborative-trees</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.services.diagram;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.eclipse.sirius.components.collaborative.diagrams.api.IParametricSVGImageFactory;
import org.eclipse.sirius.components.collaborative.diagrams.api.IParametricSVGImageRegistry;
import org.eclipse.sirius.components.collaborative.diagrams.api.ParametricSVGImage;
import org.eclipse.sirius.components.collaborative.diagrams.api.SVGAttribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
* Implementation of {@link IParametricSVGImageFactory}.
*
* @author lfasani
*/
@Service
public class ParametricSVGImageFactory implements IParametricSVGImageFactory {

private static final String HEIGHT = "height";

private static final String WIDTH = "width";

private static final String RECTANGLE_ELEMENT_LABEL_ID = "labelRectangle";

private static final String RECTANGLE_ELEMENT_MAIN_ID = "mainRectangle";

private static final Integer PADDING = 5;

private final Logger logger = LoggerFactory.getLogger(ParametricSVGImageFactory.class);

private final List<IParametricSVGImageRegistry> parametricSVGImageServices;

public ParametricSVGImageFactory(List<IParametricSVGImageRegistry> parametricSVGImageServices) {
this.parametricSVGImageServices = parametricSVGImageServices;
}

@Override
public Optional<byte[]> createSvg(String svgName, EnumMap<SVGAttribute, String> attributesValues) {

// @formatter:off
Optional<ParametricSVGImage> svgImageOpt = this.parametricSVGImageServices.stream()
.flatMap(service-> service.getImages().stream())
.filter(image -> {
return svgName.equals(image.getId().toString());
})
.findFirst();
// @formatter:on

if (svgImageOpt.isPresent()) {
ClassPathResource classPathResource = new ClassPathResource(svgImageOpt.get().getPath());
try (InputStream inputStream = classPathResource.getInputStream()) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document document = factory.newDocumentBuilder().parse(inputStream);
XPath xpath = XPathFactory.newInstance().newXPath();

// change the global size
// @formatter:off
Node svgNode = Optional.of(xpath.evaluate("/svg", document, XPathConstants.NODESET))
.filter(NodeList.class::isInstance)
.map(NodeList.class::cast)
.filter(nodeList -> nodeList.getLength() > 0)
.map(nodeList -> nodeList.item(0))
.orElse(null);
// @formatter:on

if (svgNode instanceof Element element) {
Double halfBorderSize = Double.valueOf(attributesValues.get(SVGAttribute.BORDERSIZE)) * 0.5d;
String realWidth = String.valueOf(Double.valueOf(attributesValues.get(SVGAttribute.WIDTH)) + halfBorderSize * 2);
String realHeight = String.valueOf(Double.valueOf(attributesValues.get(SVGAttribute.HEIGHT)) + halfBorderSize * 2);
String viewBox = String.format(Locale.US, "-%f -%f %s %s", halfBorderSize, halfBorderSize, realWidth, realHeight);
element.setAttribute(WIDTH, realWidth);
element.setAttribute(HEIGHT, realHeight);
element.setAttribute("viewBox", viewBox);

}

String expr = String.format("//*[contains(@id, '%s')]|//*[contains(@id, '%s')]", RECTANGLE_ELEMENT_LABEL_ID, RECTANGLE_ELEMENT_MAIN_ID);

// @formatter:off
Optional.of(xpath.evaluate(expr, document, XPathConstants.NODESET))
.filter(NodeList.class::isInstance)
.map(NodeList.class::cast)
.ifPresent(nodes-> {
for (int i = 0; i < nodes.getLength(); i++) {
Element node = (Element) nodes.item(i);
this.updateNode(attributesValues, node, svgImageOpt.get());
}
});
// @formatter:on

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Source xmlSource = new DOMSource(document);
Result outputTarget = new StreamResult(outputStream);
try {
TransformerFactory.newInstance().newTransformer().transform(xmlSource, outputTarget);
} catch (TransformerException | TransformerFactoryConfigurationError e) {
this.logger.warn(e.getMessage());
}

InputStream documentInputStream = new ByteArrayInputStream(outputStream.toByteArray());
return Optional.of(documentInputStream.readAllBytes());
} catch (IOException | ParserConfigurationException | XPathExpressionException | SAXException e) {
this.logger.warn(e.getMessage());
}
}

return Optional.empty();
}

private void updateNode(EnumMap<SVGAttribute, String> attributesValues, Element node, ParametricSVGImage parametricSVGImage) {
String idValue = node.getAttributes().getNamedItem("id").getNodeValue();
Node styleNode = node.getAttributes().getNamedItem("style");
if (styleNode != null) {
String style = styleNode.getNodeValue();
for (SVGAttribute svgAttribute : attributesValues.keySet()) {
String styleProperty = null;
if (SVGAttribute.COLOR.equals(svgAttribute)) {
styleProperty = "fill";
} else if (SVGAttribute.BORDERCOLOR.equals(svgAttribute)) {
styleProperty = "stroke";
} else if (SVGAttribute.BORDERSIZE.equals(svgAttribute)) {
styleProperty = "stroke-width";
} else if (SVGAttribute.BORDERSTYLE.equals(svgAttribute)) {
styleProperty = "stroke-dasharray";
}
if (styleProperty != null) {
style = this.updateStyleValue(style, styleProperty, attributesValues.get(svgAttribute));
}
}
node.setAttribute("style", style);
}
if (RECTANGLE_ELEMENT_LABEL_ID.equals(idValue)) {
// TODO we should use ICustomNodeLabelPositionProvider
if (parametricSVGImage.getLabel().equals("Class")) {
// The label is centered so the label area is the same size than the container
node.setAttribute(WIDTH, attributesValues.get(SVGAttribute.WIDTH));
} else {
// The label is left aligned
node.setAttribute(WIDTH, String.valueOf(Double.valueOf(attributesValues.get(SVGAttribute.LABELWIDTH)) + 2 * PADDING));
}
node.setAttribute(HEIGHT, String.valueOf(Double.valueOf(attributesValues.get(SVGAttribute.LABELHEIGHT)) + 2 * PADDING));
} else if (RECTANGLE_ELEMENT_MAIN_ID.equals(idValue)) {
Double globalHeight = 100.;
Double labelHeight = 10.;
try {
globalHeight = Double.valueOf(attributesValues.get(SVGAttribute.HEIGHT));
labelHeight = Double.valueOf(Double.valueOf(attributesValues.get(SVGAttribute.LABELHEIGHT)) + 2 * PADDING);
} catch (NumberFormatException e) {
this.logger.error(e.getMessage());
}
Double mainRectangleYPosition = labelHeight;
Double mainRectangleHeight = globalHeight - labelHeight;
node.setAttribute("y", mainRectangleYPosition.toString());
node.setAttribute(HEIGHT, mainRectangleHeight.toString());
node.setAttribute(WIDTH, attributesValues.get(SVGAttribute.WIDTH));
}
}

private String updateStyleValue(String style, String styleProperty, String newValue) {
String updatedStyle = style;
if (style.contains(styleProperty)) {
updatedStyle = style.replaceFirst(styleProperty + ":(.*?)([;\"])", styleProperty + ":" + newValue + "$2");
} else {
updatedStyle = styleProperty + ":" + newValue + ";" + updatedStyle;
}
return updatedStyle;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.services.diagram;

import java.util.List;
import java.util.UUID;

import org.eclipse.sirius.components.collaborative.diagrams.api.IParametricSVGImageRegistry;
import org.eclipse.sirius.components.collaborative.diagrams.api.ParametricSVGImage;
import org.springframework.stereotype.Service;

/**
* Provide a IParametricSVGImageRegistry.
*
* @author lfasani
*/
@Service
public class ParametricSVGImageRegistry implements IParametricSVGImageRegistry {
@Override
public List<ParametricSVGImage> getImages() {
return List.of(new ParametricSVGImage(UUID.nameUUIDFromBytes("Package".getBytes()), "Package", "parametricSVGs/package.svg"),
new ParametricSVGImage(UUID.nameUUIDFromBytes("Class".getBytes()), "Class", "parametricSVGs/class.svg"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2023 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.web.services.diagram;

import java.util.Optional;
import java.util.UUID;

import org.eclipse.elk.core.math.ElkPadding;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.sirius.components.diagrams.INodeStyle;
import org.eclipse.sirius.components.diagrams.ParametricSVGNodeStyle;
import org.eclipse.sirius.components.diagrams.Position;
import org.eclipse.sirius.components.diagrams.Size;
import org.eclipse.sirius.components.diagrams.layout.ISiriusWebLayoutConfigurator;
import org.eclipse.sirius.components.diagrams.layout.incremental.provider.ICustomNodeLabelPositionProvider;
import org.springframework.stereotype.Service;

/**
* Customize the label position for parametric SVG node styled nodes.
*
* @author lfasani
*/
@Service
public class ParametricSVGNodeStyleLabelPositionProvider implements ICustomNodeLabelPositionProvider {

@Override
public Optional<Position> getLabelPosition(ISiriusWebLayoutConfigurator layoutConfigurator, Size initialLabelSize, Size nodeSize, String nodeType, INodeStyle nodeStyle) {
Optional<Position> positionOpt = Optional.empty();
if (nodeStyle instanceof ParametricSVGNodeStyle parametricNodeStyle) {
String svgURL = parametricNodeStyle.getSvgURL();
if (svgURL.contains(UUID.nameUUIDFromBytes("Class".getBytes()).toString())) {
// horizontally centered
ElkPadding labelPadding = layoutConfigurator.configureByType(nodeType).getProperty(CoreOptions.NODE_LABELS_PADDING);
positionOpt = Optional.of(Position.at(nodeSize.getWidth() / 2 - initialLabelSize.getWidth() / 2, labelPadding.getTop()));
} else {
// horizontally left
ElkPadding labelPadding = layoutConfigurator.configureByType(nodeType).getProperty(CoreOptions.NODE_LABELS_PADDING);
positionOpt = Optional.of(Position.at(labelPadding.getLeft(), labelPadding.getTop()));
}
}
return positionOpt;
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 80c1c92

Please sign in to comment.