/*******************************************************************************
* 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.viewer2d;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.RenderedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.swing.ButtonGroup;
import javax.swing.JPopupMenu;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.weasis.core.api.gui.util.ActionState;
import org.weasis.core.api.gui.util.ActionW;
import org.weasis.core.api.gui.util.ComboItemListener;
import org.weasis.core.api.gui.util.JMVUtils;
import org.weasis.core.api.gui.util.MathUtil;
import org.weasis.core.api.gui.util.RadioMenuItem;
import org.weasis.core.api.image.CropOp;
import org.weasis.core.api.image.ImageOpNode;
import org.weasis.core.api.image.SimpleOpManager;
import org.weasis.core.api.image.WindowOp;
import org.weasis.core.api.image.util.CIELab;
import org.weasis.core.api.image.util.Unit;
import org.weasis.core.api.media.data.MediaSeries;
import org.weasis.core.api.util.EscapeChars;
import org.weasis.core.ui.editor.image.ViewButton;
import org.weasis.core.ui.editor.image.ViewCanvas;
import org.weasis.core.ui.model.AbstractGraphicModel;
import org.weasis.core.ui.model.GraphicModel;
import org.weasis.core.ui.model.graphic.AbstractGraphic;
import org.weasis.core.ui.model.graphic.Graphic;
import org.weasis.core.ui.model.graphic.imp.AnnotationGraphic;
import org.weasis.core.ui.model.graphic.imp.PointGraphic;
import org.weasis.core.ui.model.layer.GraphicLayer;
import org.weasis.core.ui.model.layer.LayerType;
import org.weasis.core.ui.model.layer.imp.DefaultLayer;
import org.weasis.core.ui.model.utils.exceptions.InvalidShapeException;
import org.weasis.core.ui.util.TitleMenuItem;
import org.weasis.dicom.codec.DicomImageElement;
import org.weasis.dicom.codec.Messages;
import org.weasis.dicom.codec.PRSpecialElement;
import org.weasis.dicom.codec.PresentationStateReader;
import org.weasis.dicom.codec.TagD;
import org.weasis.dicom.codec.display.PresetWindowLevel;
import org.weasis.dicom.codec.utils.DicomMediaUtils;
import org.weasis.dicom.explorer.DicomModel;
import org.weasis.dicom.explorer.pr.PrGraphicUtil;
public class PRManager {
private static final Logger LOGGER = LoggerFactory.getLogger(PRManager.class);
public static final String PR_PRESETS = "pr.presets"; //$NON-NLS-1$
public static final String TAG_CHANGE_PIX_CONFIG = "change.pixel"; //$NON-NLS-1$
public static final String TAG_PR_ZOOM = "original.zoom"; //$NON-NLS-1$
public static final String TAG_DICOM_LAYERS = "pr.layers"; //$NON-NLS-1$
public static void applyPresentationState(ViewCanvas<DicomImageElement> view, PresentationStateReader reader,
DicomImageElement img) {
if (view == null || reader == null || img == null) {
return;
}
// TODO should move to the model
Map<String, Object> actionsInView = view.getActionsInView();
reader.applySpatialTransformationModule(actionsInView);
List<PresetWindowLevel> presets = reader.getPresetCollection(img);
ImageOpNode node = view.getDisplayOpManager().getNode(WindowOp.OP_NAME);
if (node != null) {
List<PresetWindowLevel> presetList =
img.getPresetList(JMVUtils.getNULLtoTrue(node.getParam(ActionW.IMAGE_PIX_PADDING.cmd())));
PresetWindowLevel auto = presets.remove(presets.size() - 1);
if (!presetList.get(presetList.size() - 1).equals(auto)) {
// It happens when PR contains a new Modality LUT
String name = Messages.getString("PresetWindowLevel.full"); //$NON-NLS-1$
presets.add(new PresetWindowLevel(name + " [PR]", auto.getWindow(), auto.getLevel(), auto.getShape())); //$NON-NLS-1$
}
presets.addAll(presetList);
}
PresetWindowLevel p = presets.get(0);
actionsInView.put(ActionW.WINDOW.cmd(), p.getWindow());
actionsInView.put(ActionW.LEVEL.cmd(), p.getLevel());
actionsInView.put(PRManager.PR_PRESETS, presets);
actionsInView.put(ActionW.PRESET.cmd(), p);
actionsInView.put(ActionW.LUT_SHAPE.cmd(), p.getLutShape());
actionsInView.put(ActionW.DEFAULT_PRESET.cmd(), true);
applyPixelSpacing(view, reader, img);
GraphicModel graphicModel = PrGraphicUtil.getPresentationModel(reader.getDcmobj());
// GraphicModel graphicModel = null;
List<GraphicLayer> layers =
graphicModel == null ? readGraphicAnnotation(view, reader, img) : readXmlModel(view, graphicModel);
if (layers != null) {
view.setActionsInView(PRManager.TAG_DICOM_LAYERS, layers);
}
}
private static void applyPixelSpacing(ViewCanvas<DicomImageElement> view, PresentationStateReader reader,
DicomImageElement img) {
Map<String, Object> actionsInView = view.getActionsInView();
reader.readDisplayArea(img);
String presentationMode = TagD.getTagValue(reader, Tag.PresentationSizeMode, String.class);
boolean trueSize = "TRUE SIZE".equalsIgnoreCase(presentationMode); //$NON-NLS-1$
double[] prPixSize = TagD.getTagValue(reader, Tag.PresentationPixelSpacing, double[].class);
if (prPixSize != null && prPixSize.length == 2 && prPixSize[0] > 0.0 && prPixSize[1] > 0.0) {
if (trueSize) {
img.setPixelSize(prPixSize[1], prPixSize[0]);
img.setPixelSpacingUnit(Unit.MILLIMETER);
actionsInView.put(PRManager.TAG_CHANGE_PIX_CONFIG, true);
ActionState spUnitAction = EventManager.getInstance().getAction(ActionW.SPATIAL_UNIT);
if (spUnitAction instanceof ComboItemListener) {
((ComboItemListener) spUnitAction).setSelectedItem(Unit.MILLIMETER);
}
} else {
applyAspectRatio(img, actionsInView, prPixSize);
}
}
if (prPixSize == null) {
int[] aspects = TagD.getTagValue(reader, Tag.PresentationPixelAspectRatio, int[].class);
if (aspects != null && aspects.length == 2) {
applyAspectRatio(img, actionsInView, new double[] { aspects[0], aspects[1] });
}
}
int[] tlhc = TagD.getTagValue(reader, Tag.DisplayedAreaTopLeftHandCorner, int[].class);
int[] brhc = TagD.getTagValue(reader, Tag.DisplayedAreaBottomRightHandCorner, int[].class);
// TODO http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.10.4.html
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.8.12.2.html
String pixelOriginInterpretation = TagD.getTagValue(reader, Tag.PixelOriginInterpretation, String.class);
if (tlhc != null && tlhc.length == 2 && brhc != null && brhc.length == 2) {
// Lots of systems encode topLeft as 1,1, even when they mean 0,0
if (tlhc[0] == 1) {
tlhc[0] = 0;
}
if (tlhc[1] == 1) {
tlhc[1] = 0;
}
Rectangle area = new Rectangle();
area.setFrameFromDiagonal(tlhc[0], tlhc[1], brhc[0], brhc[1]);
RenderedImage source = view.getSourceImage();
if (source != null) {
area = area.intersection(
new Rectangle(source.getMinX(), source.getMinY(), source.getWidth(), source.getHeight()));
if (area.width > 1 && area.height > 1 && !area.equals(view.getViewModel().getModelArea())) {
SimpleOpManager opManager =
Optional.ofNullable((SimpleOpManager) actionsInView.get(ActionW.PREPROCESSING.cmd()))
.orElseGet(SimpleOpManager::new);
CropOp crop = new CropOp();
crop.setParam(CropOp.P_AREA, area);
crop.setParam(CropOp.P_SHIFT_TO_ORIGIN, true);
opManager.addImageOperationAction(crop);
actionsInView.put(ActionW.PREPROCESSING.cmd(), opManager);
}
}
actionsInView.put(ActionW.CROP.cmd(), area);
actionsInView.put(CropOp.P_SHIFT_TO_ORIGIN, true);
}
if ("SCALE TO FIT".equalsIgnoreCase(presentationMode)) { //$NON-NLS-1$
actionsInView.put(PRManager.TAG_PR_ZOOM, -200.0);
} else if ("MAGNIFY".equalsIgnoreCase(presentationMode)) { //$NON-NLS-1$
Float val = TagD.getTagValue(reader, Tag.PresentationPixelMagnificationRatio, Float.class);
actionsInView.put(PRManager.TAG_PR_ZOOM, val == null ? 1.0 : val);
} else if (trueSize) {
// Required to calibrate the screen in preferences
actionsInView.put(PRManager.TAG_PR_ZOOM, -100.0);
}
}
private static void applyAspectRatio(DicomImageElement img, Map<String, Object> actionsInView, double[] aspects) {
double[] prevPixSize = img.getDisplayPixelSize();
if (MathUtil.isDifferent(aspects[0], aspects[1]) || MathUtil.isDifferent(prevPixSize[0], prevPixSize[1])) {
// set the aspects to the pixel size of the image to stretch the image rendering (square pixel)
double[] pixelsize;
if (aspects[1] < aspects[0]) {
pixelsize = new double[] { 1.0, aspects[0] / aspects[1] };
} else {
pixelsize = new double[] { aspects[1] / aspects[0], 1.0 };
}
img.setPixelSize(pixelsize[0], pixelsize[1]);
img.setPixelSpacingUnit(Unit.PIXEL);
actionsInView.put(PRManager.TAG_CHANGE_PIX_CONFIG, true);
// TODO update graphics
}
}
private static ArrayList<GraphicLayer> readXmlModel(ViewCanvas<DicomImageElement> view, GraphicModel graphicModel) {
ArrayList<GraphicLayer> layers = new ArrayList<>();
int k = 0;
for (GraphicLayer layer : graphicModel.getLayers()) {
layer.setName(Optional.ofNullable(layer.getName()).orElseGet(layer.getType()::getDefaultName) + " [DICOM]"); //$NON-NLS-1$
layer.setLocked(true);
layer.setSerializable(false);
layer.setLevel(270 + k++);
layers.add(layer);
}
for (Graphic g : graphicModel.getModels()) {
AbstractGraphicModel.addGraphicToModel(view, g.getLayer(), g);
}
return layers;
}
private static ArrayList<GraphicLayer> readGraphicAnnotation(ViewCanvas<DicomImageElement> view,
PresentationStateReader reader, DicomImageElement img) {
Map<String, Object> actionsInView = view.getActionsInView();
ArrayList<GraphicLayer> layers = null;
Attributes dcmobj = reader.getDcmobj();
if (dcmobj != null) {
Sequence gams = dcmobj.getSequence(Tag.GraphicAnnotationSequence);
Sequence layerSeqs = dcmobj.getSequence(Tag.GraphicLayerSequence);
if (gams != null && layerSeqs != null) {
Map<String, Attributes> glms = new HashMap<>(layerSeqs.size());
for (Attributes a : layerSeqs) {
glms.put(a.getString(Tag.GraphicLayer), a);
}
/*
* Apply spatial transformations (rotation, flip) AFTER when graphics are in PIXEL mode and BEFORE when
* graphics are in DISPLAY mode.
*/
int rotation = (Integer) actionsInView.getOrDefault(ActionW.ROTATION.cmd(), 0);
boolean flip = (Boolean) actionsInView.getOrDefault(ActionW.FLIP.cmd(), false);
Rectangle area = (Rectangle) actionsInView.get(ActionW.CROP.cmd());
Rectangle2D modelArea = view.getViewModel().getModelArea();
double width = area == null ? modelArea.getWidth() : area.getWidth();
double height = area == null ? modelArea.getHeight() : area.getHeight();
double offsetx = area == null ? 0.0 : area.getX() / area.getWidth();
double offsety = area == null ? 0.0 : area.getY() / area.getHeight();
AffineTransform inverse = null;
if (rotation != 0 || flip) {
// Create inverse transformation for display coordinates (will convert in real coordinates)
inverse = AffineTransform.getTranslateInstance(offsetx, offsety);
if (flip) {
inverse.scale(-1.0, 1.0);
inverse.translate(-1.0, 0.0);
}
if (rotation != 0) {
inverse.rotate(Math.toRadians(rotation), 0.5, 0.5);
}
}
layers = new ArrayList<>();
for (Attributes gram : gams) {
String graphicLayerName = gram.getString(Tag.GraphicLayer);
Attributes glm = glms.get(graphicLayerName);
if (glm == null || !PresentationStateReader.isModuleAppicable(gram, img)) {
continue;
}
GraphicLayer layer = new DefaultLayer(LayerType.DICOM_PR);
layer.setName(graphicLayerName + " [DICOM]"); //$NON-NLS-1$
layer.setSerializable(false);
layer.setLocked(true);
layer.setSelectable(false);
layer.setLevel(310 + glm.getInt(Tag.GraphicLayerOrder, 0));
layers.add(layer);
Color rgb = PresentationStateReader.getRGBColor(
glm.getInt(Tag.GraphicLayerRecommendedDisplayGrayscaleValue, 255),
CIELab.convertToFloatLab(DicomMediaUtils.getIntAyrrayFromDicomElement(glm,
Tag.GraphicLayerRecommendedDisplayCIELabValue, null)),
DicomMediaUtils.getIntAyrrayFromDicomElement(glm, Tag.GraphicLayerRecommendedDisplayRGBValue,
null));
Sequence gos = gram.getSequence(Tag.GraphicObjectSequence);
if (gos != null) {
for (Attributes go : gos) {
Graphic graphic;
try {
graphic =
PrGraphicUtil.buildGraphic(go, rgb, false, width, height, true, inverse, false);
if (graphic != null) {
AbstractGraphicModel.addGraphicToModel(view, layer, graphic);
}
} catch (InvalidShapeException e) {
LOGGER.error("Cannot create graphic: " + e.getMessage(), e); //$NON-NLS-1$
}
}
}
Sequence txos = gram.getSequence(Tag.TextObjectSequence);
if (txos != null) {
for (Attributes txo : txos) {
Attributes style = txo.getNestedDataset(Tag.LineStyleSequence);
Float thickness = DicomMediaUtils.getFloatFromDicomElement(style, Tag.LineThickness, 1.0f);
if (style != null) {
float[] lab = CIELab.convertToFloatLab(style.getInts(Tag.PatternOnColorCIELabValue));
if (lab != null) {
rgb = PresentationStateReader.getRGBColor(255, lab, (int[]) null);
}
}
String[] textLines = EscapeChars.convertToLines(txo.getString(Tag.UnformattedTextValue));
// MATRIX not implemented
boolean isDisp = "DISPLAY".equalsIgnoreCase(txo.getString(Tag.BoundingBoxAnnotationUnits)); //$NON-NLS-1$
float[] topLeft = txo.getFloats(Tag.BoundingBoxTopLeftHandCorner);
float[] bottomRight = txo.getFloats(Tag.BoundingBoxBottomRightHandCorner);
Rectangle2D rect = null;
if (topLeft != null && bottomRight != null) {
rect = new Rectangle2D.Double(topLeft[0], topLeft[1], bottomRight[0] - topLeft[0],
bottomRight[1] - topLeft[1]);
if (isDisp) {
rect.setFrame(rect.getX() * width, rect.getY() * height, rect.getWidth() * width,
rect.getHeight() * height);
if (inverse != null) {
float[] dstPt1 = new float[2];
float[] dstPt2 = new float[2];
inverse.transform(topLeft, 0, dstPt1, 0, 1);
inverse.transform(bottomRight, 0, dstPt2, 0, 1);
rect.setFrameFromDiagonal(dstPt1[0] * width, dstPt1[1] * height,
dstPt2[0] * width, dstPt2[1] * height);
}
}
}
float[] anchor = txo.getFloats(Tag.AnchorPoint);
if (anchor != null && anchor.length == 2) {
// MATRIX not implemented
boolean disp =
"DISPLAY".equalsIgnoreCase(txo.getString(Tag.AnchorPointAnnotationUnits)); //$NON-NLS-1$
double x = disp ? anchor[0] * width : anchor[0];
double y = disp ? anchor[1] * height : anchor[1];
Point2D.Double ptAnchor = new Point2D.Double(x, y);
/*
* Use the center of the box. Do not follow DICOM specs: displaying the bounding box
* even the text doesn't match. Does not make sense!
*/
Point2D.Double ptBox =
rect == null ? ptAnchor : new Point2D.Double(rect.getCenterX(), rect.getCenterY());
if (!PrGraphicUtil.getBooleanValue(txo, Tag.AnchorPointVisibility)) {
ptAnchor = null;
}
if (ptAnchor != null && ptAnchor.equals(ptBox)) {
ptBox = new Point2D.Double(ptAnchor.getX() + 20, ptAnchor.getY() + 50);
}
try {
List<Point2D.Double> pts = new ArrayList<>(2);
pts.add(ptAnchor);
pts.add(ptBox);
Graphic g = new AnnotationGraphic().buildGraphic(pts);
g.setPaint(rgb);
g.setLineThickness(thickness);
g.setLabelVisible(Boolean.TRUE);
g.setLabel(textLines, view);
AbstractGraphicModel.addGraphicToModel(view, layer, g);
} catch (InvalidShapeException e) {
LOGGER.error("Cannot create annotation: " + e.getMessage(), e); //$NON-NLS-1$
}
} else if (rect != null) {
try {
Point2D.Double point = new Point2D.Double(rect.getMinX(), rect.getMinY());
AbstractGraphic pt =
(AbstractGraphic) new PointGraphic().buildGraphic(Arrays.asList(point));
pt.setLineThickness(thickness);
pt.setLabelVisible(Boolean.TRUE);
AbstractGraphicModel.addGraphicToModel(view, layer, pt);
pt.setShape(null, null);
pt.setLabel(textLines, view, point);
} catch (InvalidShapeException e) {
LOGGER.error("Cannot create annotation: " + e.getMessage(), e); //$NON-NLS-1$
}
}
}
}
}
}
}
return layers;
}
/** Indicate if the graphic is to be filled in */
public static void deleteDicomLayers(List<GraphicLayer> layers, GraphicModel graphicManager) {
if (layers != null) {
for (GraphicLayer layer : layers) {
graphicManager.deleteByLayer(layer);
}
}
}
public static ViewButton buildPrSelection(final View2d view, MediaSeries<DicomImageElement> series,
DicomImageElement img) {
if (view != null && series != null && img != null) {
Object key = img.getKey();
List<PRSpecialElement> prList =
DicomModel.getPrSpecialElements(series, TagD.getTagValue(img, Tag.SOPInstanceUID, String.class),
key instanceof Integer ? (Integer) key + 1 : null);
if (!prList.isEmpty()) {
Object oldPR = view.getActionValue(ActionW.PR_STATE.cmd());
if (!ActionState.NoneLabel.NONE_SERIES.equals(oldPR)) {
// Set the previous selected value, otherwise set the more recent PR by default
view.setPresentationState(prList.indexOf(oldPR) == -1 ? prList.get(0) : oldPR, true);
}
int offset = series.size(null) > 1 ? 2 : 1;
final Object[] items = new Object[prList.size() + offset];
items[0] = ActionState.NoneLabel.NONE;
if (offset == 2) {
items[1] = ActionState.NoneLabel.NONE_SERIES;
}
for (int i = offset; i < items.length; i++) {
items[i] = prList.get(i - offset);
}
ViewButton prButton = new ViewButton((invoker, x, y) -> {
Object pr = view.getActionValue(ActionW.PR_STATE.cmd());
JPopupMenu popupMenu = new JPopupMenu();
TitleMenuItem itemTitle = new TitleMenuItem(ActionW.PR_STATE.getTitle(), popupMenu.getInsets());
popupMenu.add(itemTitle);
popupMenu.addSeparator();
ButtonGroup groupButtons = new ButtonGroup();
for (Object dcm : items) {
final RadioMenuItem menuItem = new RadioMenuItem(dcm.toString(), null, dcm, dcm == pr);
menuItem.addActionListener(e -> {
if (e.getSource() instanceof RadioMenuItem) {
RadioMenuItem item = (RadioMenuItem) e.getSource();
Object val = item.getUserObject();
view.setPresentationState(val, false);
}
});
groupButtons.add(menuItem);
popupMenu.add(menuItem);
}
popupMenu.show(invoker, x, y);
}, View2d.PR_ICON);
prButton.setVisible(true);
return prButton;
}
}
return null;
}
}