/*******************************************************************************
* Copyright (c) 2016 Weasis Team and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Nicolas Roduit - initial API and implementation
*******************************************************************************/
package org.weasis.dicom.explorer.pr;
import java.awt.Color;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.weasis.core.api.gui.util.MathUtil;
import org.weasis.core.api.image.util.CIELab;
import org.weasis.core.api.media.data.ImageElement;
import org.weasis.core.api.media.data.TagW;
import org.weasis.core.api.util.GzipManager;
import org.weasis.core.ui.model.GraphicModel;
import org.weasis.core.ui.model.graphic.Graphic;
import org.weasis.core.ui.model.graphic.imp.NonEditableGraphic;
import org.weasis.core.ui.model.graphic.imp.PointGraphic;
import org.weasis.core.ui.model.graphic.imp.area.EllipseGraphic;
import org.weasis.core.ui.model.graphic.imp.area.PolygonGraphic;
import org.weasis.core.ui.model.graphic.imp.line.PolylineGraphic;
import org.weasis.core.ui.model.imp.XmlGraphicModel;
import org.weasis.core.ui.model.utils.exceptions.InvalidShapeException;
import org.weasis.dicom.codec.PresentationStateReader;
import org.weasis.dicom.codec.utils.DicomMediaUtils;
public class PrGraphicUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(PrGraphicUtil.class);
public static final String POINT = "POINT"; //$NON-NLS-1$
public static final String POLYLINE = "POLYLINE"; //$NON-NLS-1$
public static final String INTERPOLATED = "INTERPOLATED"; //$NON-NLS-1$
public static final String CIRCLE = "CIRCLE"; //$NON-NLS-1$
public static final String ELLIPSE = "ELLIPSE"; //$NON-NLS-1$
private PrGraphicUtil() {
}
public static Graphic buildGraphic(Attributes go, Color color, boolean labelVisible, double width, double height,
boolean canBeEdited, AffineTransform inverse, boolean dcmSR) throws InvalidShapeException {
/*
* For DICOM SR
*
* Graphic Type: POINT, POLYLINE (always closed), MULTIPOINT, CIRCLE and ELLIPSE
*
* Coordinates are always pixel coordinates
*/
/*
* For DICOM PR
*
* Graphic Type: POINT, POLYLINE, INTERPOLATED, CIRCLE and ELLIPSE
*
* MATRIX not implemented
*/
boolean isDisp = dcmSR ? false : "DISPLAY".equalsIgnoreCase(go.getString(Tag.GraphicAnnotationUnits)); //$NON-NLS-1$
String type = go.getString(Tag.GraphicType);
Integer groupID = DicomMediaUtils.getIntegerFromDicomElement(go, Tag.GraphicGroupID, null);
boolean filled = getBooleanValue(go, Tag.GraphicFilled);
Attributes style = go.getNestedDataset(Tag.LineStyleSequence);
Float thickness = DicomMediaUtils.getFloatFromDicomElement(style, Tag.LineThickness, 1.0f);
Boolean dashed = style == null ? Boolean.FALSE : "DASHED".equalsIgnoreCase(style.getString(Tag.LinePattern)); //$NON-NLS-1$
if (style != null) {
float[] rgb = CIELab.convertToFloatLab(style.getInts(Tag.PatternOnColorCIELabValue));
if (rgb != null) {
color = PresentationStateReader.getRGBColor(255, rgb, (int[]) null);
}
}
Graphic shape = null;
float[] points = DicomMediaUtils.getFloatArrayFromDicomElement(go, Tag.GraphicData, null);
if (isDisp && inverse != null) {
float[] dstpoints = new float[points.length];
inverse.transform(points, 0, dstpoints, 0, points.length / 2);
points = dstpoints;
}
if (POLYLINE.equalsIgnoreCase(type)) {
if (points != null) {
int size = points.length / 2;
if (size >= 2) {
if (canBeEdited) {
List<Point2D.Double> handlePointList = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
double x = isDisp ? points[i * 2] * width : points[i * 2];
double y = isDisp ? points[i * 2 + 1] * height : points[i * 2 + 1];
handlePointList.add(new Point2D.Double(x, y));
}
if (dcmSR) {
// Always close polyline for DICOM SR
if (!handlePointList.get(0).equals(handlePointList.get(size - 1))) {
handlePointList.add((Point2D.Double) handlePointList.get(0).clone());
}
}
// Closed when the first point is the same as the last point
if (handlePointList.get(0).equals(handlePointList.get(size - 1))) {
shape = new PolygonGraphic().buildGraphic(handlePointList);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
} else {
shape = new PolylineGraphic().buildGraphic(handlePointList);
setProperties(shape, thickness, color, labelVisible, Boolean.FALSE, groupID);
}
} else {
Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO, size);
double x = isDisp ? points[0] * width : points[0];
double y = isDisp ? points[1] * height : points[1];
path.moveTo(x, y);
for (int i = 1; i < size; i++) {
x = isDisp ? points[i * 2] * width : points[i * 2];
y = isDisp ? points[i * 2 + 1] * height : points[i * 2 + 1];
path.lineTo(x, y);
}
if (dcmSR) {
// Always close polyline for DICOM SR
path.closePath();
}
shape = new NonEditableGraphic(path);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
}
}
} else if (ELLIPSE.equalsIgnoreCase(type)) {
if (points != null && points.length == 8) {
double majorX1 = isDisp ? points[0] * width : points[0];
double majorY1 = isDisp ? points[1] * height : points[1];
double majorX2 = isDisp ? points[2] * width : points[2];
double majorY2 = isDisp ? points[3] * height : points[3];
double cx = (majorX1 + majorX2) / 2;
double cy = (majorY1 + majorY2) / 2;
double rx = euclideanDistance(points, 0, 2, isDisp, width, height) / 2;
double ry = euclideanDistance(points, 4, 6, isDisp, width, height) / 2;
double rotation;
if (MathUtil.isEqual(majorX1, majorX2)) {
rotation = Math.PI / 2;
} else if (MathUtil.isEqual(majorY1, majorY2)) {
rotation = 0;
} else {
rotation = Math.atan2(majorY2 - cy, majorX2 - cx);
}
Shape ellipse = new Ellipse2D.Double();
((Ellipse2D) ellipse).setFrameFromCenter(cx, cy, cx + rx, cy + ry);
if (MathUtil.isDifferentFromZero(rotation)) {
AffineTransform rotate = AffineTransform.getRotateInstance(rotation, cx, cy);
ellipse = rotate.createTransformedShape(ellipse);
}
// Only ellipse without rotation can be edited
if (canBeEdited && Objects.equals(rotation, 0)) {
shape = new EllipseGraphic().buildGraphic(((Ellipse2D) ellipse).getFrame());
setProperties(shape, thickness, color, labelVisible, filled, groupID);
} else {
shape = new NonEditableGraphic(ellipse);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
}
} else if (CIRCLE.equalsIgnoreCase(type)) {
if (points != null && points.length == 4) {
double x = isDisp ? points[0] * width : points[0];
double y = isDisp ? points[1] * height : points[1];
Ellipse2D ellipse = new Ellipse2D.Double();
double dist = euclideanDistance(points, 0, 2, isDisp, width, height);
ellipse.setFrameFromCenter(x, y, x + dist, y + dist);
if (canBeEdited) {
shape = new EllipseGraphic().buildGraphic(ellipse.getFrame());
setProperties(shape, thickness, color, labelVisible, filled, groupID);
} else {
shape = new NonEditableGraphic(ellipse);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
}
} else if (POINT.equalsIgnoreCase(type)) {
if (points != null && points.length == 2) {
double x = isDisp ? points[0] * width : points[0];
double y = isDisp ? points[1] * height : points[1];
int pointSize = 3;
if (canBeEdited) {
shape = new PointGraphic().buildGraphic(Arrays.asList(new Point2D.Double(x, y)));
((PointGraphic) shape).setPointSize(pointSize);
setProperties(shape, thickness, color, labelVisible, Boolean.TRUE, groupID);
} else {
Ellipse2D ellipse =
new Ellipse2D.Double(x - pointSize / 2.0f, y - pointSize / 2.0f, pointSize, pointSize);
shape = new NonEditableGraphic(ellipse);
setProperties(shape, thickness, color, labelVisible, Boolean.TRUE, groupID);
}
}
} else if ("MULTIPOINT".equalsIgnoreCase(type)) { //$NON-NLS-1$
if (points != null && points.length >= 2) {
int size = points.length / 2;
int pointSize = 3;
Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO, size);
for (int i = 0; i < size; i++) {
double x = isDisp ? points[i * 2] * width : points[i * 2];
double y = isDisp ? points[i * 2 + 1] * height : points[i * 2 + 1];
Ellipse2D ellipse =
new Ellipse2D.Double(x - pointSize / 2.0f, y - pointSize / 2.0f, pointSize, pointSize);
path.append(ellipse, false);
}
shape = new NonEditableGraphic(path);
setProperties(shape, thickness, color, labelVisible, Boolean.TRUE, groupID);
}
} else if (INTERPOLATED.equalsIgnoreCase(type)) {
if (points != null && points.length >= 2) {
// Only non editable graphic (required control point tool)
int size = points.length / 2;
if (size >= 2) {
Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO, size);
double lx = isDisp ? points[0] * width : points[0];
double ly = isDisp ? points[1] * height : points[1];
path.moveTo(lx, ly);
for (int i = 1; i < size; i++) {
double x = isDisp ? points[i * 2] * width : points[i * 2];
double y = isDisp ? points[i * 2 + 1] * height : points[i * 2 + 1];
double dx = lx - x;
double dy = ly - y;
double dist = Math.sqrt(dx * dx + dy * dy);
double ux = -dy / dist;
double uy = dx / dist;
// Use 1/4 distance in the perpendicular direction
double cx = (lx + x) * 0.5 + dist * 0.25 * ux;
double cy = (ly + y) * 0.5 + dist * 0.25 * uy;
path.quadTo(cx, cy, x, y);
lx = x;
ly = y;
}
shape = new NonEditableGraphic(path);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
}
}
return shape;
}
public static boolean getBooleanValue(Attributes dcmobj, int tag) {
return "Y".equalsIgnoreCase(dcmobj.getString(tag)); //$NON-NLS-1$
}
private static void setProperties(Graphic shape, Float thickness, Color color, Boolean labelVisible, Boolean filled,
Integer classID) {
shape.setLineThickness(thickness);
shape.setPaint(color);
shape.setLabelVisible(labelVisible);
shape.setClassID(classID);
shape.setFilled(filled);
}
private static double euclideanDistance(float[] points, int p1, int p2, boolean isDisp, double width,
double height) {
float dx = points[p1] - points[p2];
float dy = points[p1 + 1] - points[p2 + 1];
if (isDisp) {
dx *= width;
dy *= height;
}
return Math.sqrt(dx * dx + dy * dy);
}
public static Graphic buildCompoundGraphic(Attributes go, Color color, boolean labelVisible, double width,
double height, AffineTransform inverse) throws InvalidShapeException {
/*
*
* Graphic Type: MULTILINE, INFINITELINE, CUTLINE, RANGELINE, RULER, AXIS, CROSSHAIR, ARROW, RECTANGLE and
* ELLIPSE
*
* Coordinates are DISPLAY or PIXEL
*/
boolean isDisp = "DISPLAY".equalsIgnoreCase(go.getString(Tag.CompoundGraphicUnits)); //$NON-NLS-1$
String type = go.getString(Tag.CompoundGraphicType);
String id = go.getString(Tag.CompoundGraphicInstanceID);
Integer groupID = DicomMediaUtils.getIntegerFromDicomElement(go, Tag.GraphicGroupID, null);
boolean filled = getBooleanValue(go, Tag.GraphicFilled);
Attributes style = go.getNestedDataset(Tag.LineStyleSequence);
Float thickness = DicomMediaUtils.getFloatFromDicomElement(style, Tag.LineThickness, 1.0f);
Boolean dashed = style == null ? Boolean.FALSE : "DASHED".equalsIgnoreCase(style.getString(Tag.LinePattern)); //$NON-NLS-1$
if (style != null) {
float[] rgb = CIELab.convertToFloatLab(style.getInts(Tag.PatternOnColorCIELabValue));
if (rgb != null) {
color = PresentationStateReader.getRGBColor(255, rgb, (int[]) null);
}
}
Graphic shape = null;
float[] points = DicomMediaUtils.getFloatArrayFromDicomElement(go, Tag.GraphicData, null);
if (isDisp && inverse != null) {
float[] dstpoints = new float[points.length];
inverse.transform(points, 0, dstpoints, 0, points.length / 2);
points = dstpoints;
}
if (POLYLINE.equalsIgnoreCase(type)) {
if (points != null) {
int size = points.length / 2;
if (size >= 2) {
Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO, size);
double x = isDisp ? points[0] * width : points[0];
double y = isDisp ? points[1] * height : points[1];
path.moveTo(x, y);
for (int i = 1; i < size; i++) {
x = isDisp ? points[i * 2] * width : points[i * 2];
y = isDisp ? points[i * 2 + 1] * height : points[i * 2 + 1];
path.lineTo(x, y);
}
shape = new NonEditableGraphic(path);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
}
} else if (ELLIPSE.equalsIgnoreCase(type)) {
if (points != null && points.length == 8) {
double majorX1 = isDisp ? points[0] * width : points[0];
double majorY1 = isDisp ? points[1] * height : points[1];
double majorX2 = isDisp ? points[2] * width : points[2];
double majorY2 = isDisp ? points[3] * height : points[3];
double cx = (majorX1 + majorX2) / 2;
double cy = (majorY1 + majorY2) / 2;
double rx = euclideanDistance(points, 0, 2, isDisp, width, height) / 2;
double ry = euclideanDistance(points, 4, 6, isDisp, width, height) / 2;
double rotation;
if (MathUtil.isEqual(majorX1, majorX2)) {
rotation = Math.PI / 2;
} else if (MathUtil.isEqual(majorY1, majorY2)) {
rotation = 0;
} else {
rotation = Math.atan2(majorY2 - cy, majorX2 - cx);
}
Shape ellipse = new Ellipse2D.Double();
((Ellipse2D) ellipse).setFrameFromCenter(cx, cy, cx + rx, cy + ry);
if (MathUtil.isDifferentFromZero(rotation)) {
AffineTransform rotate = AffineTransform.getRotateInstance(rotation, cx, cy);
ellipse = rotate.createTransformedShape(ellipse);
}
shape = new NonEditableGraphic(ellipse);
setProperties(shape, thickness, color, labelVisible, filled, groupID);
}
} else if (POINT.equalsIgnoreCase(type)) {
if (points != null && points.length == 2) {
double x = isDisp ? points[0] * width : points[0];
double y = isDisp ? points[1] * height : points[1];
int pointSize = 3;
Ellipse2D ellipse =
new Ellipse2D.Double(x - pointSize / 2.0f, y - pointSize / 2.0f, pointSize, pointSize);
shape = new NonEditableGraphic(ellipse);
setProperties(shape, thickness, color, labelVisible, Boolean.TRUE, groupID);
}
}
return shape;
}
public static GraphicModel getPresentationModel(Attributes dcmobj) {
if (dcmobj != null) {
String id = dcmobj.getString(PresentationStateReader.PRIVATE_CREATOR_TAG);
if (PresentationStateReader.PR_MODEL_ID.equals(id)) {
try {
return buildPresentationModel(dcmobj.getBytes(PresentationStateReader.PR_MODEL_PRIVATE_TAG));
} catch (Exception e) {
LOGGER.error("Cannot extract binary model: ", e); //$NON-NLS-1$
}
}
}
return null;
}
public static boolean applyPresentationModel(ImageElement img) {
if (img != null) {
byte[] prBinary = TagW.getTagValue(img, TagW.PresentationModelBirary, byte[].class);
if (prBinary != null) {
GraphicModel model = buildPresentationModel(prBinary);
img.setTag(TagW.PresentationModel, model);
img.setTag(TagW.PresentationModelBirary, null);
return true;
}
}
return false;
}
private static GraphicModel buildPresentationModel(byte[] binary) {
try {
JAXBContext jaxbContext = JAXBContext.newInstance(XmlGraphicModel.class);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
ByteArrayInputStream inputStream = new ByteArrayInputStream(GzipManager.gzipUncompressToByte(binary));
GraphicModel model = (GraphicModel) jaxbUnmarshaller.unmarshal(inputStream);
int length = model.getModels().size();
model.getModels().removeIf(g -> g.getLayer() == null);
if (length > model.getModels().size()) {
LOGGER.error("Removing {} graphics wihout a attached layer", model.getModels().size() - length); //$NON-NLS-1$
}
return model;
} catch (Exception e) {
LOGGER.error("Cannot load xml graphic model: ", e); //$NON-NLS-1$
}
return null;
}
}